Path: blob/main/src/vs/platform/files/common/fileService.ts
3296 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, 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),262etag: etag({ mtime: stat.mtime, size: stat.size }),263children: undefined264};265266// check to recurse for directories267if (fileStat.isDirectory && recurse(fileStat, siblings)) {268try {269const entries = await provider.readdir(resource);270const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {271try {272const childResource = providerExtUri.joinPath(resource, name);273const childStat = resolveMetadata ? await provider.stat(childResource) : { type };274275return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse);276} catch (error) {277this.logService.trace(error);278279return null; // can happen e.g. due to permission errors280}281}));282283// make sure to get rid of null values that signal a failure to resolve a particular entry284fileStat.children = coalesce(resolvedEntries);285} catch (error) {286this.logService.trace(error);287288fileStat.children = []; // gracefully handle errors, we may not have permissions to read289}290291return fileStat;292}293294return fileStat;295}296297async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]>;298async resolveAll(toResolve: { resource: URI; options: IResolveMetadataFileOptions }[]): Promise<IFileStatResultWithMetadata[]>;299async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]> {300return Promises.settled(toResolve.map(async entry => {301try {302return { stat: await this.doResolveFile(entry.resource, entry.options), success: true };303} catch (error) {304this.logService.trace(error);305306return { stat: undefined, success: false };307}308}));309}310311async stat(resource: URI): Promise<IFileStatWithPartialMetadata> {312const provider = await this.withProvider(resource);313314const stat = await provider.stat(resource);315316return this.toFileStat(provider, resource, stat, undefined, true, () => false /* Do not resolve any children */);317}318319async realpath(resource: URI): Promise<URI | undefined> {320const provider = await this.withProvider(resource);321322if (hasFileRealpathCapability(provider)) {323const realpath = await provider.realpath(resource);324325return resource.with({ path: realpath });326}327328return undefined;329}330331async exists(resource: URI): Promise<boolean> {332const provider = await this.withProvider(resource);333334try {335const stat = await provider.stat(resource);336337return !!stat;338} catch (error) {339return false;340}341}342343//#endregion344345//#region File Reading/Writing346347async canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true> {348try {349await this.doValidateCreateFile(resource, options);350} catch (error) {351return error;352}353354return true;355}356357private async doValidateCreateFile(resource: URI, options?: ICreateFileOptions): Promise<void> {358359// validate overwrite360if (!options?.overwrite && await this.exists(resource)) {361throw 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);362}363}364365async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {366367// validate368await this.doValidateCreateFile(resource, options);369370// do write into file (this will create it too)371const fileStat = await this.writeFile(resource, bufferOrReadableOrStream);372373// events374this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));375376return fileStat;377}378379async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {380const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource);381const { providerExtUri } = this.getExtUri(provider);382383let writeFileOptions = options;384if (hasFileAtomicWriteCapability(provider) && !writeFileOptions?.atomic) {385const enforcedAtomicWrite = provider.enforceAtomicWriteFile?.(resource);386if (enforcedAtomicWrite) {387writeFileOptions = { ...options, atomic: enforcedAtomicWrite };388}389}390391try {392393// validate write (this may already return a peeked-at buffer)394let { stat, buffer: bufferOrReadableOrStreamOrBufferedStream } = await this.validateWriteFile(provider, resource, bufferOrReadableOrStream, writeFileOptions);395396// mkdir recursively as needed397if (!stat) {398await this.mkdirp(provider, providerExtUri.dirname(resource));399}400401// optimization: if the provider has unbuffered write capability and the data402// to write is not a buffer, we consume up to 3 chunks and try to write the data403// unbuffered to reduce the overhead. If the stream or readable has more data404// to provide we continue to write buffered.405if (!bufferOrReadableOrStreamOrBufferedStream) {406bufferOrReadableOrStreamOrBufferedStream = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);407}408409// write file: unbuffered410if (411!hasOpenReadWriteCloseCapability(provider) || // buffered writing is unsupported412(hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) || // data is a full buffer already413(hasReadWriteCapability(provider) && hasFileAtomicWriteCapability(provider) && writeFileOptions?.atomic) // atomic write forces unbuffered write if the provider supports it414) {415await this.doWriteUnbuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream);416}417418// write file: buffered419else {420await this.doWriteBuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);421}422423// events424this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.WRITE));425} catch (error) {426throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), writeFileOptions);427}428429return this.resolve(resource, { resolveMetadata: true });430}431432433private async peekBufferForWriting(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream> {434let peekResult: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream;435if (hasReadWriteCapability(provider) && !(bufferOrReadableOrStream instanceof VSBuffer)) {436if (isReadableStream(bufferOrReadableOrStream)) {437const bufferedStream = await peekStream(bufferOrReadableOrStream, 3);438if (bufferedStream.ended) {439peekResult = VSBuffer.concat(bufferedStream.buffer);440} else {441peekResult = bufferedStream;442}443} else {444peekResult = peekReadable(bufferOrReadableOrStream, data => VSBuffer.concat(data), 3);445}446} else {447peekResult = bufferOrReadableOrStream;448}449450return peekResult;451}452453private async validateWriteFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<{ stat: IStat | undefined; buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined }> {454455// Validate unlock support456const unlock = !!options?.unlock;457if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {458throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));459}460461// Validate atomic support462const atomic = !!options?.atomic;463if (atomic) {464if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) {465throw new Error(localize('writeFailedAtomicUnsupported1', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource)));466}467468if (!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite)) {469throw new Error(localize('writeFailedAtomicUnsupported2', "Unable to atomically write file '{0}' because provider does not support unbuffered writes.", this.resourceForError(resource)));470}471472if (unlock) {473throw new Error(localize('writeFailedAtomicUnlock', "Unable to unlock file '{0}' because atomic write is enabled.", this.resourceForError(resource)));474}475}476477// Validate via file stat meta data478let stat: IStat | undefined = undefined;479try {480stat = await provider.stat(resource);481} catch (error) {482return Object.create(null); // file might not exist483}484485// File cannot be directory486if ((stat.type & FileType.Directory) !== 0) {487throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);488}489490// File cannot be readonly491this.throwIfFileIsReadonly(resource, stat);492493// Dirty write prevention: if the file on disk has been changed and does not match our expected494// mtime and etag, we bail out to prevent dirty writing.495//496// First, we check for a mtime that is in the future before we do more checks. The assumption is497// that only the mtime is an indicator for a file that has changed on disk.498//499// Second, if the mtime has advanced, we compare the size of the file on disk with our previous500// one using the etag() function. Relying only on the mtime check has prooven to produce false501// positives due to file system weirdness (especially around remote file systems). As such, the502// check for size is a weaker check because it can return a false negative if the file has changed503// but to the same length. This is a compromise we take to avoid having to produce checksums of504// the file content for comparison which would be much slower to compute.505//506// Third, if the etag() turns out to be different, we do one attempt to compare the buffer we507// are about to write with the contents on disk to figure out if the contents are identical.508// In that case we allow the writing as it would result in the same contents in the file.509let buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined;510if (511typeof options?.mtime === 'number' && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED &&512typeof stat.mtime === 'number' && typeof stat.size === 'number' &&513options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size })514) {515buffer = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);516if (buffer instanceof VSBuffer && buffer.byteLength === stat.size) {517try {518const { value } = await this.readFile(resource, { limits: { size: stat.size } });519if (buffer.equals(value)) {520return { stat, buffer }; // allow writing since contents are identical521}522} catch (error) {523// ignore, throw the FILE_MODIFIED_SINCE error524}525}526527throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options);528}529530return { stat, buffer };531}532533async readFile(resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {534const provider = await this.withReadProvider(resource);535536if (options?.atomic) {537return this.doReadFileAtomic(provider, resource, options, token);538}539540return this.doReadFile(provider, resource, options, token);541}542543private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {544return new Promise<IFileContent>((resolve, reject) => {545this.writeQueue.queueFor(resource, async () => {546try {547const content = await this.doReadFile(provider, resource, options, token);548resolve(content);549} catch (error) {550reject(error);551}552}, this.getExtUri(provider).providerExtUri);553});554}555556private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {557const stream = await this.doReadFileStream(provider, resource, {558...options,559// optimization: since we know that the caller does not560// care about buffering, we indicate this to the reader.561// this reduces all the overhead the buffered reading562// has (open, read, close) if the provider supports563// unbuffered reading.564preferUnbuffered: true565}, token);566567return {568...stream,569value: await streamToBuffer(stream.value)570};571}572573async readFileStream(resource: URI, options?: IReadFileStreamOptions, token?: CancellationToken): Promise<IFileStreamContent> {574const provider = await this.withReadProvider(resource);575576return this.doReadFileStream(provider, resource, options, token);577}578579private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions & { preferUnbuffered?: boolean }, token?: CancellationToken): Promise<IFileStreamContent> {580581// install a cancellation token that gets cancelled582// when any error occurs. this allows us to resolve583// the content of the file while resolving metadata584// but still cancel the operation in certain cases.585//586// in addition, we pass the optional token in that587// we got from the outside to even allow for external588// cancellation of the read operation.589const cancellableSource = new CancellationTokenSource(token);590591let readFileOptions = options;592if (hasFileAtomicReadCapability(provider) && provider.enforceAtomicReadFile?.(resource)) {593readFileOptions = { ...options, atomic: true };594}595596// validate read operation597const statPromise = this.validateReadFile(resource, readFileOptions).then(stat => stat, error => {598cancellableSource.dispose(true);599600throw error;601});602603let fileStream: VSBufferReadableStream | undefined = undefined;604try {605606// if the etag is provided, we await the result of the validation607// due to the likelihood of hitting a NOT_MODIFIED_SINCE result.608// otherwise, we let it run in parallel to the file reading for609// optimal startup performance.610if (typeof readFileOptions?.etag === 'string' && readFileOptions.etag !== ETAG_DISABLED) {611await statPromise;612}613614// read unbuffered615if (616(readFileOptions?.atomic && hasFileAtomicReadCapability(provider)) || // atomic reads are always unbuffered617!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || // provider has no buffered capability618(hasReadWriteCapability(provider) && readFileOptions?.preferUnbuffered) // unbuffered read is preferred619) {620fileStream = this.readFileUnbuffered(provider, resource, readFileOptions);621}622623// read streamed (always prefer over primitive buffered read)624else if (hasFileReadStreamCapability(provider)) {625fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, readFileOptions);626}627628// read buffered629else {630fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, readFileOptions);631}632633fileStream.on('end', () => cancellableSource.dispose());634fileStream.on('error', () => cancellableSource.dispose());635636const fileStat = await statPromise;637638return {639...fileStat,640value: fileStream641};642} catch (error) {643644// Await the stream to finish so that we exit this method645// in a consistent state with file handles closed646// (https://github.com/microsoft/vscode/issues/114024)647if (fileStream) {648await consumeStream(fileStream);649}650651// Re-throw errors as file operation errors but preserve652// specific errors (such as not modified since)653throw this.restoreReadError(error, resource, readFileOptions);654}655}656657private restoreReadError(error: Error, resource: URI, options?: IReadFileStreamOptions): FileOperationError {658const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString());659660if (error instanceof NotModifiedSinceFileOperationError) {661return new NotModifiedSinceFileOperationError(message, error.stat, options);662}663664if (error instanceof TooLargeFileOperationError) {665return new TooLargeFileOperationError(message, error.fileOperationResult, error.size, error.options as IReadFileOptions);666}667668return new FileOperationError(message, toFileOperationResult(error), options);669}670671private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {672const fileStream = provider.readFileStream(resource, options, token);673674return transform(fileStream, {675data: data => data instanceof VSBuffer ? data : VSBuffer.wrap(data),676error: error => this.restoreReadError(error, resource, options)677}, data => VSBuffer.concat(data));678}679680private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {681const stream = newWriteableBufferStream();682683readFileIntoStream(provider, resource, stream, data => data, {684...options,685bufferSize: this.BUFFER_SIZE,686errorTransformer: error => this.restoreReadError(error, resource, options)687}, token);688689return stream;690}691692private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithFileAtomicReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions): VSBufferReadableStream {693const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));694695// Read the file into the stream async but do not wait for696// this to complete because streams work via events697(async () => {698try {699let buffer: Uint8Array;700if (options?.atomic && hasFileAtomicReadCapability(provider)) {701buffer = await provider.readFile(resource, { atomic: true });702} else {703buffer = await provider.readFile(resource);704}705706// respect position option707if (typeof options?.position === 'number') {708buffer = buffer.slice(options.position);709}710711// respect length option712if (typeof options?.length === 'number') {713buffer = buffer.slice(0, options.length);714}715716// Throw if file is too large to load717this.validateReadFileLimits(resource, buffer.byteLength, options);718719// End stream with data720stream.end(VSBuffer.wrap(buffer));721} catch (err) {722stream.error(err);723stream.end();724}725})();726727return stream;728}729730private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {731const stat = await this.resolve(resource, { resolveMetadata: true });732733// Throw if resource is a directory734if (stat.isDirectory) {735throw new FileOperationError(localize('fileIsDirectoryReadError', "Unable to read file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);736}737738// Throw if file not modified since (unless disabled)739if (typeof options?.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) {740throw new NotModifiedSinceFileOperationError(localize('fileNotModifiedError', "File not modified since"), stat, options);741}742743// Throw if file is too large to load744this.validateReadFileLimits(resource, stat.size, options);745746return stat;747}748749private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {750if (typeof options?.limits?.size === 'number' && size > options.limits.size) {751throw new TooLargeFileOperationError(localize('fileTooLargeError', "Unable to read file '{0}' that is too large to open", this.resourceForError(resource)), FileOperationResult.FILE_TOO_LARGE, size, options);752}753}754755//#endregion756757//#region Move/Copy/Delete/Create Folder758759async canMove(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {760return this.doCanMoveCopy(source, target, 'move', overwrite);761}762763async canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {764return this.doCanMoveCopy(source, target, 'copy', overwrite);765}766767private async doCanMoveCopy(source: URI, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<Error | true> {768if (source.toString() !== target.toString()) {769try {770const sourceProvider = mode === 'move' ? this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source) : await this.withReadProvider(source);771const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);772773await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite);774} catch (error) {775return error;776}777}778779return true;780}781782async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {783const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source);784const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);785786// move787const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', !!overwrite);788789// resolve and send events790const fileStat = await this.resolve(target, { resolveMetadata: true });791this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat));792793return fileStat;794}795796async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {797const sourceProvider = await this.withReadProvider(source);798const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);799800// copy801const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', !!overwrite);802803// resolve and send events804const fileStat = await this.resolve(target, { resolveMetadata: true });805this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat));806807return fileStat;808}809810private async doMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite: boolean): Promise<'move' | 'copy'> {811if (source.toString() === target.toString()) {812return mode; // simulate node.js behaviour here and do a no-op if paths match813}814815// validation816const { exists, isSameResourceWithDifferentPathCase } = await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite);817818// delete as needed (unless target is same resurce with different path case)819if (exists && !isSameResourceWithDifferentPathCase && overwrite) {820await this.del(target, { recursive: true });821}822823// create parent folders824await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));825826// copy source => target827if (mode === 'copy') {828829// same provider with fast copy: leverage copy() functionality830if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {831await sourceProvider.copy(source, target, { overwrite });832}833834// when copying via buffer/unbuffered, we have to manually835// traverse the source if it is a folder and not a file836else {837const sourceFile = await this.resolve(source);838if (sourceFile.isDirectory) {839await this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target);840} else {841await this.doCopyFile(sourceProvider, source, targetProvider, target);842}843}844845return mode;846}847848// move source => target849else {850851// same provider: leverage rename() functionality852if (sourceProvider === targetProvider) {853await sourceProvider.rename(source, target, { overwrite });854855return mode;856}857858// across providers: copy to target & delete at source859else {860await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite);861await this.del(source, { recursive: true });862863return 'copy';864}865}866}867868private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI): Promise<void> {869870// copy: source (buffered) => target (buffered)871if (hasOpenReadWriteCloseCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) {872return this.doPipeBuffered(sourceProvider, source, targetProvider, target);873}874875// copy: source (buffered) => target (unbuffered)876if (hasOpenReadWriteCloseCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) {877return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target);878}879880// copy: source (unbuffered) => target (buffered)881if (hasReadWriteCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) {882return this.doPipeUnbufferedToBuffered(sourceProvider, source, targetProvider, target);883}884885// copy: source (unbuffered) => target (unbuffered)886if (hasReadWriteCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) {887return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target);888}889}890891private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI): Promise<void> {892893// create folder in target894await targetProvider.mkdir(targetFolder);895896// create children in target897if (Array.isArray(sourceFolder.children)) {898await Promises.settled(sourceFolder.children.map(async sourceChild => {899const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name);900if (sourceChild.isDirectory) {901return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild);902} else {903return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild);904}905}));906}907}908909private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean; isSameResourceWithDifferentPathCase: boolean }> {910let isSameResourceWithDifferentPathCase = false;911912// Check if source is equal or parent to target (requires providers to be the same)913if (sourceProvider === targetProvider) {914const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider);915if (!isPathCaseSensitive) {916isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target);917}918919if (isSameResourceWithDifferentPathCase && mode === 'copy') {920throw 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)));921}922923if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) {924throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));925}926}927928// Extra checks if target exists and this is not a rename929const exists = await this.exists(target);930if (exists && !isSameResourceWithDifferentPathCase) {931932// Bail out if target exists and we are not about to overwrite933if (!overwrite) {934throw 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);935}936937// Special case: if the target is a parent of the source, we cannot delete938// it as it would delete the source as well. In this case we have to throw939if (sourceProvider === targetProvider) {940const { providerExtUri } = this.getExtUri(sourceProvider);941if (providerExtUri.isEqualOrParent(source, target)) {942throw 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)));943}944}945}946947return { exists, isSameResourceWithDifferentPathCase };948}949950private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri; isPathCaseSensitive: boolean } {951const isPathCaseSensitive = this.isPathCaseSensitive(provider);952953return {954providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,955isPathCaseSensitive956};957}958959private isPathCaseSensitive(provider: IFileSystemProvider): boolean {960return !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);961}962963async createFolder(resource: URI): Promise<IFileStatWithMetadata> {964const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);965966// mkdir recursively967await this.mkdirp(provider, resource);968969// events970const fileStat = await this.resolve(resource, { resolveMetadata: true });971this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));972973return fileStat;974}975976private async mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {977const directoriesToCreate: string[] = [];978979// mkdir until we reach root980const { providerExtUri } = this.getExtUri(provider);981while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {982try {983const stat = await provider.stat(directory);984if ((stat.type & FileType.Directory) === 0) {985throw new Error(localize('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", this.resourceForError(directory)));986}987988break; // we have hit a directory that exists -> good989} catch (error) {990991// Bubble up any other error that is not file not found992if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) {993throw error;994}995996// Upon error, remember directories that need to be created997directoriesToCreate.push(providerExtUri.basename(directory));998999// Continue up1000directory = providerExtUri.dirname(directory);1001}1002}10031004// Create directories as needed1005for (let i = directoriesToCreate.length - 1; i >= 0; i--) {1006directory = providerExtUri.joinPath(directory, directoriesToCreate[i]);10071008try {1009await provider.mkdir(directory);1010} catch (error) {1011if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileExists) {1012// For mkdirp() we tolerate that the mkdir() call fails1013// in case the folder already exists. This follows node.js1014// own implementation of fs.mkdir({ recursive: true }) and1015// reduces the chances of race conditions leading to errors1016// if multiple calls try to create the same folders1017// As such, we only throw an error here if it is other than1018// the fact that the file already exists.1019// (see also https://github.com/microsoft/vscode/issues/89834)1020throw error;1021}1022}1023}1024}10251026async canDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<Error | true> {1027try {1028await this.doValidateDelete(resource, options);1029} catch (error) {1030return error;1031}10321033return true;1034}10351036private async doValidateDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<IFileSystemProvider> {1037const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);10381039// Validate trash support1040const useTrash = !!options?.useTrash;1041if (useTrash && !(provider.capabilities & FileSystemProviderCapabilities.Trash)) {1042throw new Error(localize('deleteFailedTrashUnsupported', "Unable to delete file '{0}' via trash because provider does not support it.", this.resourceForError(resource)));1043}10441045// Validate atomic support1046const atomic = options?.atomic;1047if (atomic && !(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete)) {1048throw new Error(localize('deleteFailedAtomicUnsupported', "Unable to delete file '{0}' atomically because provider does not support it.", this.resourceForError(resource)));1049}10501051if (useTrash && atomic) {1052throw new Error(localize('deleteFailedTrashAndAtomicUnsupported', "Unable to atomically delete file '{0}' because using trash is enabled.", this.resourceForError(resource)));1053}10541055// Validate delete1056let stat: IStat | undefined = undefined;1057try {1058stat = await provider.stat(resource);1059} catch (error) {1060// Handled later1061}10621063if (stat) {1064this.throwIfFileIsReadonly(resource, stat);1065} else {1066throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND);1067}10681069// Validate recursive1070const recursive = !!options?.recursive;1071if (!recursive) {1072const stat = await this.resolve(resource);1073if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) {1074throw new Error(localize('deleteFailedNonEmptyFolder', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource)));1075}1076}10771078return provider;1079}10801081async del(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<void> {1082const provider = await this.doValidateDelete(resource, options);10831084let deleteFileOptions = options;1085if (hasFileAtomicDeleteCapability(provider) && !deleteFileOptions?.atomic) {1086const enforcedAtomicDelete = provider.enforceAtomicDelete?.(resource);1087if (enforcedAtomicDelete) {1088deleteFileOptions = { ...options, atomic: enforcedAtomicDelete };1089}1090}10911092const useTrash = !!deleteFileOptions?.useTrash;1093const recursive = !!deleteFileOptions?.recursive;1094const atomic = deleteFileOptions?.atomic ?? false;10951096// Delete through provider1097await provider.delete(resource, { recursive, useTrash, atomic });10981099// Events1100this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));1101}11021103//#endregion11041105//#region Clone File11061107async cloneFile(source: URI, target: URI): Promise<void> {1108const sourceProvider = await this.withProvider(source);1109const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);11101111if (sourceProvider === targetProvider && this.getExtUri(sourceProvider).providerExtUri.isEqual(source, target)) {1112return; // return early if paths are equal1113}11141115// same provider, use `cloneFile` when native support is provided1116if (sourceProvider === targetProvider && hasFileCloneCapability(sourceProvider)) {1117return sourceProvider.cloneFile(source, target);1118}11191120// otherwise, either providers are different or there is no native1121// `cloneFile` support, then we fallback to emulate a clone as best1122// as we can with the other primitives11231124// create parent folders1125await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));11261127// leverage `copy` method if provided and providers are identical1128// queue on the source to ensure atomic read1129if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {1130return this.writeQueue.queueFor(source, () => sourceProvider.copy(source, target, { overwrite: true }), this.getExtUri(sourceProvider).providerExtUri);1131}11321133// otherwise copy via buffer/unbuffered and use a write queue1134// on the source to ensure atomic operation as much as possible1135return this.writeQueue.queueFor(source, () => this.doCopyFile(sourceProvider, source, targetProvider, target), this.getExtUri(sourceProvider).providerExtUri);1136}11371138//#endregion11391140//#region File Watching11411142private readonly internalOnDidFilesChange = this._register(new Emitter<FileChangesEvent>());11431144private readonly _onDidUncorrelatedFilesChange = this._register(new Emitter<FileChangesEvent>());1145readonly onDidFilesChange = this._onDidUncorrelatedFilesChange.event; // global `onDidFilesChange` skips correlated events11461147private readonly _onDidWatchError = this._register(new Emitter<Error>());1148readonly onDidWatchError = this._onDidWatchError.event;11491150private readonly activeWatchers = new Map<number /* watch request hash */, { disposable: IDisposable; count: number }>();11511152private static WATCHER_CORRELATION_IDS = 0;11531154createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation & { recursive: false }): IFileSystemWatcher {1155return this.watch(resource, {1156...options,1157// Explicitly set a correlation id so that file events that originate1158// from requests from extensions are exclusively routed back to the1159// extension host and not into the workbench.1160correlationId: FileService.WATCHER_CORRELATION_IDS++1161});1162}11631164watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;1165watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;1166watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IFileSystemWatcher | IDisposable {1167const disposables = new DisposableStore();11681169// Forward watch request to provider and wire in disposables1170let watchDisposed = false;1171let disposeWatch = () => { watchDisposed = true; };1172disposables.add(toDisposable(() => disposeWatch()));11731174// Watch and wire in disposable which is async but1175// check if we got disposed meanwhile and forward1176(async () => {1177try {1178const disposable = await this.doWatch(resource, options);1179if (watchDisposed) {1180dispose(disposable);1181} else {1182disposeWatch = () => dispose(disposable);1183}1184} catch (error) {1185this.logService.error(error);1186}1187})();11881189// When a correlation identifier is set, return a specific1190// watcher that only emits events matching that correalation.1191const correlationId = options.correlationId;1192if (typeof correlationId === 'number') {1193const fileChangeEmitter = disposables.add(new Emitter<FileChangesEvent>());1194disposables.add(this.internalOnDidFilesChange.event(e => {1195if (e.correlates(correlationId)) {1196fileChangeEmitter.fire(e);1197}1198}));11991200const watcher: IFileSystemWatcher = {1201onDidChange: fileChangeEmitter.event,1202dispose: () => disposables.dispose()1203};12041205return watcher;1206}12071208return disposables;1209}12101211private async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {1212const provider = await this.withProvider(resource);12131214// Deduplicate identical watch requests1215const watchHash = hash([this.getExtUri(provider).providerExtUri.getComparisonKey(resource), options]);1216let watcher = this.activeWatchers.get(watchHash);1217if (!watcher) {1218watcher = {1219count: 0,1220disposable: provider.watch(resource, options)1221};12221223this.activeWatchers.set(watchHash, watcher);1224}12251226// Increment usage counter1227watcher.count += 1;12281229return toDisposable(() => {1230if (watcher) {12311232// Unref1233watcher.count--;12341235// Dispose only when last user is reached1236if (watcher.count === 0) {1237dispose(watcher.disposable);1238this.activeWatchers.delete(watchHash);1239}1240}1241});1242}12431244override dispose(): void {1245super.dispose();12461247for (const [, watcher] of this.activeWatchers) {1248dispose(watcher.disposable);1249}12501251this.activeWatchers.clear();1252}12531254//#endregion12551256//#region Helpers12571258private readonly writeQueue = this._register(new ResourceQueue());12591260private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {1261return this.writeQueue.queueFor(resource, async () => {12621263// open handle1264const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false });12651266// write into handle until all bytes from buffer have been written1267try {1268if (isReadableStream(readableOrStreamOrBufferedStream) || isReadableBufferedStream(readableOrStreamOrBufferedStream)) {1269await this.doWriteStreamBufferedQueued(provider, handle, readableOrStreamOrBufferedStream);1270} else {1271await this.doWriteReadableBufferedQueued(provider, handle, readableOrStreamOrBufferedStream);1272}1273} catch (error) {1274throw ensureFileSystemProviderError(error);1275} finally {12761277// close handle always1278await provider.close(handle);1279}1280}, this.getExtUri(provider).providerExtUri);1281}12821283private async doWriteStreamBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, streamOrBufferedStream: VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {1284let posInFile = 0;1285let stream: VSBufferReadableStream;12861287// Buffered stream: consume the buffer first by writing1288// it to the target before reading from the stream.1289if (isReadableBufferedStream(streamOrBufferedStream)) {1290if (streamOrBufferedStream.buffer.length > 0) {1291const chunk = VSBuffer.concat(streamOrBufferedStream.buffer);1292await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);12931294posInFile += chunk.byteLength;1295}12961297// If the stream has been consumed, return early1298if (streamOrBufferedStream.ended) {1299return;1300}13011302stream = streamOrBufferedStream.stream;1303}13041305// Unbuffered stream - just take as is1306else {1307stream = streamOrBufferedStream;1308}13091310return new Promise((resolve, reject) => {1311listenStream(stream, {1312onData: async chunk => {13131314// pause stream to perform async write operation1315stream.pause();13161317try {1318await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);1319} catch (error) {1320return reject(error);1321}13221323posInFile += chunk.byteLength;13241325// resume stream now that we have successfully written1326// run this on the next tick to prevent increasing the1327// execution stack because resume() may call the event1328// handler again before finishing.1329setTimeout(() => stream.resume());1330},1331onError: error => reject(error),1332onEnd: () => resolve()1333});1334});1335}13361337private async doWriteReadableBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, readable: VSBufferReadable): Promise<void> {1338let posInFile = 0;13391340let chunk: VSBuffer | null;1341while ((chunk = readable.read()) !== null) {1342await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);13431344posInFile += chunk.byteLength;1345}1346}13471348private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> {1349let totalBytesWritten = 0;1350while (totalBytesWritten < length) {13511352// Write through the provider1353const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten);1354totalBytesWritten += bytesWritten;1355}1356}13571358private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {1359return this.writeQueue.queueFor(resource, () => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream), this.getExtUri(provider).providerExtUri);1360}13611362private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {1363let buffer: VSBuffer;1364if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {1365buffer = bufferOrReadableOrStreamOrBufferedStream;1366} else if (isReadableStream(bufferOrReadableOrStreamOrBufferedStream)) {1367buffer = await streamToBuffer(bufferOrReadableOrStreamOrBufferedStream);1368} else if (isReadableBufferedStream(bufferOrReadableOrStreamOrBufferedStream)) {1369buffer = await bufferedStreamToBuffer(bufferOrReadableOrStreamOrBufferedStream);1370} else {1371buffer = readableToBuffer(bufferOrReadableOrStreamOrBufferedStream);1372}13731374// Write through the provider1375await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false, atomic: options?.atomic ?? false });1376}13771378private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {1379return this.writeQueue.queueFor(target, () => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);1380}13811382private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {1383let sourceHandle: number | undefined = undefined;1384let targetHandle: number | undefined = undefined;13851386try {13871388// Open handles1389sourceHandle = await sourceProvider.open(source, { create: false });1390targetHandle = await targetProvider.open(target, { create: true, unlock: false });13911392const buffer = VSBuffer.alloc(this.BUFFER_SIZE);13931394let posInFile = 0;1395let posInBuffer = 0;1396let bytesRead = 0;1397do {1398// read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at1399// buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).1400bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);14011402// write into target (targetHandle) at current position (posInFile) from buffer (buffer) at1403// buffer position (posInBuffer) all bytes we read (bytesRead).1404await this.doWriteBuffer(targetProvider, targetHandle, buffer, bytesRead, posInFile, posInBuffer);14051406posInFile += bytesRead;1407posInBuffer += bytesRead;14081409// when buffer full, fill it again from the beginning1410if (posInBuffer === buffer.byteLength) {1411posInBuffer = 0;1412}1413} while (bytesRead > 0);1414} catch (error) {1415throw ensureFileSystemProviderError(error);1416} finally {1417await Promises.settled([1418typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(),1419typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(),1420]);1421}1422}14231424private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {1425return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);1426}14271428private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {1429return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false, atomic: false });1430}14311432private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {1433return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);1434}14351436private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {14371438// Open handle1439const targetHandle = await targetProvider.open(target, { create: true, unlock: false });14401441// Read entire buffer from source and write buffered1442try {1443const buffer = await sourceProvider.readFile(source);1444await this.doWriteBuffer(targetProvider, targetHandle, VSBuffer.wrap(buffer), buffer.byteLength, 0, 0);1445} catch (error) {1446throw ensureFileSystemProviderError(error);1447} finally {1448await targetProvider.close(targetHandle);1449}1450}14511452private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {14531454// Read buffer via stream buffered1455const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));14561457// Write buffer into target at once1458await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);1459}14601461protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {1462if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {1463throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);1464}14651466return provider;1467}14681469private throwIfFileIsReadonly(resource: URI, stat: IStat): void {1470if ((stat.permissions ?? 0) & FilePermission.Readonly) {1471throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);1472}1473}14741475private resourceForError(resource: URI): string {1476if (resource.scheme === Schemas.file) {1477return resource.fsPath;1478}14791480return resource.toString(true);1481}14821483//#endregion1484}148514861487