Path: blob/main/src/vs/platform/files/common/fileService.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { coalesce } from '../../../base/common/arrays.js';6import { Promises, ResourceQueue } from '../../../base/common/async.js';7import { bufferedStreamToBuffer, bufferToReadable, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer, VSBufferReadable, VSBufferReadableBufferedStream, VSBufferReadableStream } from '../../../base/common/buffer.js';8import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';9import { Emitter } from '../../../base/common/event.js';10import { hash } from '../../../base/common/hash.js';11import { Iterable } from '../../../base/common/iterator.js';12import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';13import { TernarySearchTree } from '../../../base/common/ternarySearchTree.js';14import { Schemas } from '../../../base/common/network.js';15import { mark } from '../../../base/common/performance.js';16import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from '../../../base/common/resources.js';17import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from '../../../base/common/stream.js';18import { URI } from '../../../base/common/uri.js';19import { localize } from '../../../nls.js';20import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAppendCapability, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability, IWatchOptionsWithCorrelation, IFileSystemWatcher, IWatchOptionsWithoutCorrelation, hasFileRealpathCapability } from './files.js';21import { readFileIntoStream } from './io.js';22import { ILogService } from '../../log/common/log.js';23import { ErrorNoTelemetry } from '../../../base/common/errors.js';2425export class FileService extends Disposable implements IFileService {2627declare readonly _serviceBrand: undefined;2829// Choose a buffer size that is a balance between memory needs and30// manageable IPC overhead. The larger the buffer size, the less31// roundtrips we have to do for reading/writing data.32private readonly BUFFER_SIZE = 256 * 1024;3334constructor(@ILogService private readonly logService: ILogService) {35super();36}3738//#region File System Provider3940private readonly _onDidChangeFileSystemProviderRegistrations = this._register(new Emitter<IFileSystemProviderRegistrationEvent>());41readonly onDidChangeFileSystemProviderRegistrations = this._onDidChangeFileSystemProviderRegistrations.event;4243private readonly _onWillActivateFileSystemProvider = this._register(new Emitter<IFileSystemProviderActivationEvent>());44readonly onWillActivateFileSystemProvider = this._onWillActivateFileSystemProvider.event;4546private readonly _onDidChangeFileSystemProviderCapabilities = this._register(new Emitter<IFileSystemProviderCapabilitiesChangeEvent>());47readonly onDidChangeFileSystemProviderCapabilities = this._onDidChangeFileSystemProviderCapabilities.event;4849private readonly provider = new Map<string, IFileSystemProvider>();5051registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {52if (this.provider.has(scheme)) {53throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`);54}5556mark(`code/registerFilesystem/${scheme}`);5758const providerDisposables = new DisposableStore();5960// Add provider with event61this.provider.set(scheme, provider);62this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });6364// Forward events from provider65providerDisposables.add(provider.onDidChangeFile(changes => {66const event = new FileChangesEvent(changes, !this.isPathCaseSensitive(provider));6768// Always emit any event internally69this.internalOnDidFilesChange.fire(event);7071// Only emit uncorrelated events in the global `onDidFilesChange` event72if (!event.hasCorrelation()) {73this._onDidUncorrelatedFilesChange.fire(event);74}75}));76if (typeof provider.onDidWatchError === 'function') {77providerDisposables.add(provider.onDidWatchError(error => this._onDidWatchError.fire(new Error(error))));78}79providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme })));8081return toDisposable(() => {82this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider });83this.provider.delete(scheme);8485dispose(providerDisposables);86});87}8889getProvider(scheme: string): IFileSystemProvider | undefined {90return this.provider.get(scheme);91}9293async activateProvider(scheme: string): Promise<void> {9495// Emit an event that we are about to activate a provider with the given scheme.96// Listeners can participate in the activation by registering a provider for it.97const joiners: Promise<void>[] = [];98this._onWillActivateFileSystemProvider.fire({99scheme,100join(promise) {101joiners.push(promise);102},103});104105if (this.provider.has(scheme)) {106return; // provider is already here so we can return directly107}108109// If the provider is not yet there, make sure to join on the listeners assuming110// that it takes a bit longer to register the file system provider.111await Promises.settled(joiners);112}113114async canHandleResource(resource: URI): Promise<boolean> {115116// Await activation of potentially extension contributed providers117await this.activateProvider(resource.scheme);118119return this.hasProvider(resource);120}121122hasProvider(resource: URI): boolean {123return this.provider.has(resource.scheme);124}125126hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean {127const provider = this.provider.get(resource.scheme);128129return !!(provider && (provider.capabilities & capability));130}131132listCapabilities(): Iterable<{ scheme: string; capabilities: FileSystemProviderCapabilities }> {133return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities }));134}135136protected async withProvider(resource: URI): Promise<IFileSystemProvider> {137138// Assert path is absolute139if (!isAbsolutePath(resource)) {140throw new FileOperationError(localize('invalidPath', "Unable to resolve filesystem provider with relative file path '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_INVALID_PATH);141}142143// Activate provider144await this.activateProvider(resource.scheme);145146// Assert provider147const provider = this.provider.get(resource.scheme);148if (!provider) {149const error = new ErrorNoTelemetry();150error.message = localize('noProviderFound', "ENOPRO: No file system provider found for resource '{0}'", resource.toString());151152throw error;153}154155return provider;156}157158private async withReadProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability> {159const provider = await this.withProvider(resource);160161if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider) || hasFileReadStreamCapability(provider)) {162return provider;163}164165throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`);166}167168private async withWriteProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability> {169const provider = await this.withProvider(resource);170171if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider)) {172return provider;173}174175throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`);176}177178//#endregion179180//#region Operation events181182private readonly _onDidRunOperation = this._register(new Emitter<FileOperationEvent>());183readonly onDidRunOperation = this._onDidRunOperation.event;184185//#endregion186187//#region File Metadata Resolving188189async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;190async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;191async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {192try {193return await this.doResolveFile(resource, options);194} catch (error) {195196// Specially handle file not found case as file operation result197if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) {198throw new FileOperationError(localize('fileNotFoundError', "Unable to resolve nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND);199}200201// Bubble up any other error as is202throw ensureFileSystemProviderError(error);203}204}205206private async doResolveFile(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;207private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;208private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {209const provider = await this.withProvider(resource);210const isPathCaseSensitive = this.isPathCaseSensitive(provider);211212const resolveTo = options?.resolveTo;213const resolveSingleChildDescendants = options?.resolveSingleChildDescendants;214const resolveMetadata = options?.resolveMetadata;215216const stat = await provider.stat(resource);217218let trie: TernarySearchTree<URI, boolean> | undefined;219220return this.toFileStat(provider, resource, stat, undefined, !!resolveMetadata, (stat, siblings) => {221222// lazy trie to check for recursive resolving223if (!trie) {224trie = TernarySearchTree.forUris<true>(() => !isPathCaseSensitive);225trie.set(resource, true);226if (resolveTo) {227trie.fill(true, resolveTo);228}229}230231// check for recursive resolving232if (trie.get(stat.resource) || trie.findSuperstr(stat.resource.with({ query: null, fragment: null } /* required for https://github.com/microsoft/vscode/issues/128151 */))) {233return true;234}235236// check for resolving single child folders237if (stat.isDirectory && resolveSingleChildDescendants) {238return siblings === 1;239}240241return false;242});243}244245private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat>;246private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStatWithMetadata>;247private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat> {248const { providerExtUri } = this.getExtUri(provider);249250// convert to file stat251const fileStat: IFileStat = {252resource,253name: providerExtUri.basename(resource),254isFile: (stat.type & FileType.File) !== 0,255isDirectory: (stat.type & FileType.Directory) !== 0,256isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,257mtime: stat.mtime,258ctime: stat.ctime,259size: stat.size,260readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly),261locked: Boolean((stat.permissions ?? 0) & FilePermission.Locked),262executable: Boolean((stat.permissions ?? 0) & FilePermission.Executable),263etag: etag({ mtime: stat.mtime, size: stat.size }),264children: undefined265};266267// check to recurse for directories268if (fileStat.isDirectory && recurse(fileStat, siblings)) {269try {270const entries = await provider.readdir(resource);271const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {272try {273const childResource = providerExtUri.joinPath(resource, name);274const childStat = resolveMetadata ? await provider.stat(childResource) : { type };275276return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse);277} catch (error) {278this.logService.trace(error);279280return null; // can happen e.g. due to permission errors281}282}));283284// make sure to get rid of null values that signal a failure to resolve a particular entry285fileStat.children = coalesce(resolvedEntries);286} catch (error) {287this.logService.trace(error);288289fileStat.children = []; // gracefully handle errors, we may not have permissions to read290}291292return fileStat;293}294295return fileStat;296}297298async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]>;299async resolveAll(toResolve: { resource: URI; options: IResolveMetadataFileOptions }[]): Promise<IFileStatResultWithMetadata[]>;300async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]> {301return Promises.settled(toResolve.map(async entry => {302try {303return { stat: await this.doResolveFile(entry.resource, entry.options), success: true };304} catch (error) {305this.logService.trace(error);306307return { stat: undefined, success: false };308}309}));310}311312async stat(resource: URI): Promise<IFileStatWithPartialMetadata> {313const provider = await this.withProvider(resource);314315const stat = await provider.stat(resource);316317return this.toFileStat(provider, resource, stat, undefined, true, () => false /* Do not resolve any children */);318}319320async realpath(resource: URI): Promise<URI | undefined> {321const provider = await this.withProvider(resource);322323if (hasFileRealpathCapability(provider)) {324const realpath = await provider.realpath(resource);325326return resource.with({ path: realpath });327}328329return undefined;330}331332async exists(resource: URI): Promise<boolean> {333const provider = await this.withProvider(resource);334335try {336const stat = await provider.stat(resource);337338return !!stat;339} catch (error) {340return false;341}342}343344//#endregion345346//#region File Reading/Writing347348async canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true> {349try {350await this.doValidateCreateFile(resource, options);351} catch (error) {352return error;353}354355return true;356}357358private async doValidateCreateFile(resource: URI, options?: ICreateFileOptions): Promise<void> {359360// validate overwrite361if (!options?.overwrite && await this.exists(resource)) {362throw new FileOperationError(localize('fileExists', "Unable to create file '{0}' that already exists when overwrite flag is not set", this.resourceForError(resource)), FileOperationResult.FILE_MODIFIED_SINCE, options);363}364}365366async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {367368// validate369await this.doValidateCreateFile(resource, options);370371// do write into file (this will create it too)372const fileStat = await this.writeFile(resource, bufferOrReadableOrStream);373374// events375this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));376377return fileStat;378}379380async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {381const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource);382const { providerExtUri } = this.getExtUri(provider);383384let writeFileOptions = options;385if (hasFileAtomicWriteCapability(provider) && !writeFileOptions?.atomic) {386const enforcedAtomicWrite = provider.enforceAtomicWriteFile?.(resource);387if (enforcedAtomicWrite) {388writeFileOptions = { ...options, atomic: enforcedAtomicWrite };389}390}391392try {393394// validate write (this may already return a peeked-at buffer)395let { stat, buffer: bufferOrReadableOrStreamOrBufferedStream } = await this.validateWriteFile(provider, resource, bufferOrReadableOrStream, writeFileOptions);396397// mkdir recursively as needed398if (!stat) {399await this.mkdirp(provider, providerExtUri.dirname(resource));400}401402// optimization: if the provider has unbuffered write capability and the data403// to write is not a buffer, we consume up to 3 chunks and try to write the data404// unbuffered to reduce the overhead. If the stream or readable has more data405// to provide we continue to write buffered.406if (!bufferOrReadableOrStreamOrBufferedStream) {407bufferOrReadableOrStreamOrBufferedStream = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);408}409410// write file: unbuffered411if (412!hasOpenReadWriteCloseCapability(provider) || // buffered writing is unsupported413(hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) || // data is a full buffer already414(hasReadWriteCapability(provider) && hasFileAtomicWriteCapability(provider) && writeFileOptions?.atomic) // atomic write forces unbuffered write if the provider supports it415) {416await this.doWriteUnbuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream);417}418419// write file: buffered420else {421await this.doWriteBuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);422}423424// events425this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.WRITE));426} catch (error) {427throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), writeFileOptions);428}429430return this.resolve(resource, { resolveMetadata: true });431}432433434private async peekBufferForWriting(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream> {435let peekResult: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream;436if (hasReadWriteCapability(provider) && !(bufferOrReadableOrStream instanceof VSBuffer)) {437if (isReadableStream(bufferOrReadableOrStream)) {438const bufferedStream = await peekStream(bufferOrReadableOrStream, 3);439if (bufferedStream.ended) {440peekResult = VSBuffer.concat(bufferedStream.buffer);441} else {442peekResult = bufferedStream;443}444} else {445peekResult = peekReadable(bufferOrReadableOrStream, data => VSBuffer.concat(data), 3);446}447} else {448peekResult = bufferOrReadableOrStream;449}450451return peekResult;452}453454private async validateWriteFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<{ stat: IStat | undefined; buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined }> {455456// Validate unlock support457const unlock = !!options?.unlock;458if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {459throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));460}461462// Validate append support463if (options?.append && !hasFileAppendCapability(provider)) {464throw new FileOperationError(localize('err.noAppend', "Filesystem provider for scheme '{0}' does not does not support append", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);465}466467// Validate atomic support468const atomic = !!options?.atomic;469if (atomic) {470if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) {471throw new Error(localize('writeFailedAtomicUnsupported1', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource)));472}473474if (!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite)) {475throw new Error(localize('writeFailedAtomicUnsupported2', "Unable to atomically write file '{0}' because provider does not support unbuffered writes.", this.resourceForError(resource)));476}477478if (unlock) {479throw new Error(localize('writeFailedAtomicUnlock', "Unable to unlock file '{0}' because atomic write is enabled.", this.resourceForError(resource)));480}481}482483// Validate via file stat meta data484let stat: IStat | undefined = undefined;485try {486stat = await provider.stat(resource);487} catch (error) {488return Object.create(null); // file might not exist489}490491// File cannot be directory492if ((stat.type & FileType.Directory) !== 0) {493throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);494}495496// File cannot be readonly497this.throwIfFileIsReadonly(resource, stat);498499// Dirty write prevention: if the file on disk has been changed and does not match our expected500// mtime and etag, we bail out to prevent dirty writing.501//502// First, we check for a mtime that is in the future before we do more checks. The assumption is503// that only the mtime is an indicator for a file that has changed on disk.504//505// Second, if the mtime has advanced, we compare the size of the file on disk with our previous506// one using the etag() function. Relying only on the mtime check has prooven to produce false507// positives due to file system weirdness (especially around remote file systems). As such, the508// check for size is a weaker check because it can return a false negative if the file has changed509// but to the same length. This is a compromise we take to avoid having to produce checksums of510// the file content for comparison which would be much slower to compute.511//512// Third, if the etag() turns out to be different, we do one attempt to compare the buffer we513// are about to write with the contents on disk to figure out if the contents are identical.514// In that case we allow the writing as it would result in the same contents in the file.515let buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined;516if (517typeof options?.mtime === 'number' && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED &&518typeof stat.mtime === 'number' && typeof stat.size === 'number' &&519options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size })520) {521buffer = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);522if (buffer instanceof VSBuffer && buffer.byteLength === stat.size) {523try {524const { value } = await this.readFile(resource, { limits: { size: stat.size } });525if (buffer.equals(value)) {526return { stat, buffer }; // allow writing since contents are identical527}528} catch (error) {529// ignore, throw the FILE_MODIFIED_SINCE error530}531}532533throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options);534}535536return { stat, buffer };537}538539async readFile(resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {540const provider = await this.withReadProvider(resource);541542if (options?.atomic) {543return this.doReadFileAtomic(provider, resource, options, token);544}545546return this.doReadFile(provider, resource, options, token);547}548549private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {550return new Promise<IFileContent>((resolve, reject) => {551this.writeQueue.queueFor(resource, async () => {552try {553const content = await this.doReadFile(provider, resource, options, token);554resolve(content);555} catch (error) {556reject(error);557}558}, this.getExtUri(provider).providerExtUri);559});560}561562private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {563const stream = await this.doReadFileStream(provider, resource, {564...options,565// optimization: since we know that the caller does not566// care about buffering, we indicate this to the reader.567// this reduces all the overhead the buffered reading568// has (open, read, close) if the provider supports569// unbuffered reading.570preferUnbuffered: true571}, token);572573return {574...stream,575value: await streamToBuffer(stream.value)576};577}578579async readFileStream(resource: URI, options?: IReadFileStreamOptions, token?: CancellationToken): Promise<IFileStreamContent> {580const provider = await this.withReadProvider(resource);581582return this.doReadFileStream(provider, resource, options, token);583}584585private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions & { preferUnbuffered?: boolean }, token?: CancellationToken): Promise<IFileStreamContent> {586587// install a cancellation token that gets cancelled588// when any error occurs. this allows us to resolve589// the content of the file while resolving metadata590// but still cancel the operation in certain cases.591//592// in addition, we pass the optional token in that593// we got from the outside to even allow for external594// cancellation of the read operation.595const cancellableSource = new CancellationTokenSource(token);596597let readFileOptions = options;598if (hasFileAtomicReadCapability(provider) && provider.enforceAtomicReadFile?.(resource)) {599readFileOptions = { ...options, atomic: true };600}601602// validate read operation603const statPromise = this.validateReadFile(resource, readFileOptions).then(stat => stat, error => {604cancellableSource.dispose(true);605606throw error;607});608609let fileStream: VSBufferReadableStream | undefined = undefined;610try {611612// if the etag is provided, we await the result of the validation613// due to the likelihood of hitting a NOT_MODIFIED_SINCE result.614// otherwise, we let it run in parallel to the file reading for615// optimal startup performance.616if (typeof readFileOptions?.etag === 'string' && readFileOptions.etag !== ETAG_DISABLED) {617await statPromise;618}619620// read unbuffered621if (622(readFileOptions?.atomic && hasFileAtomicReadCapability(provider)) || // atomic reads are always unbuffered623!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || // provider has no buffered capability624(hasReadWriteCapability(provider) && readFileOptions?.preferUnbuffered) // unbuffered read is preferred625) {626fileStream = this.readFileUnbuffered(provider, resource, readFileOptions);627}628629// read streamed (always prefer over primitive buffered read)630else if (hasFileReadStreamCapability(provider)) {631fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, readFileOptions);632}633634// read buffered635else {636fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, readFileOptions);637}638639fileStream.on('end', () => cancellableSource.dispose());640fileStream.on('error', () => cancellableSource.dispose());641642const fileStat = await statPromise;643644return {645...fileStat,646value: fileStream647};648} catch (error) {649650// Await the stream to finish so that we exit this method651// in a consistent state with file handles closed652// (https://github.com/microsoft/vscode/issues/114024)653if (fileStream) {654await consumeStream(fileStream);655}656657// Re-throw errors as file operation errors but preserve658// specific errors (such as not modified since)659throw this.restoreReadError(error, resource, readFileOptions);660}661}662663private restoreReadError(error: Error, resource: URI, options?: IReadFileStreamOptions): FileOperationError {664const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString());665666if (error instanceof NotModifiedSinceFileOperationError) {667return new NotModifiedSinceFileOperationError(message, error.stat, options);668}669670if (error instanceof TooLargeFileOperationError) {671return new TooLargeFileOperationError(message, error.fileOperationResult, error.size, error.options as IReadFileOptions);672}673674return new FileOperationError(message, toFileOperationResult(error), options);675}676677private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {678const fileStream = provider.readFileStream(resource, options, token);679680return transform(fileStream, {681data: data => data instanceof VSBuffer ? data : VSBuffer.wrap(data),682error: error => this.restoreReadError(error, resource, options)683}, data => VSBuffer.concat(data));684}685686private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {687const stream = newWriteableBufferStream();688689readFileIntoStream(provider, resource, stream, data => data, {690...options,691bufferSize: this.BUFFER_SIZE,692errorTransformer: error => this.restoreReadError(error, resource, options)693}, token);694695return stream;696}697698private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithFileAtomicReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions): VSBufferReadableStream {699const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));700701// Read the file into the stream async but do not wait for702// this to complete because streams work via events703(async () => {704try {705let buffer: Uint8Array;706if (options?.atomic && hasFileAtomicReadCapability(provider)) {707buffer = await provider.readFile(resource, { atomic: true });708} else {709buffer = await provider.readFile(resource);710}711712// respect position option713if (typeof options?.position === 'number') {714buffer = buffer.slice(options.position);715}716717// respect length option718if (typeof options?.length === 'number') {719buffer = buffer.slice(0, options.length);720}721722// Throw if file is too large to load723this.validateReadFileLimits(resource, buffer.byteLength, options);724725// End stream with data726stream.end(VSBuffer.wrap(buffer));727} catch (err) {728stream.error(err);729stream.end();730}731})();732733return stream;734}735736private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {737const stat = await this.resolve(resource, { resolveMetadata: true });738739// Throw if resource is a directory740if (stat.isDirectory) {741throw new FileOperationError(localize('fileIsDirectoryReadError', "Unable to read file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);742}743744// Throw if file not modified since (unless disabled)745if (typeof options?.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) {746throw new NotModifiedSinceFileOperationError(localize('fileNotModifiedError', "File not modified since"), stat, options);747}748749// Throw if file is too large to load750this.validateReadFileLimits(resource, stat.size, options);751752return stat;753}754755private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {756if (typeof options?.limits?.size === 'number' && size > options.limits.size) {757throw new TooLargeFileOperationError(localize('fileTooLargeError', "Unable to read file '{0}' that is too large to open", this.resourceForError(resource)), FileOperationResult.FILE_TOO_LARGE, size, options);758}759}760761//#endregion762763//#region Move/Copy/Delete/Create Folder764765async canMove(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {766return this.doCanMoveCopy(source, target, 'move', overwrite);767}768769async canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {770return this.doCanMoveCopy(source, target, 'copy', overwrite);771}772773private async doCanMoveCopy(source: URI, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<Error | true> {774if (source.toString() !== target.toString()) {775try {776const sourceProvider = mode === 'move' ? this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source) : await this.withReadProvider(source);777const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);778779await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite);780} catch (error) {781return error;782}783}784785return true;786}787788async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {789const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source);790const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);791792// move793const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', !!overwrite);794795// resolve and send events796const fileStat = await this.resolve(target, { resolveMetadata: true });797this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat));798799return fileStat;800}801802async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {803const sourceProvider = await this.withReadProvider(source);804const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);805806// copy807const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', !!overwrite);808809// resolve and send events810const fileStat = await this.resolve(target, { resolveMetadata: true });811this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat));812813return fileStat;814}815816private async doMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite: boolean): Promise<'move' | 'copy'> {817if (source.toString() === target.toString()) {818return mode; // simulate node.js behaviour here and do a no-op if paths match819}820821// validation822const { exists, isSameResourceWithDifferentPathCase } = await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite);823824// delete as needed (unless target is same resurce with different path case)825if (exists && !isSameResourceWithDifferentPathCase && overwrite) {826await this.del(target, { recursive: true });827}828829// create parent folders830await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));831832// copy source => target833if (mode === 'copy') {834835// same provider with fast copy: leverage copy() functionality836if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {837await sourceProvider.copy(source, target, { overwrite });838}839840// when copying via buffer/unbuffered, we have to manually841// traverse the source if it is a folder and not a file842else {843const sourceFile = await this.resolve(source);844if (sourceFile.isDirectory) {845await this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target);846} else {847await this.doCopyFile(sourceProvider, source, targetProvider, target);848}849}850851return mode;852}853854// move source => target855else {856857// same provider: leverage rename() functionality858if (sourceProvider === targetProvider) {859await sourceProvider.rename(source, target, { overwrite });860861return mode;862}863864// across providers: copy to target & delete at source865else {866await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite);867await this.del(source, { recursive: true });868869return 'copy';870}871}872}873874private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI): Promise<void> {875876// copy: source (buffered) => target (buffered)877if (hasOpenReadWriteCloseCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) {878return this.doPipeBuffered(sourceProvider, source, targetProvider, target);879}880881// copy: source (buffered) => target (unbuffered)882if (hasOpenReadWriteCloseCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) {883return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target);884}885886// copy: source (unbuffered) => target (buffered)887if (hasReadWriteCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) {888return this.doPipeUnbufferedToBuffered(sourceProvider, source, targetProvider, target);889}890891// copy: source (unbuffered) => target (unbuffered)892if (hasReadWriteCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) {893return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target);894}895}896897private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI): Promise<void> {898899// create folder in target900await targetProvider.mkdir(targetFolder);901902// create children in target903if (Array.isArray(sourceFolder.children)) {904await Promises.settled(sourceFolder.children.map(async sourceChild => {905const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name);906if (sourceChild.isDirectory) {907return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild);908} else {909return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild);910}911}));912}913}914915private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean; isSameResourceWithDifferentPathCase: boolean }> {916let isSameResourceWithDifferentPathCase = false;917918// Check if source is equal or parent to target (requires providers to be the same)919if (sourceProvider === targetProvider) {920const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider);921if (!isPathCaseSensitive) {922isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target);923}924925if (isSameResourceWithDifferentPathCase && mode === 'copy') {926throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target)));927}928929if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) {930throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));931}932}933934// Extra checks if target exists and this is not a rename935const exists = await this.exists(target);936if (exists && !isSameResourceWithDifferentPathCase) {937938// Bail out if target exists and we are not about to overwrite939if (!overwrite) {940throw new FileOperationError(localize('unableToMoveCopyError3', "Unable to move/copy '{0}' because target '{1}' already exists at destination.", this.resourceForError(source), this.resourceForError(target)), FileOperationResult.FILE_MOVE_CONFLICT);941}942943// Special case: if the target is a parent of the source, we cannot delete944// it as it would delete the source as well. In this case we have to throw945if (sourceProvider === targetProvider) {946const { providerExtUri } = this.getExtUri(sourceProvider);947if (providerExtUri.isEqualOrParent(source, target)) {948throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target)));949}950}951}952953return { exists, isSameResourceWithDifferentPathCase };954}955956private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri; isPathCaseSensitive: boolean } {957const isPathCaseSensitive = this.isPathCaseSensitive(provider);958959return {960providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,961isPathCaseSensitive962};963}964965private isPathCaseSensitive(provider: IFileSystemProvider): boolean {966return !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);967}968969async createFolder(resource: URI): Promise<IFileStatWithMetadata> {970const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);971972// mkdir recursively973await this.mkdirp(provider, resource);974975// events976const fileStat = await this.resolve(resource, { resolveMetadata: true });977this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));978979return fileStat;980}981982private async mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {983const directoriesToCreate: string[] = [];984985// mkdir until we reach root986const { providerExtUri } = this.getExtUri(provider);987while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {988try {989const stat = await provider.stat(directory);990if ((stat.type & FileType.Directory) === 0) {991throw new Error(localize('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", this.resourceForError(directory)));992}993994break; // we have hit a directory that exists -> good995} catch (error) {996997// Bubble up any other error that is not file not found998if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) {999throw error;1000}10011002// Upon error, remember directories that need to be created1003directoriesToCreate.push(providerExtUri.basename(directory));10041005// Continue up1006directory = providerExtUri.dirname(directory);1007}1008}10091010// Create directories as needed1011for (let i = directoriesToCreate.length - 1; i >= 0; i--) {1012directory = providerExtUri.joinPath(directory, directoriesToCreate[i]);10131014try {1015await provider.mkdir(directory);1016} catch (error) {1017if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileExists) {1018// For mkdirp() we tolerate that the mkdir() call fails1019// in case the folder already exists. This follows node.js1020// own implementation of fs.mkdir({ recursive: true }) and1021// reduces the chances of race conditions leading to errors1022// if multiple calls try to create the same folders1023// As such, we only throw an error here if it is other than1024// the fact that the file already exists.1025// (see also https://github.com/microsoft/vscode/issues/89834)1026throw error;1027}1028}1029}1030}10311032async canDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<Error | true> {1033try {1034await this.doValidateDelete(resource, options);1035} catch (error) {1036return error;1037}10381039return true;1040}10411042private async doValidateDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<IFileSystemProvider> {1043const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);10441045// Validate trash support1046const useTrash = !!options?.useTrash;1047if (useTrash && !(provider.capabilities & FileSystemProviderCapabilities.Trash)) {1048throw new Error(localize('deleteFailedTrashUnsupported', "Unable to delete file '{0}' via trash because provider does not support it.", this.resourceForError(resource)));1049}10501051// Validate atomic support1052const atomic = options?.atomic;1053if (atomic && !(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete)) {1054throw new Error(localize('deleteFailedAtomicUnsupported', "Unable to delete file '{0}' atomically because provider does not support it.", this.resourceForError(resource)));1055}10561057if (useTrash && atomic) {1058throw new Error(localize('deleteFailedTrashAndAtomicUnsupported', "Unable to atomically delete file '{0}' because using trash is enabled.", this.resourceForError(resource)));1059}10601061// Validate delete1062let stat: IStat | undefined = undefined;1063try {1064stat = await provider.stat(resource);1065} catch (error) {1066// Handled later1067}10681069if (stat) {1070this.throwIfFileIsReadonly(resource, stat);1071} else {1072throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND);1073}10741075// Validate recursive1076const recursive = !!options?.recursive;1077if (!recursive) {1078const stat = await this.resolve(resource);1079if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) {1080throw new Error(localize('deleteFailedNonEmptyFolder', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource)));1081}1082}10831084return provider;1085}10861087async del(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<void> {1088const provider = await this.doValidateDelete(resource, options);10891090let deleteFileOptions = options;1091if (hasFileAtomicDeleteCapability(provider) && !deleteFileOptions?.atomic) {1092const enforcedAtomicDelete = provider.enforceAtomicDelete?.(resource);1093if (enforcedAtomicDelete) {1094deleteFileOptions = { ...options, atomic: enforcedAtomicDelete };1095}1096}10971098const useTrash = !!deleteFileOptions?.useTrash;1099const recursive = !!deleteFileOptions?.recursive;1100const atomic = deleteFileOptions?.atomic ?? false;11011102// Delete through provider1103await provider.delete(resource, { recursive, useTrash, atomic });11041105// Events1106this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));1107}11081109//#endregion11101111//#region Clone File11121113async cloneFile(source: URI, target: URI): Promise<void> {1114const sourceProvider = await this.withProvider(source);1115const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);11161117if (sourceProvider === targetProvider && this.getExtUri(sourceProvider).providerExtUri.isEqual(source, target)) {1118return; // return early if paths are equal1119}11201121// same provider, use `cloneFile` when native support is provided1122if (sourceProvider === targetProvider && hasFileCloneCapability(sourceProvider)) {1123return sourceProvider.cloneFile(source, target);1124}11251126// otherwise, either providers are different or there is no native1127// `cloneFile` support, then we fallback to emulate a clone as best1128// as we can with the other primitives11291130// create parent folders1131await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));11321133// leverage `copy` method if provided and providers are identical1134// queue on the source to ensure atomic read1135if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {1136return this.writeQueue.queueFor(source, () => sourceProvider.copy(source, target, { overwrite: true }), this.getExtUri(sourceProvider).providerExtUri);1137}11381139// otherwise copy via buffer/unbuffered and use a write queue1140// on the source to ensure atomic operation as much as possible1141return this.writeQueue.queueFor(source, () => this.doCopyFile(sourceProvider, source, targetProvider, target), this.getExtUri(sourceProvider).providerExtUri);1142}11431144//#endregion11451146//#region File Watching11471148private readonly internalOnDidFilesChange = this._register(new Emitter<FileChangesEvent>());11491150private readonly _onDidUncorrelatedFilesChange = this._register(new Emitter<FileChangesEvent>());1151readonly onDidFilesChange = this._onDidUncorrelatedFilesChange.event; // global `onDidFilesChange` skips correlated events11521153private readonly _onDidWatchError = this._register(new Emitter<Error>());1154readonly onDidWatchError = this._onDidWatchError.event;11551156private readonly activeWatchers = new Map<number /* watch request hash */, { disposable: IDisposable; count: number }>();11571158private static WATCHER_CORRELATION_IDS = 0;11591160createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation & { recursive: false }): IFileSystemWatcher {1161return this.watch(resource, {1162...options,1163// Explicitly set a correlation id so that file events that originate1164// from requests from extensions are exclusively routed back to the1165// extension host and not into the workbench.1166correlationId: FileService.WATCHER_CORRELATION_IDS++1167});1168}11691170watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;1171watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;1172watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IFileSystemWatcher | IDisposable {1173const disposables = new DisposableStore();11741175// Forward watch request to provider and wire in disposables1176let watchDisposed = false;1177let disposeWatch = () => { watchDisposed = true; };1178disposables.add(toDisposable(() => disposeWatch()));11791180// Watch and wire in disposable which is async but1181// check if we got disposed meanwhile and forward1182(async () => {1183try {1184const disposable = await this.doWatch(resource, options);1185if (watchDisposed) {1186dispose(disposable);1187} else {1188disposeWatch = () => dispose(disposable);1189}1190} catch (error) {1191this.logService.error(error);1192}1193})();11941195// When a correlation identifier is set, return a specific1196// watcher that only emits events matching that correalation.1197const correlationId = options.correlationId;1198if (typeof correlationId === 'number') {1199const fileChangeEmitter = disposables.add(new Emitter<FileChangesEvent>());1200disposables.add(this.internalOnDidFilesChange.event(e => {1201if (e.correlates(correlationId)) {1202fileChangeEmitter.fire(e);1203}1204}));12051206const watcher: IFileSystemWatcher = {1207onDidChange: fileChangeEmitter.event,1208dispose: () => disposables.dispose()1209};12101211return watcher;1212}12131214return disposables;1215}12161217private async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {1218const provider = await this.withProvider(resource);12191220// Deduplicate identical watch requests1221const watchHash = hash([this.getExtUri(provider).providerExtUri.getComparisonKey(resource), options]);1222let watcher = this.activeWatchers.get(watchHash);1223if (!watcher) {1224watcher = {1225count: 0,1226disposable: provider.watch(resource, options)1227};12281229this.activeWatchers.set(watchHash, watcher);1230}12311232// Increment usage counter1233watcher.count += 1;12341235return toDisposable(() => {1236if (watcher) {12371238// Unref1239watcher.count--;12401241// Dispose only when last user is reached1242if (watcher.count === 0) {1243dispose(watcher.disposable);1244this.activeWatchers.delete(watchHash);1245}1246}1247});1248}12491250override dispose(): void {1251super.dispose();12521253for (const [, watcher] of this.activeWatchers) {1254dispose(watcher.disposable);1255}12561257this.activeWatchers.clear();1258}12591260//#endregion12611262//#region Helpers12631264private readonly writeQueue = this._register(new ResourceQueue());12651266private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {1267return this.writeQueue.queueFor(resource, async () => {12681269// open handle1270const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false, append: options?.append ?? false });12711272// write into handle until all bytes from buffer have been written1273try {1274if (isReadableStream(readableOrStreamOrBufferedStream) || isReadableBufferedStream(readableOrStreamOrBufferedStream)) {1275await this.doWriteStreamBufferedQueued(provider, handle, readableOrStreamOrBufferedStream);1276} else {1277await this.doWriteReadableBufferedQueued(provider, handle, readableOrStreamOrBufferedStream);1278}1279} catch (error) {1280throw ensureFileSystemProviderError(error);1281} finally {12821283// close handle always1284await provider.close(handle);1285}1286}, this.getExtUri(provider).providerExtUri);1287}12881289private async doWriteStreamBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, streamOrBufferedStream: VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {1290let posInFile = 0;1291let stream: VSBufferReadableStream;12921293// Buffered stream: consume the buffer first by writing1294// it to the target before reading from the stream.1295if (isReadableBufferedStream(streamOrBufferedStream)) {1296if (streamOrBufferedStream.buffer.length > 0) {1297const chunk = VSBuffer.concat(streamOrBufferedStream.buffer);1298await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);12991300posInFile += chunk.byteLength;1301}13021303// If the stream has been consumed, return early1304if (streamOrBufferedStream.ended) {1305return;1306}13071308stream = streamOrBufferedStream.stream;1309}13101311// Unbuffered stream - just take as is1312else {1313stream = streamOrBufferedStream;1314}13151316return new Promise((resolve, reject) => {1317listenStream(stream, {1318onData: async chunk => {13191320// pause stream to perform async write operation1321stream.pause();13221323try {1324await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);1325} catch (error) {1326return reject(error);1327}13281329posInFile += chunk.byteLength;13301331// resume stream now that we have successfully written1332// run this on the next tick to prevent increasing the1333// execution stack because resume() may call the event1334// handler again before finishing.1335setTimeout(() => stream.resume());1336},1337onError: error => reject(error),1338onEnd: () => resolve()1339});1340});1341}13421343private async doWriteReadableBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, readable: VSBufferReadable): Promise<void> {1344let posInFile = 0;13451346let chunk: VSBuffer | null;1347while ((chunk = readable.read()) !== null) {1348await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);13491350posInFile += chunk.byteLength;1351}1352}13531354private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> {1355let totalBytesWritten = 0;1356while (totalBytesWritten < length) {13571358// Write through the provider1359const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten);1360totalBytesWritten += bytesWritten;1361}1362}13631364private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {1365return this.writeQueue.queueFor(resource, () => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream), this.getExtUri(provider).providerExtUri);1366}13671368private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {1369let buffer: VSBuffer;1370if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {1371buffer = bufferOrReadableOrStreamOrBufferedStream;1372} else if (isReadableStream(bufferOrReadableOrStreamOrBufferedStream)) {1373buffer = await streamToBuffer(bufferOrReadableOrStreamOrBufferedStream);1374} else if (isReadableBufferedStream(bufferOrReadableOrStreamOrBufferedStream)) {1375buffer = await bufferedStreamToBuffer(bufferOrReadableOrStreamOrBufferedStream);1376} else {1377buffer = readableToBuffer(bufferOrReadableOrStreamOrBufferedStream);1378}13791380// Write through the provider1381await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false, atomic: options?.atomic ?? false, append: options?.append ?? false });1382}13831384private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {1385return this.writeQueue.queueFor(target, () => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);1386}13871388private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {1389let sourceHandle: number | undefined = undefined;1390let targetHandle: number | undefined = undefined;13911392try {13931394// Open handles1395sourceHandle = await sourceProvider.open(source, { create: false });1396targetHandle = await targetProvider.open(target, { create: true, unlock: false });13971398const buffer = VSBuffer.alloc(this.BUFFER_SIZE);13991400let posInFile = 0;1401let posInBuffer = 0;1402let bytesRead = 0;1403do {1404// read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at1405// buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).1406bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);14071408// write into target (targetHandle) at current position (posInFile) from buffer (buffer) at1409// buffer position (posInBuffer) all bytes we read (bytesRead).1410await this.doWriteBuffer(targetProvider, targetHandle, buffer, bytesRead, posInFile, posInBuffer);14111412posInFile += bytesRead;1413posInBuffer += bytesRead;14141415// when buffer full, fill it again from the beginning1416if (posInBuffer === buffer.byteLength) {1417posInBuffer = 0;1418}1419} while (bytesRead > 0);1420} catch (error) {1421throw ensureFileSystemProviderError(error);1422} finally {1423await Promises.settled([1424typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(),1425typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(),1426]);1427}1428}14291430private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {1431return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);1432}14331434private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {1435return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false, atomic: false });1436}14371438private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {1439return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);1440}14411442private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {14431444// Open handle1445const targetHandle = await targetProvider.open(target, { create: true, unlock: false });14461447// Read entire buffer from source and write buffered1448try {1449const buffer = await sourceProvider.readFile(source);1450await this.doWriteBuffer(targetProvider, targetHandle, VSBuffer.wrap(buffer), buffer.byteLength, 0, 0);1451} catch (error) {1452throw ensureFileSystemProviderError(error);1453} finally {1454await targetProvider.close(targetHandle);1455}1456}14571458private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {14591460// Read buffer via stream buffered1461const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));14621463// Write buffer into target at once1464await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);1465}14661467protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {1468if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {1469throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);1470}14711472return provider;1473}14741475private throwIfFileIsReadonly(resource: URI, stat: IStat): void {1476if ((stat.permissions ?? 0) & FilePermission.Readonly) {1477throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);1478}1479}14801481private resourceForError(resource: URI): string {1482if (resource.scheme === Schemas.file) {1483return resource.fsPath;1484}14851486return resource.toString(true);1487}14881489//#endregion1490}149114921493