/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { isAbsolutePath, dirname, basename, joinPath, isEqual } from 'vs/base/common/resources'; import { localize } from 'vs/nls'; import { TernarySearchTree } from 'vs/base/common/map'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { getBaseLabel } from 'vs/base/common/labels'; import { ILogService } from 'vs/platform/log/common/log'; export class FileService2 extends Disposable implements IFileService { //#region TODO@Ben HACKS private _impl: IFileService; setImpl(service: IFileService): void { this._impl = this._register(service); this._register(service.onFileChanges(e => this._onFileChanges.fire(e))); this._register(service.onAfterOperation(e => this._onAfterOperation.fire(e))); } //#endregion _serviceBrand: ServiceIdentifier; constructor(@ILogService private logService: ILogService) { super(); } //#region File System Provider private _onDidChangeFileSystemProviderRegistrations: Emitter = this._register(new Emitter()); get onDidChangeFileSystemProviderRegistrations(): Event { return this._onDidChangeFileSystemProviderRegistrations.event; } private _onWillActivateFileSystemProvider: Emitter = this._register(new Emitter()); get onWillActivateFileSystemProvider(): Event { return this._onWillActivateFileSystemProvider.event; } private readonly provider = new Map(); registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { if (this.provider.has(scheme)) { throw new Error(`A provider for the scheme ${scheme} is already registered.`); } let legacyDisposal: IDisposable; if (this._impl) { legacyDisposal = this._impl.registerProvider(scheme, provider); } else { legacyDisposal = Disposable.None; } // Add provider with event this.provider.set(scheme, provider); this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider }); // Forward change events from provider const providerFileListener = provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes))); return combinedDisposable([ toDisposable(() => { this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider }); this.provider.delete(scheme); providerFileListener.dispose(); }), legacyDisposal ]); } async activateProvider(scheme: string): Promise { // Emit an event that we are about to activate a provider with the given scheme. // Listeners can participate in the activation by registering a provider for it. const joiners: Promise[] = []; this._onWillActivateFileSystemProvider.fire({ scheme, join(promise) { if (promise) { joiners.push(promise); } }, }); if (this.provider.has(scheme)) { return Promise.resolve(); // provider is already here so we can return directly } // If the provider is not yet there, make sure to join on the listeners assuming // that it takes a bit longer to register the file system provider. await Promise.all(joiners); } canHandleResource(resource: URI): boolean { return this.provider.has(resource.scheme); } private async withProvider(resource: URI): Promise { // Assert path is absolute if (!isAbsolutePath(resource)) { throw new FileOperationError( localize('invalidPath', "The path of resource '{0}' must be absolute", resource.toString(true)), FileOperationResult.FILE_INVALID_PATH ); } // Activate provider await this.activateProvider(resource.scheme); // Assert provider const provider = this.provider.get(resource.scheme); if (!provider) { const err = new Error(); err.name = 'ENOPRO'; err.message = `no provider for ${resource.toString()}`; return Promise.reject(err); } return provider; } //#endregion private _onAfterOperation: Emitter = this._register(new Emitter()); get onAfterOperation(): Event { return this._onAfterOperation.event; } //#region File Metadata Resolving async resolveFile(resource: URI, options?: IResolveFileOptions): Promise { try { return await this.doResolveFile(resource, options); } catch (error) { // Specially handle file not found case as file operation result if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) { throw new FileOperationError( localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), FileOperationResult.FILE_NOT_FOUND ); } // Bubble up any other error as is throw error; } } private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise { const provider = await this.withProvider(resource); // leverage a trie to check for recursive resolving const to = options && options.resolveTo; const trie = TernarySearchTree.forPaths(); trie.set(resource.toString(), true); if (isNonEmptyArray(to)) { to.forEach(uri => trie.set(uri.toString(), true)); } const stat = await provider.stat(resource); return await this.toFileStat(provider, resource, stat, undefined, (stat, siblings) => { // check for recursive resolving if (Boolean(trie.findSuperstr(stat.resource.toString()) || trie.get(stat.resource.toString()))) { return true; } // check for resolving single child folders if (stat.isDirectory && options && options.resolveSingleChildDescendants) { return siblings === 1; } return false; }); } private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise { // convert to file stat const fileStat: IFileStat = { resource, name: getBaseLabel(resource), isDirectory: (stat.type & FileType.Directory) !== 0, isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0, isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly), mtime: stat.mtime, size: stat.size, etag: stat.mtime.toString(29) + stat.size.toString(31), }; // check to recurse for directories if (fileStat.isDirectory && recurse(fileStat, siblings)) { try { const entries = await provider.readdir(resource); fileStat.children = await Promise.all(entries.map(async entry => { const childResource = joinPath(resource, entry[0]); const childStat = await provider.stat(childResource); return this.toFileStat(provider, childResource, childStat, entries.length, recurse); })); } catch (error) { this.logService.trace(error); fileStat.children = []; // gracefully handle errors, we may not have permissions to read } return fileStat; } return Promise.resolve(fileStat); } async resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise { // soft-groupBy, keep order, don't rearrange/merge groups const groups: Array = []; let group: typeof toResolve | undefined; for (const request of toResolve) { if (!group || group[0].resource.scheme !== request.resource.scheme) { group = []; groups.push(group); } group.push(request); } // resolve files const result: IResolveFileResult[] = []; for (const group of groups) { for (const groupEntry of group) { try { const stat = await this.doResolveFile(groupEntry.resource, groupEntry.options); result.push({ stat, success: true }); } catch (error) { this.logService.trace(error); result.push({ stat: undefined, success: false }); } } } return result; } async existsFile(resource: URI): Promise { try { await this.resolveFile(resource); return true; } catch (error) { return false; } } //#endregion //#region File Reading/Writing get encoding(): IResourceEncodings { return this._impl.encoding; } createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise { return this._impl.createFile(resource, content, options); } resolveContent(resource: URI, options?: IResolveContentOptions): Promise { return this._impl.resolveContent(resource, options); } resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise { return this._impl.resolveStreamContent(resource, options); } updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise { return this._impl.updateContent(resource, value, options); } //#endregion //#region Move/Copy/Delete/Create Folder moveFile(source: URI, target: URI, overwrite?: boolean): Promise { return this._impl.moveFile(source, target, overwrite); } copyFile(source: URI, target: URI, overwrite?: boolean): Promise { return this._impl.copyFile(source, target, overwrite); } async createFolder(resource: URI): Promise { const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); // mkdir recursively await this.mkdirp(provider, resource); // events const fileStat = await this.resolveFile(resource); this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); return fileStat; } private async mkdirp(provider: IFileSystemProvider, directory: URI): Promise { const directoriesToCreate: string[] = []; // mkdir until we reach root while (!isEqual(directory, dirname(directory))) { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { throw new Error(`${directory.toString()} exists, but is not a directory`); } break; // we have hit a directory that exists -> good } catch (error) { // Bubble up any other error that is not file not found if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) { throw error; } // Upon error, remember directories that need to be created directoriesToCreate.push(basename(directory)); // Continue up directory = dirname(directory); } } // Create directories as needed for (let i = directoriesToCreate.length - 1; i >= 0; i--) { directory = joinPath(directory, directoriesToCreate[i]); await provider.mkdir(directory); } } async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise { if (options && options.useTrash) { return this._impl.del(resource, options); //TODO@ben this is https://github.com/Microsoft/vscode/issues/48259 } const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); // Delete through provider await provider.delete(resource, { recursive: !!(options && options.recursive) }); // Events this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); } //#endregion //#region File Watching private _onFileChanges: Emitter = this._register(new Emitter()); get onFileChanges(): Event { return this._onFileChanges.event; } watchFileChanges(resource: URI): void { this._impl.watchFileChanges(resource); } unwatchFileChanges(resource: URI): void { this._impl.unwatchFileChanges(resource); } //#endregion //#region Helpers private throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED); } return provider; } //#endregion } registerSingleton(IFileService, FileService2);