Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/files/common/fileService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
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, 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();
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)) {
54
throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`);
55
}
56
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));
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
}));
77
if (typeof provider.onDidWatchError === 'function') {
78
providerDisposables.add(provider.onDidWatchError(error => this._onDidWatchError.fire(new Error(error))));
79
}
80
providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme })));
81
82
return toDisposable(() => {
83
this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider });
84
this.provider.delete(scheme);
85
86
dispose(providerDisposables);
87
});
88
}
89
90
getProvider(scheme: string): IFileSystemProvider | undefined {
91
return this.provider.get(scheme);
92
}
93
94
async activateProvider(scheme: string): Promise<void> {
95
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
});
105
106
if (this.provider.has(scheme)) {
107
return; // provider is already here so we can return directly
108
}
109
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);
113
}
114
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
}
122
123
hasProvider(resource: URI): boolean {
124
return this.provider.has(resource.scheme);
125
}
126
127
hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean {
128
const provider = this.provider.get(resource.scheme);
129
130
return !!(provider && (provider.capabilities & capability));
131
}
132
133
listCapabilities(): Iterable<{ scheme: string; capabilities: FileSystemProviderCapabilities }> {
134
return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities }));
135
}
136
137
protected async withProvider(resource: URI): Promise<IFileSystemProvider> {
138
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
}
143
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();
151
error.message = localize('noProviderFound', "ENOPRO: No file system provider found for resource '{0}'", resource.toString());
152
153
throw error;
154
}
155
156
return provider;
157
}
158
159
private async withReadProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability> {
160
const provider = await this.withProvider(resource);
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.`);
167
}
168
169
private async withWriteProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability> {
170
const provider = await this.withProvider(resource);
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.`);
177
}
178
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 {
194
return await this.doResolveFile(resource, options);
195
} catch (error) {
196
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);
200
}
201
202
// Bubble up any other error as is
203
throw ensureFileSystemProviderError(error);
204
}
205
}
206
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);
211
const isPathCaseSensitive = this.isPathCaseSensitive(provider);
212
213
const resolveTo = options?.resolveTo;
214
const resolveSingleChildDescendants = options?.resolveSingleChildDescendants;
215
const resolveMetadata = options?.resolveMetadata;
216
217
const stat = await provider.stat(resource);
218
219
let trie: TernarySearchTree<URI, boolean> | undefined;
220
221
return this.toFileStat(provider, resource, stat, undefined, !!resolveMetadata, (stat, siblings) => {
222
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
}
230
}
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
}
236
237
// check for resolving single child folders
238
if (stat.isDirectory && resolveSingleChildDescendants) {
239
return siblings === 1;
240
}
241
242
return false;
243
});
244
}
245
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);
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
etag: etag({ mtime: stat.mtime, size: stat.size }),
264
children: undefined
265
};
266
267
// check to recurse for directories
268
if (fileStat.isDirectory && recurse(fileStat, siblings)) {
269
try {
270
const entries = await provider.readdir(resource);
271
const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {
272
try {
273
const childResource = providerExtUri.joinPath(resource, name);
274
const childStat = resolveMetadata ? await provider.stat(childResource) : { type };
275
276
return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse);
277
} catch (error) {
278
this.logService.trace(error);
279
280
return null; // can happen e.g. due to permission errors
281
}
282
}));
283
284
// make sure to get rid of null values that signal a failure to resolve a particular entry
285
fileStat.children = coalesce(resolvedEntries);
286
} catch (error) {
287
this.logService.trace(error);
288
289
fileStat.children = []; // gracefully handle errors, we may not have permissions to read
290
}
291
292
return fileStat;
293
}
294
295
return fileStat;
296
}
297
298
async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]>;
299
async resolveAll(toResolve: { resource: URI; options: IResolveMetadataFileOptions }[]): Promise<IFileStatResultWithMetadata[]>;
300
async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]> {
301
return Promises.settled(toResolve.map(async entry => {
302
try {
303
return { stat: await this.doResolveFile(entry.resource, entry.options), success: true };
304
} catch (error) {
305
this.logService.trace(error);
306
307
return { stat: undefined, success: false };
308
}
309
}));
310
}
311
312
async stat(resource: URI): Promise<IFileStatWithPartialMetadata> {
313
const provider = await this.withProvider(resource);
314
315
const stat = await provider.stat(resource);
316
317
return this.toFileStat(provider, resource, stat, undefined, true, () => false /* Do not resolve any children */);
318
}
319
320
async realpath(resource: URI): Promise<URI | undefined> {
321
const provider = await this.withProvider(resource);
322
323
if (hasFileRealpathCapability(provider)) {
324
const realpath = await provider.realpath(resource);
325
326
return resource.with({ path: realpath });
327
}
328
329
return undefined;
330
}
331
332
async exists(resource: URI): Promise<boolean> {
333
const provider = await this.withProvider(resource);
334
335
try {
336
const stat = await provider.stat(resource);
337
338
return !!stat;
339
} catch (error) {
340
return false;
341
}
342
}
343
344
//#endregion
345
346
//#region File Reading/Writing
347
348
async canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true> {
349
try {
350
await this.doValidateCreateFile(resource, options);
351
} catch (error) {
352
return error;
353
}
354
355
return true;
356
}
357
358
private async doValidateCreateFile(resource: URI, options?: ICreateFileOptions): Promise<void> {
359
360
// validate overwrite
361
if (!options?.overwrite && await this.exists(resource)) {
362
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);
363
}
364
}
365
366
async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
367
368
// validate
369
await this.doValidateCreateFile(resource, options);
370
371
// do write into file (this will create it too)
372
const fileStat = await this.writeFile(resource, bufferOrReadableOrStream);
373
374
// events
375
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
376
377
return fileStat;
378
}
379
380
async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
381
const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource);
382
const { providerExtUri } = this.getExtUri(provider);
383
384
let writeFileOptions = options;
385
if (hasFileAtomicWriteCapability(provider) && !writeFileOptions?.atomic) {
386
const enforcedAtomicWrite = provider.enforceAtomicWriteFile?.(resource);
387
if (enforcedAtomicWrite) {
388
writeFileOptions = { ...options, atomic: enforcedAtomicWrite };
389
}
390
}
391
392
try {
393
394
// validate write (this may already return a peeked-at buffer)
395
let { stat, buffer: bufferOrReadableOrStreamOrBufferedStream } = await this.validateWriteFile(provider, resource, bufferOrReadableOrStream, writeFileOptions);
396
397
// mkdir recursively as needed
398
if (!stat) {
399
await this.mkdirp(provider, providerExtUri.dirname(resource));
400
}
401
402
// optimization: if the provider has unbuffered write capability and the data
403
// to write is not a buffer, we consume up to 3 chunks and try to write the data
404
// unbuffered to reduce the overhead. If the stream or readable has more data
405
// to provide we continue to write buffered.
406
if (!bufferOrReadableOrStreamOrBufferedStream) {
407
bufferOrReadableOrStreamOrBufferedStream = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);
408
}
409
410
// write file: unbuffered
411
if (
412
!hasOpenReadWriteCloseCapability(provider) || // buffered writing is unsupported
413
(hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) || // data is a full buffer already
414
(hasReadWriteCapability(provider) && hasFileAtomicWriteCapability(provider) && writeFileOptions?.atomic) // atomic write forces unbuffered write if the provider supports it
415
) {
416
await this.doWriteUnbuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream);
417
}
418
419
// write file: buffered
420
else {
421
await this.doWriteBuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
422
}
423
424
// events
425
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.WRITE));
426
} catch (error) {
427
throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), writeFileOptions);
428
}
429
430
return this.resolve(resource, { resolveMetadata: true });
431
}
432
433
434
private async peekBufferForWriting(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream> {
435
let peekResult: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream;
436
if (hasReadWriteCapability(provider) && !(bufferOrReadableOrStream instanceof VSBuffer)) {
437
if (isReadableStream(bufferOrReadableOrStream)) {
438
const bufferedStream = await peekStream(bufferOrReadableOrStream, 3);
439
if (bufferedStream.ended) {
440
peekResult = VSBuffer.concat(bufferedStream.buffer);
441
} else {
442
peekResult = bufferedStream;
443
}
444
} else {
445
peekResult = peekReadable(bufferOrReadableOrStream, data => VSBuffer.concat(data), 3);
446
}
447
} else {
448
peekResult = bufferOrReadableOrStream;
449
}
450
451
return peekResult;
452
}
453
454
private async validateWriteFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<{ stat: IStat | undefined; buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined }> {
455
456
// Validate unlock support
457
const unlock = !!options?.unlock;
458
if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {
459
throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
460
}
461
462
// Validate atomic support
463
const atomic = !!options?.atomic;
464
if (atomic) {
465
if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) {
466
throw new Error(localize('writeFailedAtomicUnsupported1', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource)));
467
}
468
469
if (!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite)) {
470
throw new Error(localize('writeFailedAtomicUnsupported2', "Unable to atomically write file '{0}' because provider does not support unbuffered writes.", this.resourceForError(resource)));
471
}
472
473
if (unlock) {
474
throw new Error(localize('writeFailedAtomicUnlock', "Unable to unlock file '{0}' because atomic write is enabled.", this.resourceForError(resource)));
475
}
476
}
477
478
// Validate via file stat meta data
479
let stat: IStat | undefined = undefined;
480
try {
481
stat = await provider.stat(resource);
482
} catch (error) {
483
return Object.create(null); // file might not exist
484
}
485
486
// File cannot be directory
487
if ((stat.type & FileType.Directory) !== 0) {
488
throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
489
}
490
491
// File cannot be readonly
492
this.throwIfFileIsReadonly(resource, stat);
493
494
// Dirty write prevention: if the file on disk has been changed and does not match our expected
495
// mtime and etag, we bail out to prevent dirty writing.
496
//
497
// First, we check for a mtime that is in the future before we do more checks. The assumption is
498
// that only the mtime is an indicator for a file that has changed on disk.
499
//
500
// Second, if the mtime has advanced, we compare the size of the file on disk with our previous
501
// one using the etag() function. Relying only on the mtime check has prooven to produce false
502
// positives due to file system weirdness (especially around remote file systems). As such, the
503
// check for size is a weaker check because it can return a false negative if the file has changed
504
// but to the same length. This is a compromise we take to avoid having to produce checksums of
505
// the file content for comparison which would be much slower to compute.
506
//
507
// Third, if the etag() turns out to be different, we do one attempt to compare the buffer we
508
// are about to write with the contents on disk to figure out if the contents are identical.
509
// In that case we allow the writing as it would result in the same contents in the file.
510
let buffer: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream | undefined;
511
if (
512
typeof options?.mtime === 'number' && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED &&
513
typeof stat.mtime === 'number' && typeof stat.size === 'number' &&
514
options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size })
515
) {
516
buffer = await this.peekBufferForWriting(provider, bufferOrReadableOrStream);
517
if (buffer instanceof VSBuffer && buffer.byteLength === stat.size) {
518
try {
519
const { value } = await this.readFile(resource, { limits: { size: stat.size } });
520
if (buffer.equals(value)) {
521
return { stat, buffer }; // allow writing since contents are identical
522
}
523
} catch (error) {
524
// ignore, throw the FILE_MODIFIED_SINCE error
525
}
526
}
527
528
throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options);
529
}
530
531
return { stat, buffer };
532
}
533
534
async readFile(resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
535
const provider = await this.withReadProvider(resource);
536
537
if (options?.atomic) {
538
return this.doReadFileAtomic(provider, resource, options, token);
539
}
540
541
return this.doReadFile(provider, resource, options, token);
542
}
543
544
private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
545
return new Promise<IFileContent>((resolve, reject) => {
546
this.writeQueue.queueFor(resource, async () => {
547
try {
548
const content = await this.doReadFile(provider, resource, options, token);
549
resolve(content);
550
} catch (error) {
551
reject(error);
552
}
553
}, this.getExtUri(provider).providerExtUri);
554
});
555
}
556
557
private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
558
const stream = await this.doReadFileStream(provider, resource, {
559
...options,
560
// optimization: since we know that the caller does not
561
// care about buffering, we indicate this to the reader.
562
// this reduces all the overhead the buffered reading
563
// has (open, read, close) if the provider supports
564
// unbuffered reading.
565
preferUnbuffered: true
566
}, token);
567
568
return {
569
...stream,
570
value: await streamToBuffer(stream.value)
571
};
572
}
573
574
async readFileStream(resource: URI, options?: IReadFileStreamOptions, token?: CancellationToken): Promise<IFileStreamContent> {
575
const provider = await this.withReadProvider(resource);
576
577
return this.doReadFileStream(provider, resource, options, token);
578
}
579
580
private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions & { preferUnbuffered?: boolean }, token?: CancellationToken): Promise<IFileStreamContent> {
581
582
// install a cancellation token that gets cancelled
583
// when any error occurs. this allows us to resolve
584
// the content of the file while resolving metadata
585
// but still cancel the operation in certain cases.
586
//
587
// in addition, we pass the optional token in that
588
// we got from the outside to even allow for external
589
// cancellation of the read operation.
590
const cancellableSource = new CancellationTokenSource(token);
591
592
let readFileOptions = options;
593
if (hasFileAtomicReadCapability(provider) && provider.enforceAtomicReadFile?.(resource)) {
594
readFileOptions = { ...options, atomic: true };
595
}
596
597
// validate read operation
598
const statPromise = this.validateReadFile(resource, readFileOptions).then(stat => stat, error => {
599
cancellableSource.dispose(true);
600
601
throw error;
602
});
603
604
let fileStream: VSBufferReadableStream | undefined = undefined;
605
try {
606
607
// if the etag is provided, we await the result of the validation
608
// due to the likelihood of hitting a NOT_MODIFIED_SINCE result.
609
// otherwise, we let it run in parallel to the file reading for
610
// optimal startup performance.
611
if (typeof readFileOptions?.etag === 'string' && readFileOptions.etag !== ETAG_DISABLED) {
612
await statPromise;
613
}
614
615
// read unbuffered
616
if (
617
(readFileOptions?.atomic && hasFileAtomicReadCapability(provider)) || // atomic reads are always unbuffered
618
!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || // provider has no buffered capability
619
(hasReadWriteCapability(provider) && readFileOptions?.preferUnbuffered) // unbuffered read is preferred
620
) {
621
fileStream = this.readFileUnbuffered(provider, resource, readFileOptions);
622
}
623
624
// read streamed (always prefer over primitive buffered read)
625
else if (hasFileReadStreamCapability(provider)) {
626
fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, readFileOptions);
627
}
628
629
// read buffered
630
else {
631
fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, readFileOptions);
632
}
633
634
fileStream.on('end', () => cancellableSource.dispose());
635
fileStream.on('error', () => cancellableSource.dispose());
636
637
const fileStat = await statPromise;
638
639
return {
640
...fileStat,
641
value: fileStream
642
};
643
} catch (error) {
644
645
// Await the stream to finish so that we exit this method
646
// in a consistent state with file handles closed
647
// (https://github.com/microsoft/vscode/issues/114024)
648
if (fileStream) {
649
await consumeStream(fileStream);
650
}
651
652
// Re-throw errors as file operation errors but preserve
653
// specific errors (such as not modified since)
654
throw this.restoreReadError(error, resource, readFileOptions);
655
}
656
}
657
658
private restoreReadError(error: Error, resource: URI, options?: IReadFileStreamOptions): FileOperationError {
659
const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString());
660
661
if (error instanceof NotModifiedSinceFileOperationError) {
662
return new NotModifiedSinceFileOperationError(message, error.stat, options);
663
}
664
665
if (error instanceof TooLargeFileOperationError) {
666
return new TooLargeFileOperationError(message, error.fileOperationResult, error.size, error.options as IReadFileOptions);
667
}
668
669
return new FileOperationError(message, toFileOperationResult(error), options);
670
}
671
672
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
673
const fileStream = provider.readFileStream(resource, options, token);
674
675
return transform(fileStream, {
676
data: data => data instanceof VSBuffer ? data : VSBuffer.wrap(data),
677
error: error => this.restoreReadError(error, resource, options)
678
}, data => VSBuffer.concat(data));
679
}
680
681
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
682
const stream = newWriteableBufferStream();
683
684
readFileIntoStream(provider, resource, stream, data => data, {
685
...options,
686
bufferSize: this.BUFFER_SIZE,
687
errorTransformer: error => this.restoreReadError(error, resource, options)
688
}, token);
689
690
return stream;
691
}
692
693
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithFileAtomicReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions): VSBufferReadableStream {
694
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
695
696
// Read the file into the stream async but do not wait for
697
// this to complete because streams work via events
698
(async () => {
699
try {
700
let buffer: Uint8Array;
701
if (options?.atomic && hasFileAtomicReadCapability(provider)) {
702
buffer = await provider.readFile(resource, { atomic: true });
703
} else {
704
buffer = await provider.readFile(resource);
705
}
706
707
// respect position option
708
if (typeof options?.position === 'number') {
709
buffer = buffer.slice(options.position);
710
}
711
712
// respect length option
713
if (typeof options?.length === 'number') {
714
buffer = buffer.slice(0, options.length);
715
}
716
717
// Throw if file is too large to load
718
this.validateReadFileLimits(resource, buffer.byteLength, options);
719
720
// End stream with data
721
stream.end(VSBuffer.wrap(buffer));
722
} catch (err) {
723
stream.error(err);
724
stream.end();
725
}
726
})();
727
728
return stream;
729
}
730
731
private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {
732
const stat = await this.resolve(resource, { resolveMetadata: true });
733
734
// Throw if resource is a directory
735
if (stat.isDirectory) {
736
throw new FileOperationError(localize('fileIsDirectoryReadError', "Unable to read file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
737
}
738
739
// Throw if file not modified since (unless disabled)
740
if (typeof options?.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) {
741
throw new NotModifiedSinceFileOperationError(localize('fileNotModifiedError', "File not modified since"), stat, options);
742
}
743
744
// Throw if file is too large to load
745
this.validateReadFileLimits(resource, stat.size, options);
746
747
return stat;
748
}
749
750
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {
751
if (typeof options?.limits?.size === 'number' && size > options.limits.size) {
752
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);
753
}
754
}
755
756
//#endregion
757
758
//#region Move/Copy/Delete/Create Folder
759
760
async canMove(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {
761
return this.doCanMoveCopy(source, target, 'move', overwrite);
762
}
763
764
async canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {
765
return this.doCanMoveCopy(source, target, 'copy', overwrite);
766
}
767
768
private async doCanMoveCopy(source: URI, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<Error | true> {
769
if (source.toString() !== target.toString()) {
770
try {
771
const sourceProvider = mode === 'move' ? this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source) : await this.withReadProvider(source);
772
const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
773
774
await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite);
775
} catch (error) {
776
return error;
777
}
778
}
779
780
return true;
781
}
782
783
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
784
const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source);
785
const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
786
787
// move
788
const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', !!overwrite);
789
790
// resolve and send events
791
const fileStat = await this.resolve(target, { resolveMetadata: true });
792
this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat));
793
794
return fileStat;
795
}
796
797
async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
798
const sourceProvider = await this.withReadProvider(source);
799
const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
800
801
// copy
802
const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', !!overwrite);
803
804
// resolve and send events
805
const fileStat = await this.resolve(target, { resolveMetadata: true });
806
this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat));
807
808
return fileStat;
809
}
810
811
private async doMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite: boolean): Promise<'move' | 'copy'> {
812
if (source.toString() === target.toString()) {
813
return mode; // simulate node.js behaviour here and do a no-op if paths match
814
}
815
816
// validation
817
const { exists, isSameResourceWithDifferentPathCase } = await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite);
818
819
// delete as needed (unless target is same resurce with different path case)
820
if (exists && !isSameResourceWithDifferentPathCase && overwrite) {
821
await this.del(target, { recursive: true });
822
}
823
824
// create parent folders
825
await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));
826
827
// copy source => target
828
if (mode === 'copy') {
829
830
// same provider with fast copy: leverage copy() functionality
831
if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {
832
await sourceProvider.copy(source, target, { overwrite });
833
}
834
835
// when copying via buffer/unbuffered, we have to manually
836
// traverse the source if it is a folder and not a file
837
else {
838
const sourceFile = await this.resolve(source);
839
if (sourceFile.isDirectory) {
840
await this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target);
841
} else {
842
await this.doCopyFile(sourceProvider, source, targetProvider, target);
843
}
844
}
845
846
return mode;
847
}
848
849
// move source => target
850
else {
851
852
// same provider: leverage rename() functionality
853
if (sourceProvider === targetProvider) {
854
await sourceProvider.rename(source, target, { overwrite });
855
856
return mode;
857
}
858
859
// across providers: copy to target & delete at source
860
else {
861
await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite);
862
await this.del(source, { recursive: true });
863
864
return 'copy';
865
}
866
}
867
}
868
869
private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI): Promise<void> {
870
871
// copy: source (buffered) => target (buffered)
872
if (hasOpenReadWriteCloseCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) {
873
return this.doPipeBuffered(sourceProvider, source, targetProvider, target);
874
}
875
876
// copy: source (buffered) => target (unbuffered)
877
if (hasOpenReadWriteCloseCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) {
878
return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target);
879
}
880
881
// copy: source (unbuffered) => target (buffered)
882
if (hasReadWriteCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) {
883
return this.doPipeUnbufferedToBuffered(sourceProvider, source, targetProvider, target);
884
}
885
886
// copy: source (unbuffered) => target (unbuffered)
887
if (hasReadWriteCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) {
888
return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target);
889
}
890
}
891
892
private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI): Promise<void> {
893
894
// create folder in target
895
await targetProvider.mkdir(targetFolder);
896
897
// create children in target
898
if (Array.isArray(sourceFolder.children)) {
899
await Promises.settled(sourceFolder.children.map(async sourceChild => {
900
const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name);
901
if (sourceChild.isDirectory) {
902
return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild);
903
} else {
904
return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild);
905
}
906
}));
907
}
908
}
909
910
private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean; isSameResourceWithDifferentPathCase: boolean }> {
911
let isSameResourceWithDifferentPathCase = false;
912
913
// Check if source is equal or parent to target (requires providers to be the same)
914
if (sourceProvider === targetProvider) {
915
const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider);
916
if (!isPathCaseSensitive) {
917
isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target);
918
}
919
920
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
921
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)));
922
}
923
924
if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) {
925
throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
926
}
927
}
928
929
// Extra checks if target exists and this is not a rename
930
const exists = await this.exists(target);
931
if (exists && !isSameResourceWithDifferentPathCase) {
932
933
// Bail out if target exists and we are not about to overwrite
934
if (!overwrite) {
935
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);
936
}
937
938
// Special case: if the target is a parent of the source, we cannot delete
939
// it as it would delete the source as well. In this case we have to throw
940
if (sourceProvider === targetProvider) {
941
const { providerExtUri } = this.getExtUri(sourceProvider);
942
if (providerExtUri.isEqualOrParent(source, target)) {
943
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)));
944
}
945
}
946
}
947
948
return { exists, isSameResourceWithDifferentPathCase };
949
}
950
951
private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri; isPathCaseSensitive: boolean } {
952
const isPathCaseSensitive = this.isPathCaseSensitive(provider);
953
954
return {
955
providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
956
isPathCaseSensitive
957
};
958
}
959
960
private isPathCaseSensitive(provider: IFileSystemProvider): boolean {
961
return !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
962
}
963
964
async createFolder(resource: URI): Promise<IFileStatWithMetadata> {
965
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
966
967
// mkdir recursively
968
await this.mkdirp(provider, resource);
969
970
// events
971
const fileStat = await this.resolve(resource, { resolveMetadata: true });
972
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
973
974
return fileStat;
975
}
976
977
private async mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {
978
const directoriesToCreate: string[] = [];
979
980
// mkdir until we reach root
981
const { providerExtUri } = this.getExtUri(provider);
982
while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {
983
try {
984
const stat = await provider.stat(directory);
985
if ((stat.type & FileType.Directory) === 0) {
986
throw new Error(localize('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", this.resourceForError(directory)));
987
}
988
989
break; // we have hit a directory that exists -> good
990
} catch (error) {
991
992
// Bubble up any other error that is not file not found
993
if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) {
994
throw error;
995
}
996
997
// Upon error, remember directories that need to be created
998
directoriesToCreate.push(providerExtUri.basename(directory));
999
1000
// Continue up
1001
directory = providerExtUri.dirname(directory);
1002
}
1003
}
1004
1005
// Create directories as needed
1006
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
1007
directory = providerExtUri.joinPath(directory, directoriesToCreate[i]);
1008
1009
try {
1010
await provider.mkdir(directory);
1011
} catch (error) {
1012
if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileExists) {
1013
// For mkdirp() we tolerate that the mkdir() call fails
1014
// in case the folder already exists. This follows node.js
1015
// own implementation of fs.mkdir({ recursive: true }) and
1016
// reduces the chances of race conditions leading to errors
1017
// if multiple calls try to create the same folders
1018
// As such, we only throw an error here if it is other than
1019
// the fact that the file already exists.
1020
// (see also https://github.com/microsoft/vscode/issues/89834)
1021
throw error;
1022
}
1023
}
1024
}
1025
}
1026
1027
async canDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<Error | true> {
1028
try {
1029
await this.doValidateDelete(resource, options);
1030
} catch (error) {
1031
return error;
1032
}
1033
1034
return true;
1035
}
1036
1037
private async doValidateDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<IFileSystemProvider> {
1038
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
1039
1040
// Validate trash support
1041
const useTrash = !!options?.useTrash;
1042
if (useTrash && !(provider.capabilities & FileSystemProviderCapabilities.Trash)) {
1043
throw new Error(localize('deleteFailedTrashUnsupported', "Unable to delete file '{0}' via trash because provider does not support it.", this.resourceForError(resource)));
1044
}
1045
1046
// Validate atomic support
1047
const atomic = options?.atomic;
1048
if (atomic && !(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete)) {
1049
throw new Error(localize('deleteFailedAtomicUnsupported', "Unable to delete file '{0}' atomically because provider does not support it.", this.resourceForError(resource)));
1050
}
1051
1052
if (useTrash && atomic) {
1053
throw new Error(localize('deleteFailedTrashAndAtomicUnsupported', "Unable to atomically delete file '{0}' because using trash is enabled.", this.resourceForError(resource)));
1054
}
1055
1056
// Validate delete
1057
let stat: IStat | undefined = undefined;
1058
try {
1059
stat = await provider.stat(resource);
1060
} catch (error) {
1061
// Handled later
1062
}
1063
1064
if (stat) {
1065
this.throwIfFileIsReadonly(resource, stat);
1066
} else {
1067
throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND);
1068
}
1069
1070
// Validate recursive
1071
const recursive = !!options?.recursive;
1072
if (!recursive) {
1073
const stat = await this.resolve(resource);
1074
if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) {
1075
throw new Error(localize('deleteFailedNonEmptyFolder', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource)));
1076
}
1077
}
1078
1079
return provider;
1080
}
1081
1082
async del(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<void> {
1083
const provider = await this.doValidateDelete(resource, options);
1084
1085
let deleteFileOptions = options;
1086
if (hasFileAtomicDeleteCapability(provider) && !deleteFileOptions?.atomic) {
1087
const enforcedAtomicDelete = provider.enforceAtomicDelete?.(resource);
1088
if (enforcedAtomicDelete) {
1089
deleteFileOptions = { ...options, atomic: enforcedAtomicDelete };
1090
}
1091
}
1092
1093
const useTrash = !!deleteFileOptions?.useTrash;
1094
const recursive = !!deleteFileOptions?.recursive;
1095
const atomic = deleteFileOptions?.atomic ?? false;
1096
1097
// Delete through provider
1098
await provider.delete(resource, { recursive, useTrash, atomic });
1099
1100
// Events
1101
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
1102
}
1103
1104
//#endregion
1105
1106
//#region Clone File
1107
1108
async cloneFile(source: URI, target: URI): Promise<void> {
1109
const sourceProvider = await this.withProvider(source);
1110
const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
1111
1112
if (sourceProvider === targetProvider && this.getExtUri(sourceProvider).providerExtUri.isEqual(source, target)) {
1113
return; // return early if paths are equal
1114
}
1115
1116
// same provider, use `cloneFile` when native support is provided
1117
if (sourceProvider === targetProvider && hasFileCloneCapability(sourceProvider)) {
1118
return sourceProvider.cloneFile(source, target);
1119
}
1120
1121
// otherwise, either providers are different or there is no native
1122
// `cloneFile` support, then we fallback to emulate a clone as best
1123
// as we can with the other primitives
1124
1125
// create parent folders
1126
await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));
1127
1128
// leverage `copy` method if provided and providers are identical
1129
// queue on the source to ensure atomic read
1130
if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {
1131
return this.writeQueue.queueFor(source, () => sourceProvider.copy(source, target, { overwrite: true }), this.getExtUri(sourceProvider).providerExtUri);
1132
}
1133
1134
// otherwise copy via buffer/unbuffered and use a write queue
1135
// on the source to ensure atomic operation as much as possible
1136
return this.writeQueue.queueFor(source, () => this.doCopyFile(sourceProvider, source, targetProvider, target), this.getExtUri(sourceProvider).providerExtUri);
1137
}
1138
1139
//#endregion
1140
1141
//#region File Watching
1142
1143
private readonly internalOnDidFilesChange = this._register(new Emitter<FileChangesEvent>());
1144
1145
private readonly _onDidUncorrelatedFilesChange = this._register(new Emitter<FileChangesEvent>());
1146
readonly onDidFilesChange = this._onDidUncorrelatedFilesChange.event; // global `onDidFilesChange` skips correlated events
1147
1148
private readonly _onDidWatchError = this._register(new Emitter<Error>());
1149
readonly onDidWatchError = this._onDidWatchError.event;
1150
1151
private readonly activeWatchers = new Map<number /* watch request hash */, { disposable: IDisposable; count: number }>();
1152
1153
private static WATCHER_CORRELATION_IDS = 0;
1154
1155
createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation & { recursive: false }): IFileSystemWatcher {
1156
return this.watch(resource, {
1157
...options,
1158
// Explicitly set a correlation id so that file events that originate
1159
// from requests from extensions are exclusively routed back to the
1160
// extension host and not into the workbench.
1161
correlationId: FileService.WATCHER_CORRELATION_IDS++
1162
});
1163
}
1164
1165
watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
1166
watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;
1167
watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IFileSystemWatcher | IDisposable {
1168
const disposables = new DisposableStore();
1169
1170
// Forward watch request to provider and wire in disposables
1171
let watchDisposed = false;
1172
let disposeWatch = () => { watchDisposed = true; };
1173
disposables.add(toDisposable(() => disposeWatch()));
1174
1175
// Watch and wire in disposable which is async but
1176
// check if we got disposed meanwhile and forward
1177
(async () => {
1178
try {
1179
const disposable = await this.doWatch(resource, options);
1180
if (watchDisposed) {
1181
dispose(disposable);
1182
} else {
1183
disposeWatch = () => dispose(disposable);
1184
}
1185
} catch (error) {
1186
this.logService.error(error);
1187
}
1188
})();
1189
1190
// When a correlation identifier is set, return a specific
1191
// watcher that only emits events matching that correalation.
1192
const correlationId = options.correlationId;
1193
if (typeof correlationId === 'number') {
1194
const fileChangeEmitter = disposables.add(new Emitter<FileChangesEvent>());
1195
disposables.add(this.internalOnDidFilesChange.event(e => {
1196
if (e.correlates(correlationId)) {
1197
fileChangeEmitter.fire(e);
1198
}
1199
}));
1200
1201
const watcher: IFileSystemWatcher = {
1202
onDidChange: fileChangeEmitter.event,
1203
dispose: () => disposables.dispose()
1204
};
1205
1206
return watcher;
1207
}
1208
1209
return disposables;
1210
}
1211
1212
private async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {
1213
const provider = await this.withProvider(resource);
1214
1215
// Deduplicate identical watch requests
1216
const watchHash = hash([this.getExtUri(provider).providerExtUri.getComparisonKey(resource), options]);
1217
let watcher = this.activeWatchers.get(watchHash);
1218
if (!watcher) {
1219
watcher = {
1220
count: 0,
1221
disposable: provider.watch(resource, options)
1222
};
1223
1224
this.activeWatchers.set(watchHash, watcher);
1225
}
1226
1227
// Increment usage counter
1228
watcher.count += 1;
1229
1230
return toDisposable(() => {
1231
if (watcher) {
1232
1233
// Unref
1234
watcher.count--;
1235
1236
// Dispose only when last user is reached
1237
if (watcher.count === 0) {
1238
dispose(watcher.disposable);
1239
this.activeWatchers.delete(watchHash);
1240
}
1241
}
1242
});
1243
}
1244
1245
override dispose(): void {
1246
super.dispose();
1247
1248
for (const [, watcher] of this.activeWatchers) {
1249
dispose(watcher.disposable);
1250
}
1251
1252
this.activeWatchers.clear();
1253
}
1254
1255
//#endregion
1256
1257
//#region Helpers
1258
1259
private readonly writeQueue = this._register(new ResourceQueue());
1260
1261
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1262
return this.writeQueue.queueFor(resource, async () => {
1263
1264
// open handle
1265
const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false });
1266
1267
// write into handle until all bytes from buffer have been written
1268
try {
1269
if (isReadableStream(readableOrStreamOrBufferedStream) || isReadableBufferedStream(readableOrStreamOrBufferedStream)) {
1270
await this.doWriteStreamBufferedQueued(provider, handle, readableOrStreamOrBufferedStream);
1271
} else {
1272
await this.doWriteReadableBufferedQueued(provider, handle, readableOrStreamOrBufferedStream);
1273
}
1274
} catch (error) {
1275
throw ensureFileSystemProviderError(error);
1276
} finally {
1277
1278
// close handle always
1279
await provider.close(handle);
1280
}
1281
}, this.getExtUri(provider).providerExtUri);
1282
}
1283
1284
private async doWriteStreamBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, streamOrBufferedStream: VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1285
let posInFile = 0;
1286
let stream: VSBufferReadableStream;
1287
1288
// Buffered stream: consume the buffer first by writing
1289
// it to the target before reading from the stream.
1290
if (isReadableBufferedStream(streamOrBufferedStream)) {
1291
if (streamOrBufferedStream.buffer.length > 0) {
1292
const chunk = VSBuffer.concat(streamOrBufferedStream.buffer);
1293
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
1294
1295
posInFile += chunk.byteLength;
1296
}
1297
1298
// If the stream has been consumed, return early
1299
if (streamOrBufferedStream.ended) {
1300
return;
1301
}
1302
1303
stream = streamOrBufferedStream.stream;
1304
}
1305
1306
// Unbuffered stream - just take as is
1307
else {
1308
stream = streamOrBufferedStream;
1309
}
1310
1311
return new Promise((resolve, reject) => {
1312
listenStream(stream, {
1313
onData: async chunk => {
1314
1315
// pause stream to perform async write operation
1316
stream.pause();
1317
1318
try {
1319
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
1320
} catch (error) {
1321
return reject(error);
1322
}
1323
1324
posInFile += chunk.byteLength;
1325
1326
// resume stream now that we have successfully written
1327
// run this on the next tick to prevent increasing the
1328
// execution stack because resume() may call the event
1329
// handler again before finishing.
1330
setTimeout(() => stream.resume());
1331
},
1332
onError: error => reject(error),
1333
onEnd: () => resolve()
1334
});
1335
});
1336
}
1337
1338
private async doWriteReadableBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, readable: VSBufferReadable): Promise<void> {
1339
let posInFile = 0;
1340
1341
let chunk: VSBuffer | null;
1342
while ((chunk = readable.read()) !== null) {
1343
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
1344
1345
posInFile += chunk.byteLength;
1346
}
1347
}
1348
1349
private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> {
1350
let totalBytesWritten = 0;
1351
while (totalBytesWritten < length) {
1352
1353
// Write through the provider
1354
const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten);
1355
totalBytesWritten += bytesWritten;
1356
}
1357
}
1358
1359
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1360
return this.writeQueue.queueFor(resource, () => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream), this.getExtUri(provider).providerExtUri);
1361
}
1362
1363
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1364
let buffer: VSBuffer;
1365
if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
1366
buffer = bufferOrReadableOrStreamOrBufferedStream;
1367
} else if (isReadableStream(bufferOrReadableOrStreamOrBufferedStream)) {
1368
buffer = await streamToBuffer(bufferOrReadableOrStreamOrBufferedStream);
1369
} else if (isReadableBufferedStream(bufferOrReadableOrStreamOrBufferedStream)) {
1370
buffer = await bufferedStreamToBuffer(bufferOrReadableOrStreamOrBufferedStream);
1371
} else {
1372
buffer = readableToBuffer(bufferOrReadableOrStreamOrBufferedStream);
1373
}
1374
1375
// Write through the provider
1376
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false, atomic: options?.atomic ?? false });
1377
}
1378
1379
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1380
return this.writeQueue.queueFor(target, () => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);
1381
}
1382
1383
private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1384
let sourceHandle: number | undefined = undefined;
1385
let targetHandle: number | undefined = undefined;
1386
1387
try {
1388
1389
// Open handles
1390
sourceHandle = await sourceProvider.open(source, { create: false });
1391
targetHandle = await targetProvider.open(target, { create: true, unlock: false });
1392
1393
const buffer = VSBuffer.alloc(this.BUFFER_SIZE);
1394
1395
let posInFile = 0;
1396
let posInBuffer = 0;
1397
let bytesRead = 0;
1398
do {
1399
// read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at
1400
// buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).
1401
bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);
1402
1403
// write into target (targetHandle) at current position (posInFile) from buffer (buffer) at
1404
// buffer position (posInBuffer) all bytes we read (bytesRead).
1405
await this.doWriteBuffer(targetProvider, targetHandle, buffer, bytesRead, posInFile, posInBuffer);
1406
1407
posInFile += bytesRead;
1408
posInBuffer += bytesRead;
1409
1410
// when buffer full, fill it again from the beginning
1411
if (posInBuffer === buffer.byteLength) {
1412
posInBuffer = 0;
1413
}
1414
} while (bytesRead > 0);
1415
} catch (error) {
1416
throw ensureFileSystemProviderError(error);
1417
} finally {
1418
await Promises.settled([
1419
typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(),
1420
typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(),
1421
]);
1422
}
1423
}
1424
1425
private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1426
return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);
1427
}
1428
1429
private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1430
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false, atomic: false });
1431
}
1432
1433
private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1434
return this.writeQueue.queueFor(target, () => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target), this.getExtUri(targetProvider).providerExtUri);
1435
}
1436
1437
private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1438
1439
// Open handle
1440
const targetHandle = await targetProvider.open(target, { create: true, unlock: false });
1441
1442
// Read entire buffer from source and write buffered
1443
try {
1444
const buffer = await sourceProvider.readFile(source);
1445
await this.doWriteBuffer(targetProvider, targetHandle, VSBuffer.wrap(buffer), buffer.byteLength, 0, 0);
1446
} catch (error) {
1447
throw ensureFileSystemProviderError(error);
1448
} finally {
1449
await targetProvider.close(targetHandle);
1450
}
1451
}
1452
1453
private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
1454
1455
// Read buffer via stream buffered
1456
const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));
1457
1458
// Write buffer into target at once
1459
await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);
1460
}
1461
1462
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {
1463
if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {
1464
throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
1465
}
1466
1467
return provider;
1468
}
1469
1470
private throwIfFileIsReadonly(resource: URI, stat: IStat): void {
1471
if ((stat.permissions ?? 0) & FilePermission.Readonly) {
1472
throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
1473
}
1474
}
1475
1476
private resourceForError(resource: URI): string {
1477
if (resource.scheme === Schemas.file) {
1478
return resource.fsPath;
1479
}
1480
1481
return resource.toString(true);
1482
}
1483
1484
//#endregion
1485
}
1486
1487