src/vs/platform/files/common/fileService.ts

1491 LOC · 1104 covered · 387 uncovered · 307 ranges · 3296 concepts · 77 introducers · 1763 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- fileService.ts ×18
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { coalesce } from '../../../base/common/arrays.js';
7 > import { Promises, ResourceQueue } from '../../../base/common/async.js';
8 > import { bufferedStreamToBuffer, bufferToReadable, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer, VSBufferReadable, VSBufferReadableBufferedStream, VSBufferReadableStream } from '../../../base/common/buffer.js';
9 > import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
10 > import { Emitter } from '../../../base/common/event.js';
11 > import { hash } from '../../../base/common/hash.js';
12 > import { Iterable } from '../../../base/common/iterator.js';
13 > import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
14 > import { TernarySearchTree } from '../../../base/common/ternarySearchTree.js';
15 > import { Schemas } from '../../../base/common/network.js';
16 > import { mark } from '../../../base/common/performance.js';
17 > import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from '../../../base/common/resources.js';
18 > import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from '../../../base/common/stream.js';
19 > import { URI } from '../../../base/common/uri.js';
20 > import { localize } from '../../../nls.js';
21 > import { 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';
22 > import { readFileIntoStream } from './io.js';
23 > import { ILogService } from '../../log/common/log.js';
24 > import { ErrorNoTelemetry } from '../../../base/common/errors.js';
25 >
26 > export class FileService extends Disposable implements IFileService {
27 >
28 > declare readonly _serviceBrand: undefined;
29 >
30 > // Choose a buffer size that is a balance between memory needs and
31 > // manageable IPC overhead. The larger the buffer size, the less
32 > // roundtrips we have to do for reading/writing data.
33 > private readonly BUFFER_SIZE = 256 * 1024;
34 >
35 > constructor(@ILogService private readonly logService: ILogService) {
36 > super(); fileService.ts ×56
37 > }
38 >
39 > //#region File System Provider
40 >
41 > private readonly _onDidChangeFileSystemProviderRegistrations = this._register(new Emitter<IFileSystemProviderRegistrationEvent>());
42 > readonly onDidChangeFileSystemProviderRegistrations = this._onDidChangeFileSystemProviderRegistrations.event;
43 >
44 > private readonly _onWillActivateFileSystemProvider = this._register(new Emitter<IFileSystemProviderActivationEvent>());
45 > readonly onWillActivateFileSystemProvider = this._onWillActivateFileSystemProvider.event;
46 >
47 > private readonly _onDidChangeFileSystemProviderCapabilities = this._register(new Emitter<IFileSystemProviderCapabilitiesChangeEvent>());
48 > readonly onDidChangeFileSystemProviderCapabilities = this._onDidChangeFileSystemProviderCapabilities.event;
49 >
50 > private readonly provider = new Map<string, IFileSystemProvider>();
51 >
52 > registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
53 > if (this.provider.has(scheme)) { fileService.ts ×5
54 throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`);
55 }
57 > mark(`code/registerFilesystem/${scheme}`);
58 >
59 > const providerDisposables = new DisposableStore();
60 >
61 > // Add provider with event
62 > this.provider.set(scheme, provider);
63 > this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
64 >
65 > // Forward events from provider
66 > providerDisposables.add(provider.onDidChangeFile(changes => {
67 > const event = new FileChangesEvent(changes, !this.isPathCaseSensitive(provider)); fileService.ts ×1
68 >
69 > // Always emit any event internally
70 > this.internalOnDidFilesChange.fire(event);
71 >
72 > // Only emit uncorrelated events in the global `onDidFilesChange` event
73 > if (!event.hasCorrelation()) {
74 > this._onDidUncorrelatedFilesChange.fire(event);
75 > }
76 > })); fileService.ts ×5
77 > if (typeof provider.onDidWatchError === 'function') {
78 > providerDisposables.add(provider.onDidWatchError(error => this._onDidWatchError.fire(new Error(error)))); diskFileSystemProvider.ts ×15
79 > }
80 > providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme }))); fileService.ts ×5
81 >
82 > return toDisposable(() => {
83 > this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider }); fileService.ts ×1
84 > this.provider.delete(scheme);
85 >
86 > dispose(providerDisposables);
88 > }
90 > getProvider(scheme: string): IFileSystemProvider | undefined {
91 return this.provider.get(scheme);
92 }
94 > async activateProvider(scheme: string): Promise<void> {
96 > // Emit an event that we are about to activate a provider with the given scheme.
97 > // Listeners can participate in the activation by registering a provider for it.
98 > const joiners: Promise<void>[] = [];
99 > this._onWillActivateFileSystemProvider.fire({
100 > scheme,
101 > join(promise) {
102 joiners.push(promise);
103 },
104 > }); fileService.ts ×6
105 >
106 > if (this.provider.has(scheme)) {
107 > return; // provider is already here so we can return directly fileService.ts ×2
108 > }
110 > // If the provider is not yet there, make sure to join on the listeners assuming
111 > // that it takes a bit longer to register the file system provider.
112 > await Promises.settled(joiners);
115 > async canHandleResource(resource: URI): Promise<boolean> {
116
117 // Await activation of potentially extension contributed providers
118 await this.activateProvider(resource.scheme);
119
120 return this.hasProvider(resource);
121 }
123 > hasProvider(resource: URI): boolean {
124 > return this.provider.has(resource.scheme); fileService.ts ×1
125 > }
127 > hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean {
128 > const provider = this.provider.get(resource.scheme); fileService.ts ×1
129 >
130 > return !!(provider && (provider.capabilities & capability));
131 > }
133 > listCapabilities(): Iterable<{ scheme: string; capabilities: FileSystemProviderCapabilities }> {
134 return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities }));
135 }
137 > protected async withProvider(resource: URI): Promise<IFileSystemProvider> {
139 > // Assert path is absolute
140 > if (!isAbsolutePath(resource)) {
141 throw new FileOperationError(localize('invalidPath', "Unable to resolve filesystem provider with relative file path '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_INVALID_PATH);
142 }
144 > // Activate provider
145 > await this.activateProvider(resource.scheme);
146 >
147 > // Assert provider
148 > const provider = this.provider.get(resource.scheme);
149 > if (!provider) {
150 > const error = new ErrorNoTelemetry(); fileService.ts ×2
151 > error.message = localize('noProviderFound', "ENOPRO: No file system provider found for resource '{0}'", resource.toString());
152 >
153 > throw error;
154 > }
156 > return provider;
159 > private async withReadProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability> {
160 > const provider = await this.withProvider(resource); fileService.ts ×2
161 >
162 > if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider) || hasFileReadStreamCapability(provider)) {
163 > return provider;
164 > }
165
166 throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`);
169 > private async withWriteProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability> {
170 > const provider = await this.withProvider(resource); fileService.ts ×2
171 >
172 > if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider)) {
173 > return provider;
174 > }
175
176 throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`);
179 > //#endregion
180 >
181 > //#region Operation events
182 >
183 > private readonly _onDidRunOperation = this._register(new Emitter<FileOperationEvent>());
184 > readonly onDidRunOperation = this._onDidRunOperation.event;
185 >
186 > //#endregion
187 >
188 > //#region File Metadata Resolving
189 >
190 > async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
191 > async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
192 > async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
193 > try { fileService.ts ×5
194 > return await this.doResolveFile(resource, options);
195 > } catch (error) {
197 > // Specially handle file not found case as file operation result
198 > if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) {
199 > throw new FileOperationError(localize('fileNotFoundError', "Unable to resolve nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); fileService.ts ×1
200 > }
202 > // Bubble up any other error as is
203 > throw ensureFileSystemProviderError(error);
204 > }
207 > private async doResolveFile(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
208 > private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
209 > private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
210 > const provider = await this.withProvider(resource); fileService.ts ×5
211 > const isPathCaseSensitive = this.isPathCaseSensitive(provider); fileService.ts ×1
212 >
213 > const resolveTo = options?.resolveTo;
214 > const resolveSingleChildDescendants = options?.resolveSingleChildDescendants; fileService.ts ×5
215 > const resolveMetadata = options?.resolveMetadata;
216 >
217 > const stat = await provider.stat(resource);
219 > let trie: TernarySearchTree<URI, boolean> | undefined;
220 >
221 > return this.toFileStat(provider, resource, stat, undefined, !!resolveMetadata, (stat, siblings) => {
223 > // lazy trie to check for recursive resolving
224 > if (!trie) {
225 > trie = TernarySearchTree.forUris<true>(() => !isPathCaseSensitive);
226 > trie.set(resource, true);
227 > if (resolveTo) {
228 trie.fill(true, resolveTo);
229 }
231 >
232 > // check for recursive resolving
233 > if (trie.get(stat.resource) || trie.findSuperstr(stat.resource.with({ query: null, fragment: null } /* required for https://github.com/microsoft/vscode/issues/128151 */))) {
234 > return true;
235 > }
237 > // check for resolving single child folders
238 > if (stat.isDirectory && resolveSingleChildDescendants) { fileService.ts ×6
239 return siblings === 1;
240 }
242 > return false;
243 > }); fileService.ts ×4
246 > private 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>;
247 > private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStatWithMetadata>;
248 > private 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> {
249 > const { providerExtUri } = this.getExtUri(provider); fileService.ts ×4
250 >
251 > // convert to file stat
252 > const fileStat: IFileStat = {
253 > resource,
254 > name: providerExtUri.basename(resource),
255 > isFile: (stat.type & FileType.File) !== 0,
256 > isDirectory: (stat.type & FileType.Directory) !== 0,
257 > isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
258 > mtime: stat.mtime,
259 > ctime: stat.ctime,
260 > size: stat.size,
261 > readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly),
262 > locked: Boolean((stat.permissions ?? 0) & FilePermission.Locked),
263 > executable: Boolean((stat.permissions ?? 0) & FilePermission.Executable),
264 > etag: etag({ mtime: stat.mtime, size: stat.size }),
265 > children: undefined
266 > };
267 >
268 > // check to recurse for directories
269 > if (fileStat.isDirectory && recurse(fileStat, siblings)) {
270 > try { fileService.ts ×6
271 > const entries = await provider.readdir(resource);
272 > const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {
273 > try { fileService.ts ×1
274 > const childResource = providerExtUri.joinPath(resource, name);
275 > const childStat = resolveMetadata ? await provider.stat(childResource) : { type };
276 >
277 > return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse);
278 > } catch (error) {
279 this.logService.trace(error);
280
281 return null; // can happen e.g. due to permission errors
282 }
283 > })); fileService.ts ×6
284 >
285 > // make sure to get rid of null values that signal a failure to resolve a particular entry
286 > fileStat.children = coalesce(resolvedEntries);
287 > } catch (error) {
288 this.logService.trace(error);
289
290 fileStat.children = []; // gracefully handle errors, we may not have permissions to read
291 }
293 > return fileStat;
294 > }
296 > return fileStat;
299 > async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]>;
300 > async resolveAll(toResolve: { resource: URI; options: IResolveMetadataFileOptions }[]): Promise<IFileStatResultWithMetadata[]>;
301 > async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]> {
302 > return Promises.settled(toResolve.map(async entry => { fileService.ts ×2
303 > try {
304 > return { stat: await this.doResolveFile(entry.resource, entry.options), success: true };
305 > } catch (error) {
306 > this.logService.trace(error); fileService.ts ×1
307 >
308 > return { stat: undefined, success: false };
309 > }
310 > })); fileService.ts ×2
311 > }
313 > async stat(resource: URI): Promise<IFileStatWithPartialMetadata> {
314 > const provider = await this.withProvider(resource); extensionsScannerService.ts ×30
315 >
316 > const stat = await provider.stat(resource);
317 >
318 > return this.toFileStat(provider, resource, stat, undefined, true, () => false /* Do not resolve any children */);
319 > }
321 > async realpath(resource: URI): Promise<URI | undefined> {
322 > const provider = await this.withProvider(resource); fileService.ts ×2
323 >
324 > if (hasFileRealpathCapability(provider)) {
325 > const realpath = await provider.realpath(resource); fileService.ts ×1
326 >
327 > return resource.with({ path: realpath });
328 > }
330 > return undefined;
333 > async exists(resource: URI): Promise<boolean> {
334 > const provider = await this.withProvider(resource); fileService.ts ×2
336 > try {
337 > const stat = await provider.stat(resource);
339 > return !!stat;
340 > } catch (error) { fileService.ts ×2
341 > return false; fileService.ts ×1
342 > }
345 > //#endregion
346 >
347 > //#region File Reading/Writing
348 >
349 > async canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true> {
350 try {
351 await this.doValidateCreateFile(resource, options);
352 } catch (error) {
353 return error;
354 }
355
356 return true;
357 }
359 > private async doValidateCreateFile(resource: URI, options?: ICreateFileOptions): Promise<void> {
361 > // validate overwrite
362 > if (!options?.overwrite && await this.exists(resource)) {
363 throw 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);
364 }
367 > async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
369 > // validate
370 > await this.doValidateCreateFile(resource, options);
371 >
372 > // do write into file (this will create it too)
373 > const fileStat = await this.writeFile(resource, bufferOrReadableOrStream);
374 >
375 > // events
376 > this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
377 >
378 > return fileStat;
379 > }
381 > async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
382 > const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); fileService.ts ×17
383 > const { providerExtUri } = this.getExtUri(provider);
384 >
385 > let writeFileOptions = options;
386 > if (hasFileAtomicWriteCapability(provider) && !writeFileOptions?.atomic) {
387 > const enforcedAtomicWrite = provider.enforceAtomicWriteFile?.(resource); diskFileSystemProvider.ts ×11
388 > if (enforcedAtomicWrite) {
389 writeFileOptions = { ...options, atomic: enforcedAtomicWrite };
390 }
393 > try {
394 >
395 > // validate write (this may already return a peeked-at buffer)
396 > let { stat, buffer: bufferOrReadableOrStreamOrBufferedStream } = await this.validateWriteFile(provider, resource, bufferOrReadableOrStream, writeFileOptions);
397 >
398 > // mkdir recursively as needed
399 > if (!stat) {
400 > await this.mkdirp(provider, providerExtUri.dirname(resource)); fileService.ts ×2
401 > }
403 > // optimization: if the provider has unbuffered write capability and the data
404 > // to write is not a buffer, we consume up to 3 chunks and try to write the data
405 > // unbuffered to reduce the overhead. If the stream or readable has more data
406 > // to provide we continue to write buffered.
407 > if (!bufferOrReadableOrStreamOrBufferedStream) {
408 > bufferOrReadableOrStreamOrBufferedStream = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);
409 > }
410 >
411 > // write file: unbuffered
412 > if (
413 > !hasOpenReadWriteCloseCapability(provider) || // buffered writing is unsupported
414 > (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) || // data is a full buffer already
415 (hasReadWriteCapability(provider) && hasFileAtomicWriteCapability(provider) && writeFileOptions?.atomic) // atomic write forces unbuffered write if the provider supports it
417 > await this.doWriteUnbuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream);
418 > }
419
420 // write file: buffered
421 else {
422 await this.doWriteBuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
423 }
425 > // events
426 > this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.WRITE));
427 > } catch (error) {
428 throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), writeFileOptions);
429 }
431 > return this.resolve(resource, { resolveMetadata: true });
432 > }
434 >
435 > private async peekBufferForWriting(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream> {
436 > let peekResult: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream; fileService.ts ×17
437 > if (hasReadWriteCapability(provider) && !(bufferOrReadableOrStream instanceof VSBuffer)) {
438 if (isReadableStream(bufferOrReadableOrStream)) {
439 const bufferedStream = await peekStream(bufferOrReadableOrStream, 3);
440 if (bufferedStream.ended) {
441 peekResult = VSBuffer.concat(bufferedStream.buffer);
442 } else {
443 peekResult = bufferedStream;
444 }
445 } else {
446 peekResult = peekReadable(bufferOrReadableOrStream, data => VSBuffer.concat(data), 3);
447 }
448 > } else { fileService.ts ×17
449 > peekResult = bufferOrReadableOrStream;
450 > }
451 >
452 > return peekResult;
453 > }
455 > private async validateWriteFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<{ stat: IStat | undefined; buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined }> {
457 > // Validate unlock support
458 > const unlock = !!options?.unlock;
459 > if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {
460 throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
461 }
463 > // Validate append support
464 > if (options?.append && !hasFileAppendCapability(provider)) {
465 throw new FileOperationError(localize('err.noAppend', "Filesystem provider for scheme '{0}' does not does not support append", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
466 }
468 > // Validate atomic support
469 > const atomic = !!options?.atomic;
470 > if (atomic) {
471 > if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) { diskFileSystemProvider.ts ×9
472 throw new Error(localize('writeFailedAtomicUnsupported1', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource)));
473 }
475 > if (!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite)) {
476 throw new Error(localize('writeFailedAtomicUnsupported2', "Unable to atomically write file '{0}' because provider does not support unbuffered writes.", this.resourceForError(resource)));
477 }
479 > if (unlock) {
480 throw new Error(localize('writeFailedAtomicUnlock', "Unable to unlock file '{0}' because atomic write is enabled.", this.resourceForError(resource)));
481 }
484 > // Validate via file stat meta data
485 > let stat: IStat | undefined = undefined;
486 > try {
487 > stat = await provider.stat(resource);
488 > } catch (error) {
489 > return Object.create(null); // file might not exist fileService.ts ×2
490 > }
492 > // File cannot be directory
493 > if ((stat.type & FileType.Directory) !== 0) {
494 throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
495 }
497 > // File cannot be readonly
498 > this.throwIfFileIsReadonly(resource, stat);
499 >
500 > // Dirty write prevention: if the file on disk has been changed and does not match our expected
501 > // mtime and etag, we bail out to prevent dirty writing.
502 > //
503 > // First, we check for a mtime that is in the future before we do more checks. The assumption is
504 > // that only the mtime is an indicator for a file that has changed on disk.
505 > //
506 > // Second, if the mtime has advanced, we compare the size of the file on disk with our previous
507 > // one using the etag() function. Relying only on the mtime check has prooven to produce false
508 > // positives due to file system weirdness (especially around remote file systems). As such, the
509 > // check for size is a weaker check because it can return a false negative if the file has changed
510 > // but to the same length. This is a compromise we take to avoid having to produce checksums of
511 > // the file content for comparison which would be much slower to compute.
512 > //
513 > // Third, if the etag() turns out to be different, we do one attempt to compare the buffer we
514 > // are about to write with the contents on disk to figure out if the contents are identical.
515 > // In that case we allow the writing as it would result in the same contents in the file.
516 > let buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined;
517 > if (
518 > typeof options?.mtime === 'number' && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && fileService.ts ×17
519 > typeof stat.mtime === 'number' && typeof stat.size === 'number' &&
520 > options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size })
521 > ) {
522 buffer = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);
523 if (buffer instanceof VSBuffer && buffer.byteLength === stat.size) {
524 try {
525 const { value } = await this.readFile(resource, { limits: { size: stat.size } });
526 if (buffer.equals(value)) {
527 return { stat, buffer }; // allow writing since contents are identical
528 }
529 } catch (error) {
530 // ignore, throw the FILE_MODIFIED_SINCE error
531 }
532 }
533
534 throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options);
535 }
537 > return { stat, buffer };
540 > async readFile(resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
541 > const provider = await this.withReadProvider(resource); fileService.ts ×11
542 >
543 > if (options?.atomic) {
544 return this.doReadFileAtomic(provider, resource, options, token);
545 }
547 > return this.doReadFile(provider, resource, options, token);
548 > }
550 > private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
551 return new Promise<IFileContent>((resolve, reject) => {
552 this.writeQueue.queueFor(resource, async () => {
553 try {
554 const content = await this.doReadFile(provider, resource, options, token);
555 resolve(content);
556 } catch (error) {
557 reject(error);
558 }
559 }, this.getExtUri(provider).providerExtUri);
560 });
561 }
563 > private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
564 > const stream = await this.doReadFileStream(provider, resource, { fileService.ts ×11
565 > ...options,
566 > // optimization: since we know that the caller does not
567 > // care about buffering, we indicate this to the reader.
568 > // this reduces all the overhead the buffered reading
569 > // has (open, read, close) if the provider supports
570 > // unbuffered reading.
571 > preferUnbuffered: true
572 > }, token);
574 > return {
575 > ...stream,
576 > value: await streamToBuffer(stream.value)
577 > };
580 > async readFileStream(resource: URI, options?: IReadFileStreamOptions, token?: CancellationToken): Promise<IFileStreamContent> {
581 > const provider = await this.withReadProvider(resource); io.ts ×12
582 >
583 > return this.doReadFileStream(provider, resource, options, token);
584 > }
586 > private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions & { preferUnbuffered?: boolean }, token?: CancellationToken): Promise<IFileStreamContent> {
588 > // install a cancellation token that gets cancelled
589 > // when any error occurs. this allows us to resolve
590 > // the content of the file while resolving metadata
591 > // but still cancel the operation in certain cases.
592 > //
593 > // in addition, we pass the optional token in that
594 > // we got from the outside to even allow for external
595 > // cancellation of the read operation.
596 > const cancellableSource = new CancellationTokenSource(token);
597 >
598 > let readFileOptions = options;
599 > if (hasFileAtomicReadCapability(provider) && provider.enforceAtomicReadFile?.(resource)) {
600 readFileOptions = { ...options, atomic: true };
601 }
603 > // validate read operation
604 > const statPromise = this.validateReadFile(resource, readFileOptions).then(stat => stat, error => {
605 > cancellableSource.dispose(true); fileService.ts ×6
606 >
607 > throw error;
609 >
610 > let fileStream: VSBufferReadableStream | undefined = undefined;
611 > try {
612 >
613 > // if the etag is provided, we await the result of the validation
614 > // due to the likelihood of hitting a NOT_MODIFIED_SINCE result.
615 > // otherwise, we let it run in parallel to the file reading for
616 > // optimal startup performance.
617 > if (typeof readFileOptions?.etag === 'string' && readFileOptions.etag !== ETAG_DISABLED) {
618 await statPromise;
619 }
621 > // read unbuffered
622 > if (
623 > (readFileOptions?.atomic && hasFileAtomicReadCapability(provider)) || // atomic reads are always unbuffered
624 > !(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || // provider has no buffered capability
625 > (hasReadWriteCapability(provider) && readFileOptions?.preferUnbuffered) // unbuffered read is preferred
626 > ) {
627 > fileStream = this.readFileUnbuffered(provider, resource, readFileOptions); fileService.ts ×11
628 > }
629 > io.ts ×12
630 > // read streamed (always prefer over primitive buffered read)
631 > else if (hasFileReadStreamCapability(provider)) {
632 > fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, readFileOptions);
633 > }
634
635 // read buffered
636 else {
637 fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, readFileOptions);
638 }
640 > fileStream.on('end', () => cancellableSource.dispose());
641 > fileStream.on('error', () => cancellableSource.dispose());
642 >
643 > const fileStat = await statPromise;
645 > return {
646 > ...fileStat,
647 > value: fileStream
648 > };
649 > } catch (error) { fileService.ts ×10
651 > // Await the stream to finish so that we exit this method
652 > // in a consistent state with file handles closed
653 > // (https://github.com/microsoft/vscode/issues/114024)
654 > if (fileStream) {
655 > await consumeStream(fileStream);
656 > }
657 >
658 > // Re-throw errors as file operation errors but preserve
659 > // specific errors (such as not modified since)
660 > throw this.restoreReadError(error, resource, readFileOptions);
661 > }
664 > private restoreReadError(error: Error, resource: URI, options?: IReadFileStreamOptions): FileOperationError {
665 > const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()); fileService.ts ×6
666 >
667 > if (error instanceof NotModifiedSinceFileOperationError) {
668 return new NotModifiedSinceFileOperationError(message, error.stat, options);
669 }
671 > if (error instanceof TooLargeFileOperationError) {
672 return new TooLargeFileOperationError(message, error.fileOperationResult, error.size, error.options as IReadFileOptions);
673 }
675 > return new FileOperationError(message, toFileOperationResult(error), options);
676 > }
678 > private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
679 > const fileStream = provider.readFileStream(resource, options, token); io.ts ×12
680 >
681 > return transform(fileStream, {
682 > data: data => data instanceof VSBuffer ? data : VSBuffer.wrap(data),
683 > error: error => this.restoreReadError(error, resource, options)
684 > }, data => VSBuffer.concat(data));
685 > }
687 > private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
688 const stream = newWriteableBufferStream();
689
690 readFileIntoStream(provider, resource, stream, data => data, {
691 ...options,
692 bufferSize: this.BUFFER_SIZE,
693 errorTransformer: error => this.restoreReadError(error, resource, options)
694 }, token);
695
696 return stream;
697 }
699 > private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithFileAtomicReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions): VSBufferReadableStream {
700 > const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data)); fileService.ts ×11
701 >
702 > // Read the file into the stream async but do not wait for
703 > // this to complete because streams work via events
704 > (async () => {
705 > try {
706 > let buffer: Uint8Array;
707 > if (options?.atomic && hasFileAtomicReadCapability(provider)) {
708 buffer = await provider.readFile(resource, { atomic: true });
709 > } else { fileService.ts ×11
710 > buffer = await provider.readFile(resource);
712 >
713 > // respect position option
714 > if (typeof options?.position === 'number') { fileService.ts ×11
715 buffer = buffer.slice(options.position);
716 }
718 > // respect length option
719 > if (typeof options?.length === 'number') { fileService.ts ×11
720 buffer = buffer.slice(0, options.length);
721 }
723 > // Throw if file is too large to load
724 > this.validateReadFileLimits(resource, buffer.byteLength, options);
725 >
726 > // End stream with data
727 > stream.end(VSBuffer.wrap(buffer));
728 > } catch (err) { fileService.ts ×11
729 > stream.error(err); fileService.ts ×6
730 > stream.end();
731 > }
732 > })(); fileService.ts ×11
733 >
734 > return stream;
735 > }
737 > private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {
738 > const stat = await this.resolve(resource, { resolveMetadata: true }); fileService.ts ×10
740 > // Throw if resource is a directory
741 > if (stat.isDirectory) {
742 throw new FileOperationError(localize('fileIsDirectoryReadError', "Unable to read file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
743 }
745 > // Throw if file not modified since (unless disabled)
746 > if (typeof options?.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) { fileService.ts ×10
747 throw new NotModifiedSinceFileOperationError(localize('fileNotModifiedError', "File not modified since"), stat, options);
748 }
750 > // Throw if file is too large to load
751 > this.validateReadFileLimits(resource, stat.size, options);
752 >
753 > return stat;
756 > private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {
757 > if (typeof options?.limits?.size === 'number' && size > options.limits.size) { fileService.ts ×6
758 throw new TooLargeFileOperationError(localize('fileTooLargeError', "Unable to read file '{0}' that is too large to open", this.resourceForError(resource)), FileOperationResult.FILE_TOO_LARGE, size, options);
759 }
762 > //#endregion
763 >
764 > //#region Move/Copy/Delete/Create Folder
765 >
766 > async canMove(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {
767 return this.doCanMoveCopy(source, target, 'move', overwrite);
768 }
770 > async canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {
771 return this.doCanMoveCopy(source, target, 'copy', overwrite);
772 }
774 > private async doCanMoveCopy(source: URI, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<Error | true> {
775 if (source.toString() !== target.toString()) {
776 try {
777 const sourceProvider = mode === 'move' ? this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source) : await this.withReadProvider(source);
778 const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
779
780 await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite);
781 } catch (error) {
782 return error;
783 }
784 }
785
786 return true;
787 }
789 > async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
790 > const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source); fileService.ts ×7
791 > const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
792 >
793 > // move
794 > const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', !!overwrite);
795 >
796 > // resolve and send events
797 > const fileStat = await this.resolve(target, { resolveMetadata: true });
798 > this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat));
799 >
800 > return fileStat;
801 > }
803 > async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
804 > const sourceProvider = await this.withReadProvider(source); agentPluginManager.ts ×19
805 > const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
806 >
807 > // copy
808 > const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', !!overwrite);
810 > // resolve and send events
811 > const fileStat = await this.resolve(target, { resolveMetadata: true });
812 > this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat)); agentPluginManager.ts ×19
813 >
814 > return fileStat;
815 > }
817 > private async doMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite: boolean): Promise<'move' | 'copy'> {
818 > if (source.toString() === target.toString()) { fileService.ts ×7
819 return mode; // simulate node.js behaviour here and do a no-op if paths match
820 }
822 > // validation
823 > const { exists, isSameResourceWithDifferentPathCase } = await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite);
824 >
825 > // delete as needed (unless target is same resurce with different path case)
826 > if (exists && !isSameResourceWithDifferentPathCase && overwrite) {
827 await this.del(target, { recursive: true });
828 }
830 > // create parent folders
831 > await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));
832 >
833 > // copy source => target
834 > if (mode === 'copy') {
836 > // same provider with fast copy: leverage copy() functionality
837 > if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {
838 await sourceProvider.copy(source, target, { overwrite });
839 }
841 > // when copying via buffer/unbuffered, we have to manually
842 > // traverse the source if it is a folder and not a file
843 > else {
844 > const sourceFile = await this.resolve(source);
845 > if (sourceFile.isDirectory) { fileService.ts ×15
846 > await this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target);
847 > } else {
848 await this.doCopyFile(sourceProvider, source, targetProvider, target);
849 }
852 > return mode;
853 > }
855 > // move source => target
856 > else {
857 >
858 > // same provider: leverage rename() functionality
859 > if (sourceProvider === targetProvider) {
860 > await sourceProvider.rename(source, target, { overwrite });
861 >
862 > return mode;
863 > }
864
865 // across providers: copy to target & delete at source
866 else {
867 await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite);
868 await this.del(source, { recursive: true });
869
870 return 'copy';
871 }
875 > private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI): Promise<void> {
877 > // copy: source (buffered) => target (buffered)
878 > if (hasOpenReadWriteCloseCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) {
879 > return this.doPipeBuffered(sourceProvider, source, targetProvider, target);
880 > }
881
882 // copy: source (buffered) => target (unbuffered)
883 > if (hasOpenReadWriteCloseCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) { fileService.ts ×15
884 return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target);
885 }
886
887 // copy: source (unbuffered) => target (buffered)
888 > if (hasReadWriteCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) { fileService.ts ×15
889 return this.doPipeUnbufferedToBuffered(sourceProvider, source, targetProvider, target);
890 }
891
892 // copy: source (unbuffered) => target (unbuffered)
893 > if (hasReadWriteCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) { fileService.ts ×15
894 return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target);
895 }
898 > private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI): Promise<void> {
900 > // create folder in target
901 > await targetProvider.mkdir(targetFolder);
902 >
903 > // create children in target
904 > if (Array.isArray(sourceFolder.children)) {
905 > await Promises.settled(sourceFolder.children.map(async sourceChild => {
906 > const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name);
907 > if (sourceChild.isDirectory) {
908 return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild);
909 > } else { fileService.ts ×15
910 > return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild);
911 > }
912 > }));
913 > }
914 > }
916 > private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean; isSameResourceWithDifferentPathCase: boolean }> {
917 > let isSameResourceWithDifferentPathCase = false; fileService.ts ×7
918 >
919 > // Check if source is equal or parent to target (requires providers to be the same)
920 > if (sourceProvider === targetProvider) {
921 > const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); fileService.ts ×7
922 > if (!isPathCaseSensitive) {
923 isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target);
924 }
926 > if (isSameResourceWithDifferentPathCase && mode === 'copy') {
927 throw 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)));
928 }
930 > if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) {
931 throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
932 }
935 > // Extra checks if target exists and this is not a rename
936 > const exists = await this.exists(target);
937 > if (exists && !isSameResourceWithDifferentPathCase) {
938
939 // Bail out if target exists and we are not about to overwrite
940 if (!overwrite) {
941 throw 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);
942 }
943
944 // Special case: if the target is a parent of the source, we cannot delete
945 // it as it would delete the source as well. In this case we have to throw
946 if (sourceProvider === targetProvider) {
947 const { providerExtUri } = this.getExtUri(sourceProvider);
948 if (providerExtUri.isEqualOrParent(source, target)) {
949 throw 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)));
950 }
951 }
952 }
954 > return { exists, isSameResourceWithDifferentPathCase };
955 > }
957 > private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri; isPathCaseSensitive: boolean } {
958 > const isPathCaseSensitive = this.isPathCaseSensitive(provider); fileService.ts ×1
959 >
960 > return {
961 > providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
962 > isPathCaseSensitive
963 > };
964 > }
966 > private isPathCaseSensitive(provider: IFileSystemProvider): boolean {
967 > return !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); fileService.ts ×1
968 > }
970 > async createFolder(resource: URI): Promise<IFileStatWithMetadata> {
971 > const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); fileService.ts ×1
972 >
973 > // mkdir recursively
974 > await this.mkdirp(provider, resource);
975 >
976 > // events
977 > const fileStat = await this.resolve(resource, { resolveMetadata: true });
978 > this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
979 >
980 > return fileStat;
981 > }
983 > private async mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {
984 > const directoriesToCreate: string[] = []; fileService.ts ×3
985 >
986 > // mkdir until we reach root
987 > const { providerExtUri } = this.getExtUri(provider);
988 > while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {
989 > try { fileService.ts ×5
990 > const stat = await provider.stat(directory);
991 > if ((stat.type & FileType.Directory) === 0) { fileService.ts ×2
992 throw new Error(localize('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", this.resourceForError(directory)));
993 }
995 > break; // we have hit a directory that exists -> good
996 > } catch (error) { fileService.ts ×5
997 >
998 > // Bubble up any other error that is not file not found
999 > if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) {
1000 throw error;
1001 }
1003 > // Upon error, remember directories that need to be created
1004 > directoriesToCreate.push(providerExtUri.basename(directory));
1005 >
1006 > // Continue up
1007 > directory = providerExtUri.dirname(directory);
1008 > }
1009 > }
1011 > // Create directories as needed
1012 > for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
1013 > directory = providerExtUri.joinPath(directory, directoriesToCreate[i]); fileService.ts ×5
1014 >
1015 > try {
1016 > await provider.mkdir(directory);
1017 > } catch (error) {
1018 > if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileExists) { fileService.ts ×2
1019 // For mkdirp() we tolerate that the mkdir() call fails
1020 // in case the folder already exists. This follows node.js
1021 // own implementation of fs.mkdir({ recursive: true }) and
1022 // reduces the chances of race conditions leading to errors
1023 // if multiple calls try to create the same folders
1024 // As such, we only throw an error here if it is other than
1025 // the fact that the file already exists.
1026 // (see also https://github.com/microsoft/vscode/issues/89834)
1027 throw error;
1028 }
1033 > async canDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<Error | true> {
1034 try {
1035 await this.doValidateDelete(resource, options);
1036 } catch (error) {
1037 return error;
1038 }
1039
1040 return true;
1041 }
1043 > private async doValidateDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<IFileSystemProvider> {
1044 > const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); fileService.ts ×12
1045 >
1046 > // Validate trash support
1047 > const useTrash = !!options?.useTrash;
1048 > if (useTrash && !(provider.capabilities & FileSystemProviderCapabilities.Trash)) {
1049 throw new Error(localize('deleteFailedTrashUnsupported', "Unable to delete file '{0}' via trash because provider does not support it.", this.resourceForError(resource)));
1050 }
1052 > // Validate atomic support
1053 > const atomic = options?.atomic;
1054 > if (atomic && !(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete)) {
1055 throw new Error(localize('deleteFailedAtomicUnsupported', "Unable to delete file '{0}' atomically because provider does not support it.", this.resourceForError(resource)));
1056 }
1058 > if (useTrash && atomic) {
1059 throw new Error(localize('deleteFailedTrashAndAtomicUnsupported', "Unable to atomically delete file '{0}' because using trash is enabled.", this.resourceForError(resource)));
1060 }
1062 > // Validate delete
1063 > let stat: IStat | undefined = undefined;
1064 > try {
1065 > stat = await provider.stat(resource);
1066 > } catch (error) {
1067 > // Handled later fileService.ts ×2
1068 > }
1070 > if (stat) {
1071 > this.throwIfFileIsReadonly(resource, stat); fileService.ts ×5
1072 > } else { fileService.ts ×12
1073 > throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); fileService.ts ×2
1074 > }
1076 > // Validate recursive
1077 > const recursive = !!options?.recursive;
1078 > if (!recursive) { fileService.ts ×12
1079 > const stat = await this.resolve(resource); fileService.ts ×2
1080 > if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) {
1081 throw new Error(localize('deleteFailedNonEmptyFolder', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource)));
1082 }
1085 > return provider;
1088 > async del(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<void> {
1089 > const provider = await this.doValidateDelete(resource, options); fileService.ts ×12
1091 > let deleteFileOptions = options;
1092 > if (hasFileAtomicDeleteCapability(provider) && !deleteFileOptions?.atomic) { fileService.ts ×12
1093 > const enforcedAtomicDelete = provider.enforceAtomicDelete?.(resource); diskFileSystemProvider.ts ×20
1094 > if (enforcedAtomicDelete) {
1095 deleteFileOptions = { ...options, atomic: enforcedAtomicDelete };
1096 }
1099 > const useTrash = !!deleteFileOptions?.useTrash;
1100 > const recursive = !!deleteFileOptions?.recursive; fileService.ts ×12
1101 > const atomic = deleteFileOptions?.atomic ?? false;
1102 >
1103 > // Delete through provider
1104 > await provider.delete(resource, { recursive, useTrash, atomic });
1106 > // Events
1107 > this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
1110 > //#endregion
1111 >
1112 > //#region Clone File
1113 >
1114 > async cloneFile(source: URI, target: URI): Promise<void> {
1115 const sourceProvider = await this.withProvider(source);
1116 const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
1117
1118 if (sourceProvider === targetProvider && this.getExtUri(sourceProvider).providerExtUri.isEqual(source, target)) {
1119 return; // return early if paths are equal
1120 }
1121
1122 // same provider, use `cloneFile` when native support is provided
1123 if (sourceProvider === targetProvider && hasFileCloneCapability(sourceProvider)) {
1124 return sourceProvider.cloneFile(source, target);
1125 }
1126
1127 // otherwise, either providers are different or there is no native
1128 // `cloneFile` support, then we fallback to emulate a clone as best
1129 // as we can with the other primitives
1130
1131 // create parent folders
1132 await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));
1133
1134 // leverage `copy` method if provided and providers are identical
1135 // queue on the source to ensure atomic read
1136 if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {
1137 return this.writeQueue.queueFor(source, () => sourceProvider.copy(source, target, { overwrite: true }), this.getExtUri(sourceProvider).providerExtUri);
1138 }
1139
1140 // otherwise copy via buffer/unbuffered and use a write queue
1141 // on the source to ensure atomic operation as much as possible
1142 return this.writeQueue.queueFor(source, () => this.doCopyFile(sourceProvider, source, targetProvider, target), this.getExtUri(sourceProvider).providerExtUri);
1143 }
1145 > //#endregion
1146 >
1147 > //#region File Watching
1148 >
1149 > private readonly internalOnDidFilesChange = this._register(new Emitter<FileChangesEvent>());
1150 >
1151 > private readonly _onDidUncorrelatedFilesChange = this._register(new Emitter<FileChangesEvent>());
1152 > readonly onDidFilesChange = this._onDidUncorrelatedFilesChange.event; // global `onDidFilesChange` skips correlated events
1153 >
1154 > private readonly _onDidWatchError = this._register(new Emitter<Error>());
1155 > readonly onDidWatchError = this._onDidWatchError.event;
1156 >
1157 > private readonly activeWatchers = new Map<number /* watch request hash */, { disposable: IDisposable; count: number }>();
1158 >
1159 > private static WATCHER_CORRELATION_IDS = 0;
1160 >
1161 > createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation & { recursive: false }): IFileSystemWatcher {
1162 > return this.watch(resource, { fileService.ts ×3
1163 > ...options,
1164 > // Explicitly set a correlation id so that file events that originate
1165 > // from requests from extensions are exclusively routed back to the
1166 > // extension host and not into the workbench.
1167 > correlationId: FileService.WATCHER_CORRELATION_IDS++
1168 > });
1169 > }
1171 > watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
1172 > watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;
1173 > watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IFileSystemWatcher | IDisposable {
1174 > const disposables = new DisposableStore(); fileService.ts ×6
1175 >
1176 > // Forward watch request to provider and wire in disposables
1177 > let watchDisposed = false;
1178 > let disposeWatch = () => { watchDisposed = true; };
1179 > disposables.add(toDisposable(() => disposeWatch()));
1180 >
1181 > // Watch and wire in disposable which is async but
1182 > // check if we got disposed meanwhile and forward
1183 > (async () => {
1184 > try {
1185 > const disposable = await this.doWatch(resource, options);
1186 > if (watchDisposed) { fileService.ts ×3
1187 > dispose(disposable); fileService.ts ×1
1188 > } else { fileService.ts ×3
1189 > disposeWatch = () => dispose(disposable); fileService.ts ×1
1190 > }
1191 > } catch (error) { fileService.ts ×6
1192 > this.logService.error(error); fileService.ts ×1
1193 > }
1194 > })(); fileService.ts ×6
1195 >
1196 > // When a correlation identifier is set, return a specific
1197 > // watcher that only emits events matching that correalation.
1198 > const correlationId = options.correlationId;
1199 > if (typeof correlationId === 'number') {
1200 > const fileChangeEmitter = disposables.add(new Emitter<FileChangesEvent>()); fileService.ts ×3
1201 > disposables.add(this.internalOnDidFilesChange.event(e => {
1202 > if (e.correlates(correlationId)) { fileService.ts ×1
1203 fileChangeEmitter.fire(e);
1204 }
1205 > })); fileService.ts ×3
1206 >
1207 > const watcher: IFileSystemWatcher = {
1208 > onDidChange: fileChangeEmitter.event,
1209 > dispose: () => disposables.dispose()
1210 > };
1211 >
1212 > return watcher;
1213 > }
1215 > return disposables;
1218 > private async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {
1219 > const provider = await this.withProvider(resource); fileService.ts ×6
1221 > // Deduplicate identical watch requests
1222 > const watchHash = hash([this.getExtUri(provider).providerExtUri.getComparisonKey(resource), options]);
1223 > let watcher = this.activeWatchers.get(watchHash);
1224 > if (!watcher) {
1225 > watcher = {
1226 > count: 0,
1227 > disposable: provider.watch(resource, options)
1228 > };
1229 >
1230 > this.activeWatchers.set(watchHash, watcher);
1231 > }
1232 >
1233 > // Increment usage counter
1234 > watcher.count += 1;
1235 >
1236 > return toDisposable(() => {
1237 > if (watcher) {
1238 >
1239 > // Unref
1240 > watcher.count--;
1241 >
1242 > // Dispose only when last user is reached
1243 > if (watcher.count === 0) {
1244 > dispose(watcher.disposable);
1245 > this.activeWatchers.delete(watchHash);
1246 > }
1247 > }
1248 > });
1251 > override dispose(): void {
1252 > super.dispose(); fileService.ts ×2
1253 >
1254 > for (const [, watcher] of this.activeWatchers) {
1255 > dispose(watcher.disposable); fileService.ts ×1
1256 > }
1258 > this.activeWatchers.clear();
1259 > }
1261 > //#endregion
1262 >
1263 > //#region Helpers
1264 >
1265 > private readonly writeQueue = this._register(new ResourceQueue());
1267 > private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1268 return this.writeQueue.queueFor(resource, async () => {
1269
1270 // open handle
1271 const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false, append: options?.append ?? false });
1272
1273 // write into handle until all bytes from buffer have been written
1274 try {
1275 if (isReadableStream(readableOrStreamOrBufferedStream) || isReadableBufferedStream(readableOrStreamOrBufferedStream)) {
1276 await this.doWriteStreamBufferedQueued(provider, handle, readableOrStreamOrBufferedStream);
1277 } else {
1278 await this.doWriteReadableBufferedQueued(provider, handle, readableOrStreamOrBufferedStream);
1279 }
1280 } catch (error) {
1281 throw ensureFileSystemProviderError(error);
1282 } finally {
1283
1284 // close handle always
1285 await provider.close(handle);
1286 }
1287 }, this.getExtUri(provider).providerExtUri);
1288 }
1290 > private async doWriteStreamBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, streamOrBufferedStream: VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1291 let posInFile = 0;
1292 let stream: VSBufferReadableStream;
1293
1294 // Buffered stream: consume the buffer first by writing
1295 // it to the target before reading from the stream.
1296 if (isReadableBufferedStream(streamOrBufferedStream)) {
1297 if (streamOrBufferedStream.buffer.length > 0) {
1298 const chunk = VSBuffer.concat(streamOrBufferedStream.buffer);
1299 await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
1300
1301 posInFile += chunk.byteLength;
1302 }
1303
1304 // If the stream has been consumed, return early
1305 if (streamOrBufferedStream.ended) {
1306 return;
1307 }
1308
1309 stream = streamOrBufferedStream.stream;
1310 }
1311
1312 // Unbuffered stream - just take as is
1313 else {
1314 stream = streamOrBufferedStream;
1315 }
1316
1317 return new Promise((resolve, reject) => {
1318 listenStream(stream, {
1319 onData: async chunk => {
1320
1321 // pause stream to perform async write operation
1322 stream.pause();
1323
1324 try {
1325 await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
1326 } catch (error) {
1327 return reject(error);
1328 }
1329
1330 posInFile += chunk.byteLength;
1331
1332 // resume stream now that we have successfully written
1333 // run this on the next tick to prevent increasing the
1334 // execution stack because resume() may call the event
1335 // handler again before finishing.
1336 setTimeout(() => stream.resume());
1337 },
1338 onError: error => reject(error),
1339 onEnd: () => resolve()
1340 });
1341 });
1342 }
1344 > private async doWriteReadableBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, readable: VSBufferReadable): Promise<void> {
1345 let posInFile = 0;
1346
1347 let chunk: VSBuffer | null;
1348 while ((chunk = readable.read()) !== null) {
1349 await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
1350
1351 posInFile += chunk.byteLength;
1352 }
1353 }
1355 > private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> {
1356 > let totalBytesWritten = 0; fileService.ts ×15
1357 > while (totalBytesWritten < length) {
1358 >
1359 > // Write through the provider
1360 > const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten);
1361 > totalBytesWritten += bytesWritten;
1362 > }
1363 > }
1365 > private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1366 > return this.writeQueue.queueFor(resource, () => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream), this.getExtUri(provider).providerExtUri); fileService.ts ×17
1367 > }
1369 > private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1370 > let buffer: VSBuffer; fileService.ts ×17
1371 > if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
1372 > buffer = bufferOrReadableOrStreamOrBufferedStream;
1373 > } else if (isReadableStream(bufferOrReadableOrStreamOrBufferedStream)) {
1374 buffer = await streamToBuffer(bufferOrReadableOrStreamOrBufferedStream);
1375 } else if (isReadableBufferedStream(bufferOrReadableOrStreamOrBufferedStream)) {
1376 buffer = await bufferedStreamToBuffer(bufferOrReadableOrStreamOrBufferedStream);
1377 } else {
1378 buffer = readableToBuffer(bufferOrReadableOrStreamOrBufferedStream);
1379 }
1381 > // Write through the provider
1382 > await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false, atomic: options?.atomic ?? false, append: options?.append ?? false });
1383 > }
1385 > private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1386 > return this.writeQueue.queueFor(target, () => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri); fileService.ts ×15
1387 > }
1389 > private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1390 > let sourceHandle: number | undefined = undefined; fileService.ts ×15
1391 > let targetHandle: number | undefined = undefined;
1392 >
1393 > try {
1394 >
1395 > // Open handles
1396 > sourceHandle = await sourceProvider.open(source, { create: false });
1397 > targetHandle = await targetProvider.open(target, { create: true, unlock: false });
1398 >
1399 > const buffer = VSBuffer.alloc(this.BUFFER_SIZE);
1400 >
1401 > let posInFile = 0;
1402 > let posInBuffer = 0;
1403 > let bytesRead = 0;
1404 > do {
1405 > // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at
1406 > // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).
1407 > bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);
1408 >
1409 > // write into target (targetHandle) at current position (posInFile) from buffer (buffer) at
1410 > // buffer position (posInBuffer) all bytes we read (bytesRead).
1411 > await this.doWriteBuffer(targetProvider, targetHandle, buffer, bytesRead, posInFile, posInBuffer);
1412 >
1413 > posInFile += bytesRead;
1414 > posInBuffer += bytesRead;
1415 >
1416 > // when buffer full, fill it again from the beginning
1417 > if (posInBuffer === buffer.byteLength) {
1418 posInBuffer = 0;
1419 }
1420 > } while (bytesRead > 0); fileService.ts ×15
1421 > } catch (error) {
1422 throw ensureFileSystemProviderError(error);
1423 > } finally { fileService.ts ×15
1424 > await Promises.settled([
1425 > typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(),
1426 > typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(),
1427 > ]);
1428 > }
1429 > }
1431 > private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1432 return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);
1433 }
1435 > private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1436 return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false, atomic: false });
1437 }
1439 > private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1440 return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);
1441 }
1443 > private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1444
1445 // Open handle
1446 const targetHandle = await targetProvider.open(target, { create: true, unlock: false });
1447
1448 // Read entire buffer from source and write buffered
1449 try {
1450 const buffer = await sourceProvider.readFile(source);
1451 await this.doWriteBuffer(targetProvider, targetHandle, VSBuffer.wrap(buffer), buffer.byteLength, 0, 0);
1452 } catch (error) {
1453 throw ensureFileSystemProviderError(error);
1454 } finally {
1455 await targetProvider.close(targetHandle);
1456 }
1457 }
1459 > private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1460
1461 // Read buffer via stream buffered
1462 const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));
1463
1464 // Write buffer into target at once
1465 await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);
1466 }
1468 > protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {
1469 > if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { fileService.ts ×2
1470 throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
1471 }
1473 > return provider;
1474 > }
1476 > private throwIfFileIsReadonly(resource: URI, stat: IStat): void {
1477 > if ((stat.permissions ?? 0) & FilePermission.Readonly) { fileService.ts ×2
1478 throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
1479 }
1482 > private resourceForError(resource: URI): string {
1483 > if (resource.scheme === Schemas.file) { fileService.ts ×2
1484 > return resource.fsPath; fileService.ts ×1
1485 > }
1487 > return resource.toString(true);
1490 > //#endregion
1491 > }