blob: cb94bae32119525f5305c6a2f02e73533be05280 [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 FileSystemWorkspaceBinding {
/**
* @param {!Persistence.IsolatedFileSystemManager} isolatedFileSystemManager
* @param {!Workspace.Workspace} workspace
*/
constructor(isolatedFileSystemManager, workspace) {
this._isolatedFileSystemManager = isolatedFileSystemManager;
this._workspace = workspace;
this._eventListeners = [
this._isolatedFileSystemManager.addEventListener(
Persistence.IsolatedFileSystemManager.Events.FileSystemAdded, this._onFileSystemAdded, this),
this._isolatedFileSystemManager.addEventListener(
Persistence.IsolatedFileSystemManager.Events.FileSystemRemoved, this._onFileSystemRemoved, this),
this._isolatedFileSystemManager.addEventListener(
Persistence.IsolatedFileSystemManager.Events.FileSystemFilesChanged, this._fileSystemFilesChanged, this)
];
/** @type {!Map.<string, !FileSystem>} */
this._boundFileSystems = new Map();
this._isolatedFileSystemManager.waitForFileSystems().then(this._onFileSystemsLoaded.bind(this));
}
/**
* @param {string} fileSystemPath
* @return {string}
*/
static projectId(fileSystemPath) {
return fileSystemPath;
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!Array<string>}
*/
static relativePath(uiSourceCode) {
const baseURL =
/** @type {!FileSystem}*/ (uiSourceCode.project())._fileSystemBaseURL;
return uiSourceCode.url().substring(baseURL.length).split('/');
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {string}
*/
static tooltipForUISourceCode(uiSourceCode) {
const fileSystem =
/** @type {!FileSystem}*/ (uiSourceCode.project())._fileSystem;
return fileSystem.tooltipForURL(uiSourceCode.url());
}
/**
* @param {!Workspace.Project} project
* @return {string}
*/
static fileSystemType(project) {
const fileSystem =
/** @type {!FileSystem}*/ (project)._fileSystem;
return fileSystem.type();
}
/**
* @param {!Workspace.Project} project
* @return {boolean}
*/
static fileSystemSupportsAutomapping(project) {
const fileSystem =
/** @type {!FileSystem}*/ (project)._fileSystem;
return fileSystem.supportsAutomapping();
}
/**
* @param {!Workspace.Project} project
* @param {string} relativePath
* @return {string}
*/
static completeURL(project, relativePath) {
const fsProject = /** @type {!FileSystem}*/ (project);
return fsProject._fileSystemBaseURL + relativePath;
}
/**
* @param {string} projectId
* @return {string}
*/
static fileSystemPath(projectId) {
return projectId;
}
/**
* @return {!Persistence.IsolatedFileSystemManager}
*/
fileSystemManager() {
return this._isolatedFileSystemManager;
}
/**
* @param {!Array<!Persistence.IsolatedFileSystem>} fileSystems
*/
_onFileSystemsLoaded(fileSystems) {
for (const fileSystem of fileSystems) {
this._addFileSystem(fileSystem);
}
}
/**
* @param {!Common.Event} event
*/
_onFileSystemAdded(event) {
const fileSystem = /** @type {!Persistence.PlatformFileSystem} */ (event.data);
this._addFileSystem(fileSystem);
}
/**
* @param {!Persistence.PlatformFileSystem} fileSystem
*/
_addFileSystem(fileSystem) {
const boundFileSystem = new FileSystem(this, fileSystem, this._workspace);
this._boundFileSystems.set(fileSystem.path(), boundFileSystem);
}
/**
* @param {!Common.Event} event
*/
_onFileSystemRemoved(event) {
const fileSystem = /** @type {!Persistence.PlatformFileSystem} */ (event.data);
const boundFileSystem = this._boundFileSystems.get(fileSystem.path());
boundFileSystem.dispose();
this._boundFileSystems.remove(fileSystem.path());
}
/**
* @param {!Common.Event} event
*/
_fileSystemFilesChanged(event) {
const paths = /** @type {!Persistence.IsolatedFileSystemManager.FilesChangedData} */ (event.data);
for (const fileSystemPath of paths.changed.keysArray()) {
const fileSystem = this._boundFileSystems.get(fileSystemPath);
if (!fileSystem) {
continue;
}
paths.changed.get(fileSystemPath).forEach(path => fileSystem._fileChanged(path));
}
for (const fileSystemPath of paths.added.keysArray()) {
const fileSystem = this._boundFileSystems.get(fileSystemPath);
if (!fileSystem) {
continue;
}
paths.added.get(fileSystemPath).forEach(path => fileSystem._fileChanged(path));
}
for (const fileSystemPath of paths.removed.keysArray()) {
const fileSystem = this._boundFileSystems.get(fileSystemPath);
if (!fileSystem) {
continue;
}
paths.removed.get(fileSystemPath).forEach(path => fileSystem.removeUISourceCode(path));
}
}
dispose() {
Common.EventTarget.removeEventListeners(this._eventListeners);
for (const fileSystem of this._boundFileSystems.values()) {
fileSystem.dispose();
this._boundFileSystems.remove(fileSystem._fileSystem.path());
}
}
}
/**
* @implements {Workspace.Project}
* @unrestricted
*/
export class FileSystem extends Workspace.ProjectStore {
/**
* @param {!FileSystemWorkspaceBinding} fileSystemWorkspaceBinding
* @param {!Persistence.PlatformFileSystem} isolatedFileSystem
* @param {!Workspace.Workspace} workspace
*/
constructor(fileSystemWorkspaceBinding, isolatedFileSystem, workspace) {
const fileSystemPath = isolatedFileSystem.path();
const id = FileSystemWorkspaceBinding.projectId(fileSystemPath);
console.assert(!workspace.project(id));
const displayName = fileSystemPath.substr(fileSystemPath.lastIndexOf('/') + 1);
super(workspace, id, Workspace.projectTypes.FileSystem, displayName);
this._fileSystem = isolatedFileSystem;
this._fileSystemBaseURL = this._fileSystem.path() + '/';
this._fileSystemParentURL = this._fileSystemBaseURL.substr(0, fileSystemPath.lastIndexOf('/') + 1);
this._fileSystemWorkspaceBinding = fileSystemWorkspaceBinding;
this._fileSystemPath = fileSystemPath;
/** @type {!Set<string>} */
this._creatingFilesGuard = new Set();
workspace.addProject(this);
this.populate();
}
/**
* @return {string}
*/
fileSystemPath() {
return this._fileSystemPath;
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {string}
*/
mimeType(uiSourceCode) {
return this._fileSystem.mimeFromPath(uiSourceCode.url());
}
/**
* @return {!Array<string>}
*/
initialGitFolders() {
return this._fileSystem.initialGitFolders().map(folder => this._fileSystemPath + '/' + folder);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {string}
*/
_filePathForUISourceCode(uiSourceCode) {
return uiSourceCode.url().substring(this._fileSystemPath.length);
}
/**
* @override
* @return {boolean}
*/
isServiceProject() {
return false;
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!Promise<?Workspace.UISourceCodeMetadata>}
*/
requestMetadata(uiSourceCode) {
if (uiSourceCode[_metadata]) {
return uiSourceCode[_metadata];
}
const relativePath = this._filePathForUISourceCode(uiSourceCode);
const promise = this._fileSystem.getMetadata(relativePath).then(onMetadata);
uiSourceCode[_metadata] = promise;
return promise;
/**
* @param {?{modificationTime: !Date, size: number}} metadata
* @return {?Workspace.UISourceCodeMetadata}
*/
function onMetadata(metadata) {
if (!metadata) {
return null;
}
return new Workspace.UISourceCodeMetadata(metadata.modificationTime, metadata.size);
}
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!Promise<?Blob>}
*/
requestFileBlob(uiSourceCode) {
return this._fileSystem.requestFileBlob(this._filePathForUISourceCode(uiSourceCode));
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @returns {!Promise<!Common.DeferredContent>}
*/
requestFileContent(uiSourceCode) {
const filePath = this._filePathForUISourceCode(uiSourceCode);
return this._fileSystem.requestFileContent(filePath);
}
/**
* @override
* @return {boolean}
*/
canSetFileContent() {
return true;
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {string} newContent
* @param {boolean} isBase64
* @return {!Promise}
*/
async setFileContent(uiSourceCode, newContent, isBase64) {
const filePath = this._filePathForUISourceCode(uiSourceCode);
await this._fileSystem.setFileContent(filePath, newContent, isBase64);
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {string}
*/
fullDisplayName(uiSourceCode) {
const baseURL =
/** @type {!FileSystem}*/ (uiSourceCode.project())._fileSystemParentURL;
return uiSourceCode.url().substring(baseURL.length);
}
/**
* @override
* @return {boolean}
*/
canRename() {
return true;
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {string} newName
* @param {function(boolean, string=, string=, !Common.ResourceType=)} callback
*/
rename(uiSourceCode, newName, callback) {
if (newName === uiSourceCode.name()) {
callback(true, uiSourceCode.name(), uiSourceCode.url(), uiSourceCode.contentType());
return;
}
let filePath = this._filePathForUISourceCode(uiSourceCode);
this._fileSystem.renameFile(filePath, newName, innerCallback.bind(this));
/**
* @param {boolean} success
* @param {string=} newName
* @this {FileSystem}
*/
function innerCallback(success, newName) {
if (!success || !newName) {
callback(false, newName);
return;
}
console.assert(newName);
const slash = filePath.lastIndexOf('/');
const parentPath = filePath.substring(0, slash);
filePath = parentPath + '/' + newName;
filePath = filePath.substr(1);
const newURL = this._fileSystemBaseURL + filePath;
const newContentType = this._fileSystem.contentType(newName);
this.renameUISourceCode(uiSourceCode, newName);
callback(true, newName, newURL, newContentType);
}
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {string} query
* @param {boolean} caseSensitive
* @param {boolean} isRegex
* @return {!Promise<!Array<!Common.ContentProvider.SearchMatch>>}
*/
async searchInFileContent(uiSourceCode, query, caseSensitive, isRegex) {
const filePath = this._filePathForUISourceCode(uiSourceCode);
const {content} = await this._fileSystem.requestFileContent(filePath);
if (content) {
return Common.ContentProvider.performSearchInContent(content, query, caseSensitive, isRegex);
}
return [];
}
/**
* @override
* @param {!Workspace.ProjectSearchConfig} searchConfig
* @param {!Array.<string>} filesMathingFileQuery
* @param {!Common.Progress} progress
* @return {!Promise<!Array<string>>}
*/
async findFilesMatchingSearchRequest(searchConfig, filesMathingFileQuery, progress) {
let result = filesMathingFileQuery;
const queriesToRun = searchConfig.queries().slice();
if (!queriesToRun.length) {
queriesToRun.push('');
}
progress.setTotalWork(queriesToRun.length);
for (const query of queriesToRun) {
const files = await this._fileSystem.searchInPath(searchConfig.isRegex() ? '' : query, progress);
result = result.intersectOrdered(files.sort(), String.naturalOrderComparator);
progress.worked(1);
}
progress.done();
return result;
}
/**
* @override
* @param {!Common.Progress} progress
*/
indexContent(progress) {
this._fileSystem.indexContent(progress);
}
populate() {
const chunkSize = 1000;
const filePaths = this._fileSystem.initialFilePaths();
reportFileChunk.call(this, 0);
/**
* @param {number} from
* @this {FileSystem}
*/
function reportFileChunk(from) {
const to = Math.min(from + chunkSize, filePaths.length);
for (let i = from; i < to; ++i) {
this._addFile(filePaths[i]);
}
if (to < filePaths.length) {
setTimeout(reportFileChunk.bind(this, to), 100);
}
}
}
/**
* @override
* @param {string} url
*/
excludeFolder(url) {
let relativeFolder = url.substring(this._fileSystemBaseURL.length);
if (!relativeFolder.startsWith('/')) {
relativeFolder = '/' + relativeFolder;
}
if (!relativeFolder.endsWith('/')) {
relativeFolder += '/';
}
this._fileSystem.addExcludedFolder(relativeFolder);
const uiSourceCodes = this.uiSourceCodes().slice();
for (let i = 0; i < uiSourceCodes.length; ++i) {
const uiSourceCode = uiSourceCodes[i];
if (uiSourceCode.url().startsWith(url)) {
this.removeUISourceCode(uiSourceCode.url());
}
}
}
/**
* @override
* @param {string} path
* @return {boolean}
*/
canExcludeFolder(path) {
return this._fileSystem.canExcludeFolder(path);
}
/**
* @override
* @return {boolean}
*/
canCreateFile() {
return true;
}
/**
* @override
* @param {string} path
* @param {?string} name
* @param {string} content
* @param {boolean=} isBase64
* @return {!Promise<?Workspace.UISourceCode>}
*/
async createFile(path, name, content, isBase64) {
const guardFileName = this._fileSystemPath + path + (!path.endsWith('/') ? '/' : '') + name;
this._creatingFilesGuard.add(guardFileName);
const filePath = await this._fileSystem.createFile(path, name);
if (!filePath) {
return null;
}
const uiSourceCode = this._addFile(filePath);
uiSourceCode.setContent(content, !!isBase64);
this._creatingFilesGuard.delete(guardFileName);
return uiSourceCode;
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
*/
deleteFile(uiSourceCode) {
const relativePath = this._filePathForUISourceCode(uiSourceCode);
this._fileSystem.deleteFile(relativePath).then(success => {
if (success) {
this.removeUISourceCode(uiSourceCode.url());
}
});
}
/**
* @override
*/
remove() {
this._fileSystemWorkspaceBinding._isolatedFileSystemManager.removeFileSystem(this._fileSystem);
}
/**
* @param {string} filePath
* @return {!Workspace.UISourceCode}
*/
_addFile(filePath) {
const contentType = this._fileSystem.contentType(filePath);
const uiSourceCode = this.createUISourceCode(this._fileSystemBaseURL + filePath, contentType);
this.addUISourceCode(uiSourceCode);
return uiSourceCode;
}
/**
* @param {string} path
*/
_fileChanged(path) {
// Ignore files that are being created but do not have content yet.
if (this._creatingFilesGuard.has(path)) {
return;
}
const uiSourceCode = this.uiSourceCodeForURL(path);
if (!uiSourceCode) {
const contentType = this._fileSystem.contentType(path);
this.addUISourceCode(this.createUISourceCode(path, contentType));
return;
}
uiSourceCode[_metadata] = null;
uiSourceCode.checkContentUpdated();
}
/**
* @param {string} url
* @return {string}
*/
tooltipForURL(url) {
return this._fileSystem.tooltipForURL(url);
}
dispose() {
this.removeProject();
}
}
const _metadata = Symbol('FileSystemWorkspaceBinding.Metadata');
/* Legacy exported object */
self.Persistence = self.Persistence || {};
/* Legacy exported object */
Persistence = Persistence || {};
/** @constructor */
Persistence.FileSystemWorkspaceBinding = FileSystemWorkspaceBinding;
/** @constructor */
Persistence.FileSystemWorkspaceBinding.FileSystem = FileSystem;