UNPKG

@theia/filesystem

Version:
916 lines • 72.8 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2020 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/platform/files/common/fileService.ts // and https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/textfile/browser/textFileService.ts // and https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts // and https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts // and https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts Object.defineProperty(exports, "__esModule", { value: true }); exports.FileService = exports.TextFileOperationError = exports.FileServiceContribution = void 0; const tslib_1 = require("tslib"); /* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable no-null/no-null */ /* eslint-disable @typescript-eslint/tslint/config */ /* eslint-disable @typescript-eslint/no-explicit-any */ const inversify_1 = require("@theia/core/shared/inversify"); const promise_util_1 = require("@theia/core/lib/common/promise-util"); const cancellation_1 = require("@theia/core/lib/common/cancellation"); const disposable_1 = require("@theia/core/lib/common/disposable"); const event_1 = require("@theia/core/lib/common/event"); const contribution_provider_1 = require("@theia/core/lib/common/contribution-provider"); const ternary_search_tree_1 = require("@theia/core/lib/common/ternary-search-tree"); const files_1 = require("../common/files"); const buffer_1 = require("@theia/core/lib/common/buffer"); const stream_1 = require("@theia/core/lib/common/stream"); const label_provider_1 = require("@theia/core/lib/browser/label-provider"); const filesystem_preferences_1 = require("./filesystem-preferences"); const progress_service_1 = require("@theia/core/lib/common/progress-service"); const delegating_file_system_provider_1 = require("../common/delegating-file-system-provider"); const encoding_registry_1 = require("@theia/core/lib/browser/encoding-registry"); const encodings_1 = require("@theia/core/lib/common/encodings"); const encoding_service_1 = require("@theia/core/lib/common/encoding-service"); const io_1 = require("../common/io"); const filesystem_watcher_error_handler_1 = require("./filesystem-watcher-error-handler"); const filesystem_utils_1 = require("../common/filesystem-utils"); const core_1 = require("@theia/core"); exports.FileServiceContribution = Symbol('FileServiceContribution'); class TextFileOperationError extends files_1.FileOperationError { constructor(message, textFileOperationResult, options) { super(message, 11 /* FileOperationResult.FILE_OTHER_ERROR */); this.textFileOperationResult = textFileOperationResult; this.options = options; Object.setPrototypeOf(this, TextFileOperationError.prototype); } } exports.TextFileOperationError = TextFileOperationError; /** * The {@link FileService} is the common facade responsible for all interactions with file systems. * It manages all registered {@link FileSystemProvider}s and * forwards calls to the responsible {@link FileSystemProvider}, determined by the scheme. * For additional documentation regarding the provided functions see also {@link FileSystemProvider}. */ let FileService = class FileService { constructor() { this.BUFFER_SIZE = 64 * 1024; // #region Events this.correlationIds = 0; this.onWillRunUserOperationEmitter = new event_1.AsyncEmitter(); /** * An event that is emitted when file operation is being performed. * This event is triggered by user gestures. */ this.onWillRunUserOperation = this.onWillRunUserOperationEmitter.event; this.onDidFailUserOperationEmitter = new event_1.AsyncEmitter(); /** * An event that is emitted when file operation is failed. * This event is triggered by user gestures. */ this.onDidFailUserOperation = this.onDidFailUserOperationEmitter.event; this.onDidRunUserOperationEmitter = new event_1.AsyncEmitter(); /** * An event that is emitted when file operation is finished. * This event is triggered by user gestures. */ this.onDidRunUserOperation = this.onDidRunUserOperationEmitter.event; // #endregion // #region File System Provider this.onDidChangeFileSystemProviderRegistrationsEmitter = new event_1.Emitter(); this.onDidChangeFileSystemProviderRegistrations = this.onDidChangeFileSystemProviderRegistrationsEmitter.event; this.onWillActivateFileSystemProviderEmitter = new event_1.Emitter(); /** * See `FileServiceContribution.registerProviders`. */ this.onWillActivateFileSystemProvider = this.onWillActivateFileSystemProviderEmitter.event; this.onDidChangeFileSystemProviderCapabilitiesEmitter = new event_1.Emitter(); this.onDidChangeFileSystemProviderCapabilities = this.onDidChangeFileSystemProviderCapabilitiesEmitter.event; this.onDidChangeFileSystemProviderReadOnlyMessageEmitter = new event_1.Emitter(); this.onDidChangeFileSystemProviderReadOnlyMessage = this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.event; this.providers = new Map(); this.activations = new Map(); // #endregion this.onDidRunOperationEmitter = new event_1.Emitter(); /** * An event that is emitted when operation is finished. * This event is triggered by user gestures and programmatically. */ this.onDidRunOperation = this.onDidRunOperationEmitter.event; // #endregion // #region File Watching this.onDidFilesChangeEmitter = new event_1.Emitter(); this.activeWatchers = new Map(); // #endregion // #region Helpers this.writeQueues = new Map(); // #endregion // #region File operation participants this.participants = []; } init() { for (const contribution of this.contributions.getContributions()) { contribution.registerFileSystemProviders(this); } } /** * Registers a new {@link FileSystemProvider} for the given scheme. * @param scheme The (uri) scheme for which the provider should be registered. * @param provider The file system provider that should be registered. * * @returns A `Disposable` that can be invoked to unregister the given provider. */ registerProvider(scheme, provider) { if (this.providers.has(scheme)) { throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`); } this.providers.set(scheme, provider); this.onDidChangeFileSystemProviderRegistrationsEmitter.fire({ added: true, scheme, provider }); const providerDisposables = new disposable_1.DisposableCollection(); providerDisposables.push(provider.onDidChangeFile(changes => this.onDidFilesChangeEmitter.fire(new files_1.FileChangesEvent(changes)))); providerDisposables.push(provider.onFileWatchError(() => this.handleFileWatchError())); providerDisposables.push(provider.onDidChangeCapabilities(() => this.onDidChangeFileSystemProviderCapabilitiesEmitter.fire({ provider, scheme }))); if (files_1.ReadOnlyMessageFileSystemProvider.is(provider)) { providerDisposables.push(provider.onDidChangeReadOnlyMessage(message => this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.fire({ provider, scheme, message }))); } return disposable_1.Disposable.create(() => { this.onDidChangeFileSystemProviderRegistrationsEmitter.fire({ added: false, scheme, provider }); this.providers.delete(scheme); providerDisposables.dispose(); }); } /** * Try to activate the registered provider for the given scheme * @param scheme The uri scheme for which the responsible provider should be activated. * * @returns A promise of the activated file system provider. Only resolves if a provider is available for this scheme, gets rejected otherwise. */ async activateProvider(scheme) { let provider = this.providers.get(scheme); if (provider) { return provider; } let activation = this.activations.get(scheme); if (!activation) { const deferredActivation = new promise_util_1.Deferred(); this.activations.set(scheme, activation = deferredActivation.promise); event_1.WaitUntilEvent.fire(this.onWillActivateFileSystemProviderEmitter, { scheme }).then(() => { provider = this.providers.get(scheme); if (!provider) { const error = new Error(); error.name = 'ENOPRO'; error.message = `No file system provider found for scheme ${scheme}`; throw error; } else { deferredActivation.resolve(provider); } }).catch(e => deferredActivation.reject(e)); } return activation; } hasProvider(scheme) { return this.providers.has(scheme); } /** * Tests if the service (i.e. any of its registered {@link FileSystemProvider}s) can handle the given resource. * @param resource `URI` of the resource to test. * * @returns `true` if the resource can be handled, `false` otherwise. */ canHandleResource(resource) { return this.providers.has(resource.scheme); } getReadOnlyMessage(resource) { const provider = this.providers.get(resource.scheme); if (files_1.ReadOnlyMessageFileSystemProvider.is(provider)) { return provider.readOnlyMessage; } return undefined; } /** * Tests if the service (i.e the {@link FileSystemProvider} registered for the given uri scheme) provides the given capability. * @param resource `URI` of the resource to test. * @param capability The required capability. * * @returns `true` if the resource can be handled and the required capability can be provided. */ hasCapability(resource, capability) { const provider = this.providers.get(resource.scheme); return !!(provider && (provider.capabilities & capability)); } /** * List the schemes and capabilities for registered file system providers */ listCapabilities() { return Array.from(this.providers.entries()).map(([scheme, provider]) => ({ scheme, capabilities: provider.capabilities })); } async withProvider(resource) { // Assert path is absolute if (!resource.path.isAbsolute) { throw new files_1.FileOperationError(core_1.nls.localizeByDefault("Unable to resolve filesystem provider with relative file path '{0}'", this.resourceForError(resource)), 8 /* FileOperationResult.FILE_INVALID_PATH */); } return this.activateProvider(resource.scheme); } async withReadProvider(resource) { const provider = await this.withProvider(resource); if ((0, files_1.hasOpenReadWriteCloseCapability)(provider) || (0, files_1.hasReadWriteCapability)(provider)) { return provider; } throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`); } async withWriteProvider(resource) { const provider = await this.withProvider(resource); if ((0, files_1.hasOpenReadWriteCloseCapability)(provider) || (0, files_1.hasReadWriteCapability)(provider)) { return provider; } throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`); } async resolve(resource, options) { try { return await this.doResolveFile(resource, options); } catch (error) { // Specially handle file not found case as file operation result if ((0, files_1.toFileSystemProviderErrorCode)(error) === files_1.FileSystemProviderErrorCode.FileNotFound) { throw new files_1.FileOperationError(core_1.nls.localizeByDefault("Unable to resolve nonexistent file '{0}'", this.resourceForError(resource)), 1 /* FileOperationResult.FILE_NOT_FOUND */); } // Bubble up any other error as is throw (0, files_1.ensureFileSystemProviderError)(error); } } async doResolveFile(resource, options) { const provider = await this.withProvider(resource); const resolveTo = options === null || options === void 0 ? void 0 : options.resolveTo; const resolveSingleChildDescendants = options === null || options === void 0 ? void 0 : options.resolveSingleChildDescendants; const resolveMetadata = options === null || options === void 0 ? void 0 : options.resolveMetadata; const stat = await provider.stat(resource); let trie; return this.toFileStat(provider, resource, stat, undefined, !!resolveMetadata, (stat, siblings) => { // lazy trie to check for recursive resolving if (!trie) { trie = ternary_search_tree_1.TernarySearchTree.forUris(!!(provider.capabilities & 1024 /* FileSystemProviderCapabilities.PathCaseSensitive */)); trie.set(resource, true); if (Array.isArray(resolveTo) && resolveTo.length) { resolveTo.forEach(uri => trie.set(uri, true)); } } // check for recursive resolving if (Boolean(trie.findSuperstr(stat.resource) || trie.get(stat.resource))) { return true; } // check for resolving single child folders if (stat.isDirectory && resolveSingleChildDescendants) { return siblings === 1; } return false; }); } async toFileStat(provider, resource, stat, siblings, resolveMetadata, recurse) { const fileStat = files_1.FileStat.fromStat(resource, stat); // check to recurse for directories if (fileStat.isDirectory && recurse(fileStat, siblings)) { try { const entries = await provider.readdir(resource); const resolvedEntries = await Promise.all(entries.map(async ([name, type]) => { try { const childResource = resource.resolve(name); const childStat = resolveMetadata ? await provider.stat(childResource) : { type }; return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse); } catch (error) { console.trace(error); return null; // can happen e.g. due to permission errors } })); // make sure to get rid of null values that signal a failure to resolve a particular entry fileStat.children = resolvedEntries.filter(e => !!e); } catch (error) { console.trace(error); fileStat.children = []; // gracefully handle errors, we may not have permissions to read } return fileStat; } return fileStat; } async resolveAll(toResolve) { return Promise.all(toResolve.map(async (entry) => { try { return { stat: await this.doResolveFile(entry.resource, entry.options), success: true }; } catch (error) { console.trace(error); return { stat: undefined, success: false }; } })); } /** * Tests if the given resource exists in the filesystem. * @param resource `URI` of the resource which should be tested. * @throws Will throw an error if no {@link FileSystemProvider} is registered for the given resource. * * @returns A promise that resolves to `true` if the resource exists. */ async exists(resource) { const provider = await this.withProvider(resource); try { const stat = await provider.stat(resource); return !!stat; } catch (error) { return false; } } /** * Tests a user's permissions for the given resource. * @param resource `URI` of the resource which should be tested. * @param mode An optional integer that specifies the accessibility checks to be performed. * Check `FileAccess.Constants` for possible values of mode. * It is possible to create a mask consisting of the bitwise `OR` of two or more values (e.g. FileAccess.Constants.W_OK | FileAccess.Constants.R_OK). * If `mode` is not defined, `FileAccess.Constants.F_OK` will be used instead. */ async access(resource, mode) { const provider = await this.withProvider(resource); if (!(0, files_1.hasAccessCapability)(provider)) { return false; } try { await provider.access(resource, mode); return true; } catch (error) { return false; } } /** * Resolves the fs path of the given URI. * * USE WITH CAUTION: You should always prefer URIs to paths if possible, as they are * portable and platform independent. Paths should only be used in cases you directly * interact with the OS, e.g. when running a command on the shell. * * If you need to display human readable simple or long names then use `LabelProvider` instead. * @param resource `URI` of the resource that should be resolved. * @throws Will throw an error if no {@link FileSystemProvider} is registered for the given resource. * * @returns A promise of the resolved fs path. */ async fsPath(resource) { const provider = await this.withProvider(resource); if (!(0, files_1.hasAccessCapability)(provider)) { return resource.path.toString(); } return provider.fsPath(resource); } // #region Text File Reading/Writing async create(resource, value, options) { if ((options === null || options === void 0 ? void 0 : options.fromUserGesture) === false) { return this.doCreate(resource, value, options); } await this.runFileOperationParticipants(resource, undefined, 0 /* FileOperation.CREATE */); const event = { correlationId: this.correlationIds++, operation: 0 /* FileOperation.CREATE */, target: resource }; await this.onWillRunUserOperationEmitter.fire(event); let stat; try { stat = await this.doCreate(resource, value, options); } catch (error) { await this.onDidFailUserOperationEmitter.fire(event); throw error; } await this.onDidRunUserOperationEmitter.fire(event); return stat; } async doCreate(resource, value, options) { const encoding = await this.getWriteEncoding(resource, options); const encoded = await this.encodingService.encodeStream(value, encoding); return this.createFile(resource, encoded, options); } async write(resource, value, options) { const encoding = await this.getWriteEncoding(resource, options); const encoded = await this.encodingService.encodeStream(value, encoding); return Object.assign(await this.writeFile(resource, encoded, options), { encoding: encoding.encoding }); } async read(resource, options) { const [bufferStream, decoder] = await this.doRead(resource, { ...options, preferUnbuffered: this.shouldReadUnbuffered(options) }); return { ...bufferStream, encoding: decoder.detected.encoding || encodings_1.UTF8, value: await (0, stream_1.consumeStream)(decoder.stream, strings => strings.join('')) }; } async readStream(resource, options) { const [bufferStream, decoder] = await this.doRead(resource, options); return { ...bufferStream, encoding: decoder.detected.encoding || encodings_1.UTF8, value: decoder.stream }; } async doRead(resource, options) { options = this.resolveReadOptions(options); // read stream raw (either buffered or unbuffered) let bufferStream; if (options === null || options === void 0 ? void 0 : options.preferUnbuffered) { const content = await this.readFile(resource, options); bufferStream = { ...content, value: buffer_1.BinaryBufferReadableStream.fromBuffer(content.value) }; } else { bufferStream = await this.readFileStream(resource, options); } const decoder = await this.encodingService.decodeStream(bufferStream.value, { guessEncoding: options.autoGuessEncoding, overwriteEncoding: detectedEncoding => this.getReadEncoding(resource, options, detectedEncoding) }); // validate binary if ((options === null || options === void 0 ? void 0 : options.acceptTextOnly) && decoder.detected.seemsBinary) { throw new TextFileOperationError(core_1.nls.localizeByDefault('File seems to be binary and cannot be opened as text'), 0 /* TextFileOperationResult.FILE_IS_BINARY */, options); } return [bufferStream, decoder]; } resolveReadOptions(options) { options = { ...options, autoGuessEncoding: typeof (options === null || options === void 0 ? void 0 : options.autoGuessEncoding) === 'boolean' ? options.autoGuessEncoding : this.preferences['files.autoGuessEncoding'] }; const limits = options.limits = options.limits || {}; if (typeof limits.size !== 'number') { limits.size = this.preferences['files.maxFileSizeMB'] * 1024 * 1024; } return options; } async update(resource, changes, options) { const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); try { await this.validateWriteFile(provider, resource, options); if ((0, files_1.hasUpdateCapability)(provider)) { const encoding = await this.getEncodingForResource(resource, options ? options.encoding : undefined); ; const stat = await provider.updateFile(resource, changes, { readEncoding: options.readEncoding, writeEncoding: encoding, overwriteEncoding: options.overwriteEncoding || false }); return Object.assign(files_1.FileStat.fromStat(resource, stat), { encoding: stat.encoding }); } else { throw new Error('incremental file update is not supported'); } } catch (error) { this.rethrowAsFileOperationError("Unable to write file '{0}' ({1})", resource, error, options); } } // #endregion // #region File Reading/Writing async createFile(resource, bufferOrReadableOrStream = buffer_1.BinaryBuffer.fromString(''), options) { // validate overwrite if (!(options === null || options === void 0 ? void 0 : options.overwrite) && await this.exists(resource)) { throw new files_1.FileOperationError(core_1.nls.localizeByDefault("Unable to create file '{0}' that already exists when overwrite flag is not set", this.resourceForError(resource)), 3 /* FileOperationResult.FILE_MODIFIED_SINCE */, options); } // do write into file (this will create it too) const fileStat = await this.writeFile(resource, bufferOrReadableOrStream); // events this.onDidRunOperationEmitter.fire(new files_1.FileOperationEvent(resource, 0 /* FileOperation.CREATE */, fileStat)); return fileStat; } async writeFile(resource, bufferOrReadableOrStream, options) { const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); try { // validate write const stat = await this.validateWriteFile(provider, resource, options); // mkdir recursively as needed if (!stat) { await this.mkdirp(provider, resource.parent); } // optimization: if the provider has unbuffered write capability and the data // to write is a Readable, we consume up to 3 chunks and try to write the data // unbuffered to reduce the overhead. If the Readable has more data to provide // we continue to write buffered. let bufferOrReadableOrStreamOrBufferedStream; if ((0, files_1.hasReadWriteCapability)(provider) && !(bufferOrReadableOrStream instanceof buffer_1.BinaryBuffer)) { if ((0, stream_1.isReadableStream)(bufferOrReadableOrStream)) { const bufferedStream = await (0, stream_1.peekStream)(bufferOrReadableOrStream, 3); if (bufferedStream.ended) { bufferOrReadableOrStreamOrBufferedStream = buffer_1.BinaryBuffer.concat(bufferedStream.buffer); } else { bufferOrReadableOrStreamOrBufferedStream = bufferedStream; } } else { bufferOrReadableOrStreamOrBufferedStream = (0, stream_1.peekReadable)(bufferOrReadableOrStream, data => buffer_1.BinaryBuffer.concat(data), 3); } } else { bufferOrReadableOrStreamOrBufferedStream = bufferOrReadableOrStream; } // write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability) if (!(0, files_1.hasOpenReadWriteCloseCapability)(provider) || ((0, files_1.hasReadWriteCapability)(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof buffer_1.BinaryBuffer)) { await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream); } // write file: buffered else { await this.doWriteBuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream instanceof buffer_1.BinaryBuffer ? buffer_1.BinaryBufferReadable.fromBuffer(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream); } } catch (error) { this.rethrowAsFileOperationError("Unable to write file '{0}' ({1})", resource, error, options); } return this.resolve(resource, { resolveMetadata: true }); } async validateWriteFile(provider, resource, options) { let stat = undefined; try { stat = await provider.stat(resource); } catch (error) { return undefined; // file might not exist } // file cannot be directory if ((stat.type & files_1.FileType.Directory) !== 0) { throw new files_1.FileOperationError(core_1.nls.localizeByDefault("Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), 0 /* FileOperationResult.FILE_IS_DIRECTORY */, options); } if (this.modifiedSince(stat, options)) { throw new files_1.FileOperationError(core_1.nls.localizeByDefault('File Modified Since'), 3 /* FileOperationResult.FILE_MODIFIED_SINCE */, options); } return stat; } /** * Dirty write prevention: if the file on disk has been changed and does not match our expected * mtime and etag, we bail out to prevent dirty writing. * * First, we check for a mtime that is in the future before we do more checks. The assumption is * that only the mtime is an indicator for a file that has changed on disk. * * Second, if the mtime has advanced, we compare the size of the file on disk with our previous * one using the etag() function. Relying only on the mtime check has proven to produce false * positives due to file system weirdness (especially around remote file systems). As such, the * check for size is a weaker check because it can return a false negative if the file has changed * but to the same length. This is a compromise we take to avoid having to produce checksums of * the file content for comparison which would be much slower to compute. */ modifiedSince(stat, options) { return !!options && typeof options.mtime === 'number' && typeof options.etag === 'string' && options.etag !== files_1.ETAG_DISABLED && typeof stat.mtime === 'number' && typeof stat.size === 'number' && options.mtime < stat.mtime && options.etag !== (0, files_1.etag)({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size }); } shouldReadUnbuffered(options) { // optimization: since we know that the caller does not // care about buffering, we indicate this to the reader. // this reduces all the overhead the buffered reading // has (open, read, close) if the provider supports // unbuffered reading. // // However, if we read only part of the file we still // want buffered reading as otherwise we need to read // the whole file and cut out the specified part later. return (options === null || options === void 0 ? void 0 : options.position) === undefined && (options === null || options === void 0 ? void 0 : options.length) === undefined; } async readFile(resource, options) { const provider = await this.withReadProvider(resource); const stream = await this.doReadAsFileStream(provider, resource, { ...options, preferUnbuffered: this.shouldReadUnbuffered(options) }); return { ...stream, value: await buffer_1.BinaryBufferReadableStream.toBuffer(stream.value) }; } async readFileStream(resource, options) { const provider = await this.withReadProvider(resource); return this.doReadAsFileStream(provider, resource, options); } async doReadAsFileStream(provider, resource, options) { // install a cancellation token that gets cancelled // when any error occurs. this allows us to resolve // the content of the file while resolving metadata // but still cancel the operation in certain cases. const cancellableSource = new cancellation_1.CancellationTokenSource(); // validate read operation const statPromise = this.validateReadFile(resource, options).then(stat => stat, error => { cancellableSource.cancel(); throw error; }); try { // if the etag is provided, we await the result of the validation // due to the likelyhood of hitting a NOT_MODIFIED_SINCE result. // otherwise, we let it run in parallel to the file reading for // optimal startup performance. if (options && typeof options.etag === 'string' && options.etag !== files_1.ETAG_DISABLED) { await statPromise; } let fileStreamPromise; // read unbuffered (only if either preferred, or the provider has no buffered read capability) if (!((0, files_1.hasOpenReadWriteCloseCapability)(provider) || (0, files_1.hasFileReadStreamCapability)(provider)) || ((0, files_1.hasReadWriteCapability)(provider) && (options === null || options === void 0 ? void 0 : options.preferUnbuffered))) { fileStreamPromise = this.readFileUnbuffered(provider, resource, options); } // read streamed (always prefer over primitive buffered read) else if ((0, files_1.hasFileReadStreamCapability)(provider)) { fileStreamPromise = Promise.resolve(this.readFileStreamed(provider, resource, cancellableSource.token, options)); } // read buffered else { fileStreamPromise = Promise.resolve(this.readFileBuffered(provider, resource, cancellableSource.token, options)); } const [fileStat, fileStream] = await Promise.all([statPromise, fileStreamPromise]); return { ...fileStat, value: fileStream }; } catch (error) { this.rethrowAsFileOperationError("Unable to read file '{0}' ({1})", resource, error, options); } } readFileStreamed(provider, resource, token, options = Object.create(null)) { const fileStream = provider.readFileStream(resource, options, token); return (0, stream_1.transform)(fileStream, { data: data => data instanceof buffer_1.BinaryBuffer ? data : buffer_1.BinaryBuffer.wrap(data), error: error => this.asFileOperationError("Unable to read file '{0}' ({1})", resource, error, options) }, data => buffer_1.BinaryBuffer.concat(data)); } readFileBuffered(provider, resource, token, options = Object.create(null)) { const stream = buffer_1.BinaryBufferWriteableStream.create(); (0, io_1.readFileIntoStream)(provider, resource, stream, data => data, { ...options, bufferSize: this.BUFFER_SIZE, errorTransformer: error => this.asFileOperationError("Unable to read file '{0}' ({1})", resource, error, options) }, token); return stream; } rethrowAsFileOperationError(message, resource, error, options) { throw this.asFileOperationError(message, resource, error, options); } asFileOperationError(message, resource, error, options) { const fileOperationError = new files_1.FileOperationError(core_1.nls.localizeByDefault(message, this.resourceForError(resource), (0, files_1.ensureFileSystemProviderError)(error).toString()), (0, files_1.toFileOperationResult)(error), options); fileOperationError.stack = `${fileOperationError.stack}\nCaused by: ${error.stack}`; return fileOperationError; } async readFileUnbuffered(provider, resource, options) { let buffer = await provider.readFile(resource); // respect position option if (options && typeof options.position === 'number') { buffer = buffer.slice(options.position); } // respect length option if (options && typeof options.length === 'number') { buffer = buffer.slice(0, options.length); } // Throw if file is too large to load this.validateReadFileLimits(resource, buffer.byteLength, options); return buffer_1.BinaryBufferReadableStream.fromBuffer(buffer_1.BinaryBuffer.wrap(buffer)); } async validateReadFile(resource, options) { const stat = await this.resolve(resource, { resolveMetadata: true }); // Throw if resource is a directory if (stat.isDirectory) { throw new files_1.FileOperationError(core_1.nls.localizeByDefault("Unable to read file '{0}' that is actually a directory", this.resourceForError(resource)), 0 /* FileOperationResult.FILE_IS_DIRECTORY */, options); } // Throw if file not modified since (unless disabled) if (options && typeof options.etag === 'string' && options.etag !== files_1.ETAG_DISABLED && options.etag === stat.etag) { throw new files_1.FileOperationError(core_1.nls.localizeByDefault('File not modified since'), 2 /* FileOperationResult.FILE_NOT_MODIFIED_SINCE */, options); } // Throw if file is too large to load this.validateReadFileLimits(resource, stat.size, options); return stat; } validateReadFileLimits(resource, size, options) { if (options === null || options === void 0 ? void 0 : options.limits) { let tooLargeErrorResult = undefined; if (typeof options.limits.memory === 'number' && size > options.limits.memory) { tooLargeErrorResult = 9 /* FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT */; } if (typeof options.limits.size === 'number' && size > options.limits.size) { tooLargeErrorResult = 7 /* FileOperationResult.FILE_TOO_LARGE */; } if (typeof tooLargeErrorResult === 'number') { throw new files_1.FileOperationError(core_1.nls.localizeByDefault("Unable to read file '{0}' that is too large to open", this.resourceForError(resource)), tooLargeErrorResult); } } } // #endregion // #region Move/Copy/Delete/Create Folder async move(source, target, options) { if ((options === null || options === void 0 ? void 0 : options.fromUserGesture) === false) { return this.doMove(source, target, options.overwrite); } await this.runFileOperationParticipants(target, source, 2 /* FileOperation.MOVE */); const event = { correlationId: this.correlationIds++, operation: 2 /* FileOperation.MOVE */, target, source }; await this.onWillRunUserOperationEmitter.fire(event); let stat; try { stat = await this.doMove(source, target, options === null || options === void 0 ? void 0 : options.overwrite); } catch (error) { await this.onDidFailUserOperationEmitter.fire(event); throw error; } await this.onDidRunUserOperationEmitter.fire(event); return stat; } async doMove(source, target, overwrite) { const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source); const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); // move const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', !!overwrite); // resolve and send events const fileStat = await this.resolve(target, { resolveMetadata: true }); this.onDidRunOperationEmitter.fire(new files_1.FileOperationEvent(source, mode === 'move' ? 2 /* FileOperation.MOVE */ : 3 /* FileOperation.COPY */, fileStat)); return fileStat; } async copy(source, target, options) { if ((options === null || options === void 0 ? void 0 : options.fromUserGesture) === false) { return this.doCopy(source, target, options.overwrite); } await this.runFileOperationParticipants(target, source, 3 /* FileOperation.COPY */); const event = { correlationId: this.correlationIds++, operation: 3 /* FileOperation.COPY */, target, source }; await this.onWillRunUserOperationEmitter.fire(event); let stat; try { stat = await this.doCopy(source, target, options === null || options === void 0 ? void 0 : options.overwrite); } catch (error) { await this.onDidFailUserOperationEmitter.fire(event); throw error; } await this.onDidRunUserOperationEmitter.fire(event); return stat; } async doCopy(source, target, overwrite) { const sourceProvider = await this.withReadProvider(source); const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); // copy const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', !!overwrite); // resolve and send events const fileStat = await this.resolve(target, { resolveMetadata: true }); this.onDidRunOperationEmitter.fire(new files_1.FileOperationEvent(source, mode === 'copy' ? 3 /* FileOperation.COPY */ : 2 /* FileOperation.MOVE */, fileStat)); return fileStat; } async doMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite) { if (source.toString() === target.toString()) { return mode; // simulate node.js behaviour here and do a no-op if paths match } // validation const { exists, isSameResourceWithDifferentPathCase } = await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite); // if target exists get valid target if (exists && !overwrite) { const parent = await this.resolve(target.parent); const targetFileStat = await this.resolve(target); target = filesystem_utils_1.FileSystemUtils.generateUniqueResourceURI(parent, target, targetFileStat.isDirectory, isSameResourceWithDifferentPathCase ? 'copy' : undefined); } // delete as needed (unless target is same resource with different path case) if (exists && !isSameResourceWithDifferentPathCase && overwrite) { await this.delete(target, { recursive: true }); } // create parent folders await this.mkdirp(targetProvider, target.parent); // copy source => target if (mode === 'copy') { // same provider with fast copy: leverage copy() functionality if (sourceProvider === targetProvider && (0, files_1.hasFileFolderCopyCapability)(sourceProvider)) { await sourceProvider.copy(source, target, { overwrite }); } // when copying via buffer/unbuffered, we have to manually // traverse the source if it is a folder and not a file else { const sourceFile = await this.resolve(source); if (sourceFile.isDirectory) { await this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target); } else { await this.doCopyFile(sourceProvider, source, targetProvider, target); } } return mode; } // move source => target else { // same provider: leverage rename() functionality if (sourceProvider === targetProvider) { await sourceProvider.rename(source, target, { overwrite }); return mode; } // across providers: copy to target & delete at source else { await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite); await this.delete(source, { recursive: true }); return 'copy'; } } } async doCopyFile(sourceProvider, source, targetProvider, target) { // copy: source (buffered) => target (buffered) if ((0, files_1.hasOpenReadWriteCloseCapability)(sourceProvider) && (0, files_1.hasOpenReadWriteCloseCapability)(targetProvider)) { return this.doPipeBuffered(sourceProvider, source, targetProvider, target); } // copy: source (buffered) => target (unbuffered) if ((0, files_1.hasOpenReadWriteCloseCapability)(sourceProvider) && (0, files_1.hasReadWriteCapability)(targetProvider)) { return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target); } // copy: source (unbuffered) => target (buffered) if ((0, files_1.hasReadWriteCapability)(sourceProvider) && (0, files_1.hasOpenReadWriteCloseCapability)(targetProvider)) { return this.doPipeUnbufferedToBuffered(sourceProvider, source, targetProvider, target); } // copy: source (unbuffered) => target (unbuffered) if ((0, files_1.hasReadWriteCapability)(sourceProvider) && (0, files_1.hasReadWriteCapability)(targetProvider)) { return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target); } } async doCopyFolder(sourceProvider, sourceFolder, targetProvider, targetFolder) { // create folder in target await targetProvider.mkdir(targetFolder); // create children in target if (Array.isArray(sourceFolder.children)) { await Promise.all(sourceFolder.children.map(async (sourceChild) => { const targetChild = targetFolder.resolve(sourceChild.name); if (sourceChild.isDirectory) { return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild); } else { return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild); } })); } } async doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite) { let isSameResourceWithDifferentPathCase = false; // Check if source is equal or parent to target (requires providers to be the same) if (sourceProvider === targetProvider) { const isPathCaseSensitive = !!(sourceProvider.capabilities & 1024 /* FileSystemProviderCapabilities.PathCaseSensitive */); if (!isPathCaseSensitive) { isSameResourceWithDifferentPathCase = source.toString().toLowerCase() === target.toString().toLowerCase(); } if (isSameResourceWithDifferentPathCase && mode === 'copy') { throw new Error(core_1.nls.localizeByDefault("Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target))); } if (!isSameResourceWithDifferentPathCase && target.isEqualOrParent(source, isPathCaseSensitive)) { throw new Error(core_1.nls.localizeByDefault("Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target))); } } // Extra checks if target exists and this is not a rename const exists = await this.exists(target); if (exists && !isSameResourceWithDifferentPathCase) { // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw if (sourceProvider === targetProvider) { const isPathCaseSensitive = !!(sourceProvider.capabilities & 1024 /* FileSystemProviderCapabilities.PathCaseSensitive */); if (source.isEqualOrParent(target, isPathCaseSensitive)) { throw new Error(core_1.nls.localizeByDefault("Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target))); } } } return { exists, isSameResourceWithDifferentPathCase }; } async createFolder(resource, options = {}) { const { fromUserGesture = true, } = options; const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); // mkdir recursively await this.mkdirp(provider, resource); // events const fileStat = await this.resolve(resource, { resolveMetadata: true }); if (fromUserGesture) { this.onDidRunUserOperationEmitter.fire({ correlationId: this.correlationIds++, operation: 0 /* FileOperation.CREATE */, target: resource }); } else { this.onDidRunOperationEmitter.fire(new files_1.FileOperationEvent(resource, 0 /* FileOperation.CREATE */, fileStat)); } return fileStat; } async mkdirp(provider, directory) { const directoriesToCreate = []; // mkdir until we reach root while (!directory.path.isRoot) { try { const stat = await provider.stat(directory); if ((stat.type & files_1.FileType.Directory) === 0) { throw new Error(core_1.nls.localizeByDefault("Unable to create folder '{0}' that already exists but is not a directory", this.resourceForError(directory))); } break; // we have hit a directory