blob: cf1c6883c76c93dd36a095c9c2ff3178128162cd [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
export default class IsolatedFileSystem extends Persistence.PlatformFileSystem {
/**
* @param {!Persistence.IsolatedFileSystemManager} manager
* @param {string} path
* @param {string} embedderPath
* @param {!DOMFileSystem} domFileSystem
* @param {string} type
*/
constructor(manager, path, embedderPath, domFileSystem, type) {
super(path, type);
this._manager = manager;
this._embedderPath = embedderPath;
this._domFileSystem = domFileSystem;
this._excludedFoldersSetting = Common.settings.createLocalSetting('workspaceExcludedFolders', {});
/** @type {!Set<string>} */
this._excludedFolders = new Set(this._excludedFoldersSetting.get()[path] || []);
/** @type {!Array<string>} */
this._excludedEmbedderFolders = [];
/** @type {!Set<string>} */
this._initialFilePaths = new Set();
/** @type {!Set<string>} */
this._initialGitFolders = new Set();
/** @type {!Map<string, !Promise>} */
this._fileLocks = new Map();
}
/**
* @param {!Persistence.IsolatedFileSystemManager} manager
* @param {string} path
* @param {string} embedderPath
* @param {string} type
* @param {string} name
* @param {string} rootURL
* @return {!Promise<?IsolatedFileSystem>}
*/
static create(manager, path, embedderPath, type, name, rootURL) {
const domFileSystem = Host.InspectorFrontendHost.isolatedFileSystem(name, rootURL);
if (!domFileSystem) {
return Promise.resolve(/** @type {?IsolatedFileSystem} */ (null));
}
const fileSystem = new IsolatedFileSystem(manager, path, embedderPath, domFileSystem, type);
return fileSystem._initializeFilePaths()
.then(() => fileSystem)
.catchException(/** @type {?IsolatedFileSystem} */ (null));
}
/**
* @param {!DOMError} error
* @return {string}
*/
static errorMessage(error) {
return Common.UIString('File system error: %s', error.message);
}
/**
* @template T
* @param {string} path
* @param {function():!Promise<T>} operation
* @return {!Promise<T>}
*/
_serializedFileOperation(path, operation) {
const promise = Promise.resolve(this._fileLocks.get(path)).then(() => operation.call(null));
this._fileLocks.set(path, promise);
return promise;
}
/**
* @override
* @param {string} path
* @return {!Promise<?{modificationTime: !Date, size: number}>}
*/
getMetadata(path) {
let fulfill;
const promise = new Promise(f => fulfill = f);
this._domFileSystem.root.getFile(path, undefined, fileEntryLoaded, errorHandler);
return promise;
/**
* @param {!FileEntry} entry
*/
function fileEntryLoaded(entry) {
entry.getMetadata(fulfill, errorHandler);
}
/**
* @param {!FileError} error
*/
function errorHandler(error) {
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(errorMessage + ' when getting file metadata \'' + path);
fulfill(null);
}
}
/**
* @override
* @return {!Array<string>}
*/
initialFilePaths() {
return this._initialFilePaths.valuesArray();
}
/**
* @override
* @return {!Array<string>}
*/
initialGitFolders() {
return this._initialGitFolders.valuesArray();
}
/**
* @override
* @return {string}
*/
embedderPath() {
return this._embedderPath;
}
/**
* @return {!Promise}
*/
_initializeFilePaths() {
let fulfill;
const promise = new Promise(x => fulfill = x);
let pendingRequests = 1;
const boundInnerCallback = innerCallback.bind(this);
this._requestEntries('', boundInnerCallback);
return promise;
/**
* @param {!Array.<!FileEntry>} entries
* @this {IsolatedFileSystem}
*/
function innerCallback(entries) {
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
if (!entry.isDirectory) {
if (this.isFileExcluded(entry.fullPath)) {
continue;
}
this._initialFilePaths.add(entry.fullPath.substr(1));
} else {
if (entry.fullPath.endsWith('/.git')) {
const lastSlash = entry.fullPath.lastIndexOf('/');
const parentFolder = entry.fullPath.substring(1, lastSlash);
this._initialGitFolders.add(parentFolder);
}
if (this.isFileExcluded(entry.fullPath + '/')) {
this._excludedEmbedderFolders.push(
Common.ParsedURL.urlToPlatformPath(this.path() + entry.fullPath, Host.isWin()));
continue;
}
++pendingRequests;
this._requestEntries(entry.fullPath, boundInnerCallback);
}
}
if ((--pendingRequests === 0)) {
fulfill();
}
}
}
/**
* @param {string} folderPath
* @return {!Promise<?DirectoryEntry>}
*/
async _createFoldersIfNotExist(folderPath) {
// Fast-path. If parent directory already exists we return it immidiatly.
let dirEntry = await new Promise(
resolve => this._domFileSystem.root.getDirectory(folderPath, undefined, resolve, () => resolve(null)));
if (dirEntry) {
return dirEntry;
}
const paths = folderPath.split('/');
let activePath = '';
for (const path of paths) {
activePath = activePath + '/' + path;
dirEntry = await this._innerCreateFolderIfNeeded(activePath);
if (!dirEntry) {
return null;
}
}
return dirEntry;
}
/**
* @param {string} path
* @return {!Promise<?DirectoryEntry>}
*/
_innerCreateFolderIfNeeded(path) {
return new Promise(resolve => {
this._domFileSystem.root.getDirectory(path, {create: true}, dirEntry => resolve(dirEntry), error => {
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(errorMessage + ' trying to create directory \'' + path + '\'');
resolve(null);
});
});
}
/**
* @override
* @param {string} path
* @param {?string} name
* @return {!Promise<?string>}
*/
async createFile(path, name) {
const dirEntry = await this._createFoldersIfNotExist(path);
if (!dirEntry) {
return null;
}
const fileEntry = await this._serializedFileOperation(path, createFileCandidate.bind(this, name || 'NewFile'));
if (!fileEntry) {
return null;
}
return fileEntry.fullPath.substr(1);
/**
* @param {string} name
* @param {number=} newFileIndex
* @return {!Promise<?FileEntry>}
* @this {IsolatedFileSystem}
*/
function createFileCandidate(name, newFileIndex) {
return new Promise(resolve => {
const nameCandidate = name + (newFileIndex || '');
dirEntry.getFile(nameCandidate, {create: true, exclusive: true}, resolve, error => {
if (error.name === 'InvalidModificationError') {
resolve(createFileCandidate.call(this, name, (newFileIndex ? newFileIndex + 1 : 1)));
return;
}
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(
errorMessage + ' when testing if file exists \'' + (this.path() + '/' + path + '/' + nameCandidate) +
'\'');
resolve(null);
});
});
}
}
/**
* @override
* @param {string} path
* @return {!Promise<boolean>}
*/
deleteFile(path) {
let resolveCallback;
const promise = new Promise(resolve => resolveCallback = resolve);
this._domFileSystem.root.getFile(path, undefined, fileEntryLoaded.bind(this), errorHandler.bind(this));
return promise;
/**
* @param {!FileEntry} fileEntry
* @this {IsolatedFileSystem}
*/
function fileEntryLoaded(fileEntry) {
fileEntry.remove(fileEntryRemoved, errorHandler.bind(this));
}
function fileEntryRemoved() {
resolveCallback(true);
}
/**
* @param {!FileError} error
* @this {IsolatedFileSystem}
* @suppress {checkTypes}
* TODO(jsbell): Update externs replacing FileError with DOMException. https://crbug.com/496901
*/
function errorHandler(error) {
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(errorMessage + ' when deleting file \'' + (this.path() + '/' + path) + '\'');
resolveCallback(false);
}
}
/**
* @override
* @param {string} path
* @return {!Promise<?Blob>}
*/
requestFileBlob(path) {
return new Promise(resolve => {
this._domFileSystem.root.getFile(path, undefined, entry => {
entry.file(resolve, errorHandler.bind(this));
}, errorHandler.bind(this));
/**
* @this {IsolatedFileSystem}
*/
function errorHandler(error) {
if (error.name === 'NotFoundError') {
resolve(null);
return;
}
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(errorMessage + ' when getting content for file \'' + (this.path() + '/' + path) + '\'');
resolve(null);
}
});
}
/**
* @override
* @param {string} path
* @returns {!Promise<!Common.DeferredContent>}
*/
requestFileContent(path) {
return this._serializedFileOperation(path, () => this._innerRequestFileContent(path));
}
/**
* @param {string} path
* @return {!Promise<!Common.DeferredContent>}
*/
async _innerRequestFileContent(path) {
const blob = await this.requestFileBlob(path);
if (!blob) {
return {error: ls`Blob could not be loaded.`, isEncoded: false};
}
const reader = new FileReader();
const extension = Common.ParsedURL.extractExtension(path);
const encoded = Persistence.IsolatedFileSystem.BinaryExtensions.has(extension);
const readPromise = new Promise(x => reader.onloadend = x);
if (encoded) {
reader.readAsBinaryString(blob);
} else {
reader.readAsText(blob);
}
await readPromise;
if (reader.error) {
const error = ls`Can't read file: ${path}: ${reader.error}`;
console.error(error);
return {isEncoded: false, error};
}
let result = null;
let error = null;
try {
result = /** @type {string} */ (reader.result);
} catch (e) {
result = null;
error = ls`Can't read file: ${path}: ${e.message}`;
}
if (result === undefined || result === null) {
error = error || ls`Unknown error reading file: ${path}`;
console.error(error);
return {isEncoded: false, error};
}
return {isEncoded: encoded, content: encoded ? btoa(result) : result};
}
/**
* @override
* @param {string} path
* @param {string} content
* @param {boolean} isBase64
*/
async setFileContent(path, content, isBase64) {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.FileSavedInWorkspace);
let callback;
const innerSetFileContent = () => {
const promise = new Promise(x => callback = x);
this._domFileSystem.root.getFile(path, {create: true}, fileEntryLoaded.bind(this), errorHandler.bind(this));
return promise;
};
this._serializedFileOperation(path, innerSetFileContent);
/**
* @param {!FileEntry} entry
* @this {IsolatedFileSystem}
*/
function fileEntryLoaded(entry) {
entry.createWriter(fileWriterCreated.bind(this), errorHandler.bind(this));
}
/**
* @param {!FileWriter} fileWriter
* @this {IsolatedFileSystem}
*/
async function fileWriterCreated(fileWriter) {
fileWriter.onerror = errorHandler.bind(this);
fileWriter.onwriteend = fileWritten;
let blob;
if (isBase64) {
blob = await (await fetch(`data:application/octet-stream;base64,${content}`)).blob();
} else {
blob = new Blob([content], {type: 'text/plain'});
}
fileWriter.write(blob);
function fileWritten() {
fileWriter.onwriteend = callback;
fileWriter.truncate(blob.size);
}
}
/**
* @this {IsolatedFileSystem}
*/
function errorHandler(error) {
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(errorMessage + ' when setting content for file \'' + (this.path() + '/' + path) + '\'');
callback();
}
}
/**
* @override
* @param {string} path
* @param {string} newName
* @param {function(boolean, string=)} callback
*/
renameFile(path, newName, callback) {
newName = newName ? newName.trim() : newName;
if (!newName || newName.indexOf('/') !== -1) {
callback(false);
return;
}
let fileEntry;
let dirEntry;
this._domFileSystem.root.getFile(path, undefined, fileEntryLoaded.bind(this), errorHandler.bind(this));
/**
* @param {!FileEntry} entry
* @this {IsolatedFileSystem}
*/
function fileEntryLoaded(entry) {
if (entry.name === newName) {
callback(false);
return;
}
fileEntry = entry;
fileEntry.getParent(dirEntryLoaded.bind(this), errorHandler.bind(this));
}
/**
* @param {!Entry} entry
* @this {IsolatedFileSystem}
*/
function dirEntryLoaded(entry) {
dirEntry = entry;
dirEntry.getFile(newName, null, newFileEntryLoaded, newFileEntryLoadErrorHandler.bind(this));
}
/**
* @param {!FileEntry} entry
*/
function newFileEntryLoaded(entry) {
callback(false);
}
/**
* @this {IsolatedFileSystem}
*/
function newFileEntryLoadErrorHandler(error) {
if (error.name !== 'NotFoundError') {
callback(false);
return;
}
fileEntry.moveTo(dirEntry, newName, fileRenamed, errorHandler.bind(this));
}
/**
* @param {!FileEntry} entry
*/
function fileRenamed(entry) {
callback(true, entry.name);
}
/**
* @this {IsolatedFileSystem}
*/
function errorHandler(error) {
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(errorMessage + ' when renaming file \'' + (this.path() + '/' + path) + '\' to \'' + newName + '\'');
callback(false);
}
}
/**
* @param {!DirectoryEntry} dirEntry
* @param {function(!Array.<!FileEntry>)} callback
*/
_readDirectory(dirEntry, callback) {
const dirReader = dirEntry.createReader();
let entries = [];
function innerCallback(results) {
if (!results.length) {
callback(entries.sort());
} else {
entries = entries.concat(toArray(results));
dirReader.readEntries(innerCallback, errorHandler);
}
}
function toArray(list) {
return Array.prototype.slice.call(list || [], 0);
}
dirReader.readEntries(innerCallback, errorHandler);
function errorHandler(error) {
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(errorMessage + ' when reading directory \'' + dirEntry.fullPath + '\'');
callback([]);
}
}
/**
* @param {string} path
* @param {function(!Array.<!FileEntry>)} callback
*/
_requestEntries(path, callback) {
this._domFileSystem.root.getDirectory(path, undefined, innerCallback.bind(this), errorHandler);
/**
* @param {!DirectoryEntry} dirEntry
* @this {IsolatedFileSystem}
*/
function innerCallback(dirEntry) {
this._readDirectory(dirEntry, callback);
}
function errorHandler(error) {
const errorMessage = IsolatedFileSystem.errorMessage(error);
console.error(errorMessage + ' when requesting entry \'' + path + '\'');
callback([]);
}
}
_saveExcludedFolders() {
const settingValue = this._excludedFoldersSetting.get();
settingValue[this.path()] = this._excludedFolders.valuesArray();
this._excludedFoldersSetting.set(settingValue);
}
/**
* @override
* @param {string} path
*/
addExcludedFolder(path) {
this._excludedFolders.add(path);
this._saveExcludedFolders();
this._manager.dispatchEventToListeners(Persistence.IsolatedFileSystemManager.Events.ExcludedFolderAdded, path);
}
/**
* @override
* @param {string} path
*/
removeExcludedFolder(path) {
this._excludedFolders.delete(path);
this._saveExcludedFolders();
this._manager.dispatchEventToListeners(Persistence.IsolatedFileSystemManager.Events.ExcludedFolderRemoved, path);
}
/**
* @override
*/
fileSystemRemoved() {
const settingValue = this._excludedFoldersSetting.get();
delete settingValue[this.path()];
this._excludedFoldersSetting.set(settingValue);
}
/**
* @override
* @param {string} folderPath
* @return {boolean}
*/
isFileExcluded(folderPath) {
if (this._excludedFolders.has(folderPath)) {
return true;
}
const regex = this._manager.workspaceFolderExcludePatternSetting().asRegExp();
return !!(regex && regex.test(folderPath));
}
/**
* @override
* @return {!Set<string>}
*/
excludedFolders() {
return this._excludedFolders;
}
/**
* @override
* @param {string} query
* @param {!Common.Progress} progress
* @return {!Promise<!Array<string>>}
*/
searchInPath(query, progress) {
return new Promise(resolve => {
const requestId = this._manager.registerCallback(innerCallback);
Host.InspectorFrontendHost.searchInPath(requestId, this._embedderPath, query);
/**
* @param {!Array<string>} files
*/
function innerCallback(files) {
resolve(files.map(path => Common.ParsedURL.platformPathToURL(path)));
progress.worked(1);
}
});
}
/**
* @override
* @param {!Common.Progress} progress
*/
indexContent(progress) {
progress.setTotalWork(1);
const requestId = this._manager.registerProgress(progress);
Host.InspectorFrontendHost.indexPath(requestId, this._embedderPath, JSON.stringify(this._excludedEmbedderFolders));
}
/**
* @override
* @param {string} path
* @return {string}
*/
mimeFromPath(path) {
return Common.ResourceType.mimeFromURL(path) || 'text/plain';
}
/**
* @override
* @param {string} path
* @return {boolean}
*/
canExcludeFolder(path) {
return !!path && this.type() !== 'overrides';
}
/**
* @override
* @param {string} path
* @return {!Common.ResourceType}
*/
contentType(path) {
const extension = Common.ParsedURL.extractExtension(path);
if (_styleSheetExtensions.has(extension)) {
return Common.resourceTypes.Stylesheet;
}
if (_documentExtensions.has(extension)) {
return Common.resourceTypes.Document;
}
if (ImageExtensions.has(extension)) {
return Common.resourceTypes.Image;
}
if (_scriptExtensions.has(extension)) {
return Common.resourceTypes.Script;
}
return BinaryExtensions.has(extension) ? Common.resourceTypes.Other : Common.resourceTypes.Document;
}
/**
* @override
* @param {string} url
* @return {string}
*/
tooltipForURL(url) {
const path = Common.ParsedURL.urlToPlatformPath(url, Host.isWin()).trimMiddle(150);
return ls`Linked to ${path}`;
}
/**
* @override
* @return {boolean}
*/
supportsAutomapping() {
return this.type() !== 'overrides';
}
}
const _styleSheetExtensions = new Set(['css', 'scss', 'sass', 'less']);
const _documentExtensions = new Set(['htm', 'html', 'asp', 'aspx', 'phtml', 'jsp']);
const _scriptExtensions = new Set([
'asp', 'aspx', 'c', 'cc', 'cljs', 'coffee', 'cpp', 'cs', 'dart', 'java', 'js',
'jsp', 'jsx', 'h', 'm', 'mjs', 'mm', 'py', 'sh', 'ts', 'tsx', 'ls'
]);
const ImageExtensions = new Set(['jpeg', 'jpg', 'svg', 'gif', 'webp', 'png', 'ico', 'tiff', 'tif', 'bmp']);
export const BinaryExtensions = new Set([
// Executable extensions, roughly taken from https://en.wikipedia.org/wiki/Comparison_of_executable_file_formats
'cmd', 'com', 'exe',
// Archive extensions, roughly taken from https://en.wikipedia.org/wiki/List_of_archive_formats
'a', 'ar', 'iso', 'tar', 'bz2', 'gz', 'lz', 'lzma', 'z', '7z', 'apk', 'arc', 'cab', 'dmg', 'jar', 'pak', 'rar', 'zip',
// Audio file extensions, roughly taken from https://en.wikipedia.org/wiki/Audio_file_format#List_of_formats
'3gp', 'aac', 'aiff', 'flac', 'm4a', 'mmf', 'mp3', 'ogg', 'oga', 'raw', 'sln', 'wav', 'wma', 'webm',
// Video file extensions, roughly taken from https://en.wikipedia.org/wiki/Video_file_format
'mkv', 'flv', 'vob', 'ogv', 'gifv', 'avi', 'mov', 'qt', 'mp4', 'm4p', 'm4v', 'mpg', 'mpeg',
// Image file extensions
'jpeg', 'jpg', 'gif', 'webp', 'png', 'ico', 'tiff', 'tif', 'bmp'
]);
/* Legacy exported object */
self.Persistence = self.Persistence || {};
/* Legacy exported object */
Persistence = Persistence || {};
/** @constructor */
Persistence.IsolatedFileSystem = IsolatedFileSystem;
Persistence.IsolatedFileSystem.BinaryExtensions = BinaryExtensions;