Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/files/common/fileService.ts
5240 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, 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();
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
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 {
271
const entries = await provider.readdir(resource);
272
const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {
273
try {
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
}));
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
}
292
293
return fileStat;
294
}
295
296
return fileStat;
297
}
298
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 => {
303
try {
304
return { stat: await this.doResolveFile(entry.resource, entry.options), success: true };
305
} catch (error) {
306
this.logService.trace(error);
307
308
return { stat: undefined, success: false };
309
}
310
}));
311
}
312
313
async stat(resource: URI): Promise<IFileStatWithPartialMetadata> {
314
const provider = await this.withProvider(resource);
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
}
320
321
async realpath(resource: URI): Promise<URI | undefined> {
322
const provider = await this.withProvider(resource);
323
324
if (hasFileRealpathCapability(provider)) {
325
const realpath = await provider.realpath(resource);
326
327
return resource.with({ path: realpath });
328
}
329
330
return undefined;
331
}
332
333
async exists(resource: URI): Promise<boolean> {
334
const provider = await this.withProvider(resource);
335
336
try {
337
const stat = await provider.stat(resource);
338
339
return !!stat;
340
} catch (error) {
341
return false;
342
}
343
}
344
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
}
358
359
private async doValidateCreateFile(resource: URI, options?: ICreateFileOptions): Promise<void> {
360
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
}
365
}
366
367
async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
368
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
}
380
381
async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
382
const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource);
383
const { providerExtUri } = this.getExtUri(provider);
384
385
let writeFileOptions = options;
386
if (hasFileAtomicWriteCapability(provider) && !writeFileOptions?.atomic) {
387
const enforcedAtomicWrite = provider.enforceAtomicWriteFile?.(resource);
388
if (enforcedAtomicWrite) {
389
writeFileOptions = { ...options, atomic: enforcedAtomicWrite };
390
}
391
}
392
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));
401
}
402
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
416
) {
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
}
424
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
}
430
431
return this.resolve(resource, { resolveMetadata: true });
432
}
433
434
435
private async peekBufferForWriting(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream> {
436
let peekResult: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream;
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 {
449
peekResult = bufferOrReadableOrStream;
450
}
451
452
return peekResult;
453
}
454
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 }> {
456
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
}
462
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
}
467
468
// Validate atomic support
469
const atomic = !!options?.atomic;
470
if (atomic) {
471
if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) {
472
throw new Error(localize('writeFailedAtomicUnsupported1', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource)));
473
}
474
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
}
478
479
if (unlock) {
480
throw new Error(localize('writeFailedAtomicUnlock', "Unable to unlock file '{0}' because atomic write is enabled.", this.resourceForError(resource)));
481
}
482
}
483
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
490
}
491
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
}
496
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 &&
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
}
536
537
return { stat, buffer };
538
}
539
540
async readFile(resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
541
const provider = await this.withReadProvider(resource);
542
543
if (options?.atomic) {
544
return this.doReadFileAtomic(provider, resource, options, token);
545
}
546
547
return this.doReadFile(provider, resource, options, token);
548
}
549
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
}
562
563
private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions, token?: CancellationToken): Promise<IFileContent> {
564
const stream = await this.doReadFileStream(provider, resource, {
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);
573
574
return {
575
...stream,
576
value: await streamToBuffer(stream.value)
577
};
578
}
579
580
async readFileStream(resource: URI, options?: IReadFileStreamOptions, token?: CancellationToken): Promise<IFileStreamContent> {
581
const provider = await this.withReadProvider(resource);
582
583
return this.doReadFileStream(provider, resource, options, token);
584
}
585
586
private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions & { preferUnbuffered?: boolean }, token?: CancellationToken): Promise<IFileStreamContent> {
587
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
}
602
603
// validate read operation
604
const statPromise = this.validateReadFile(resource, readFileOptions).then(stat => stat, error => {
605
cancellableSource.dispose(true);
606
607
throw error;
608
});
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
}
620
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);
628
}
629
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
}
639
640
fileStream.on('end', () => cancellableSource.dispose());
641
fileStream.on('error', () => cancellableSource.dispose());
642
643
const fileStat = await statPromise;
644
645
return {
646
...fileStat,
647
value: fileStream
648
};
649
} catch (error) {
650
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
}
662
}
663
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());
666
667
if (error instanceof NotModifiedSinceFileOperationError) {
668
return new NotModifiedSinceFileOperationError(message, error.stat, options);
669
}
670
671
if (error instanceof TooLargeFileOperationError) {
672
return new TooLargeFileOperationError(message, error.fileOperationResult, error.size, error.options as IReadFileOptions);
673
}
674
675
return new FileOperationError(message, toFileOperationResult(error), options);
676
}
677
678
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
679
const fileStream = provider.readFileStream(resource, options, token);
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
}
686
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
}
698
699
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithFileAtomicReadCapability, resource: URI, options?: IReadFileOptions & IReadFileStreamOptions): VSBufferReadableStream {
700
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
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 {
710
buffer = await provider.readFile(resource);
711
}
712
713
// respect position option
714
if (typeof options?.position === 'number') {
715
buffer = buffer.slice(options.position);
716
}
717
718
// respect length option
719
if (typeof options?.length === 'number') {
720
buffer = buffer.slice(0, options.length);
721
}
722
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) {
729
stream.error(err);
730
stream.end();
731
}
732
})();
733
734
return stream;
735
}
736
737
private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {
738
const stat = await this.resolve(resource, { resolveMetadata: true });
739
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
}
744
745
// Throw if file not modified since (unless disabled)
746
if (typeof options?.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) {
747
throw new NotModifiedSinceFileOperationError(localize('fileNotModifiedError', "File not modified since"), stat, options);
748
}
749
750
// Throw if file is too large to load
751
this.validateReadFileLimits(resource, stat.size, options);
752
753
return stat;
754
}
755
756
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {
757
if (typeof options?.limits?.size === 'number' && size > options.limits.size) {
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
}
760
}
761
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
}
769
770
async canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true> {
771
return this.doCanMoveCopy(source, target, 'copy', overwrite);
772
}
773
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
}
788
789
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
790
const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source);
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
}
802
803
async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
804
const sourceProvider = await this.withReadProvider(source);
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);
809
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));
813
814
return fileStat;
815
}
816
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()) {
819
return mode; // simulate node.js behaviour here and do a no-op if paths match
820
}
821
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
}
829
830
// create parent folders
831
await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));
832
833
// copy source => target
834
if (mode === 'copy') {
835
836
// same provider with fast copy: leverage copy() functionality
837
if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {
838
await sourceProvider.copy(source, target, { overwrite });
839
}
840
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) {
846
await this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target);
847
} else {
848
await this.doCopyFile(sourceProvider, source, targetProvider, target);
849
}
850
}
851
852
return mode;
853
}
854
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
}
872
}
873
}
874
875
private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI): Promise<void> {
876
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)) {
884
return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target);
885
}
886
887
// copy: source (unbuffered) => target (buffered)
888
if (hasReadWriteCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) {
889
return this.doPipeUnbufferedToBuffered(sourceProvider, source, targetProvider, target);
890
}
891
892
// copy: source (unbuffered) => target (unbuffered)
893
if (hasReadWriteCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) {
894
return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target);
895
}
896
}
897
898
private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI): Promise<void> {
899
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 {
910
return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild);
911
}
912
}));
913
}
914
}
915
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;
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);
922
if (!isPathCaseSensitive) {
923
isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target);
924
}
925
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
}
929
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
}
933
}
934
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
}
953
954
return { exists, isSameResourceWithDifferentPathCase };
955
}
956
957
private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri; isPathCaseSensitive: boolean } {
958
const isPathCaseSensitive = this.isPathCaseSensitive(provider);
959
960
return {
961
providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
962
isPathCaseSensitive
963
};
964
}
965
966
private isPathCaseSensitive(provider: IFileSystemProvider): boolean {
967
return !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
968
}
969
970
async createFolder(resource: URI): Promise<IFileStatWithMetadata> {
971
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
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
}
982
983
private async mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {
984
const directoriesToCreate: string[] = [];
985
986
// mkdir until we reach root
987
const { providerExtUri } = this.getExtUri(provider);
988
while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {
989
try {
990
const stat = await provider.stat(directory);
991
if ((stat.type & FileType.Directory) === 0) {
992
throw new Error(localize('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", this.resourceForError(directory)));
993
}
994
995
break; // we have hit a directory that exists -> good
996
} catch (error) {
997
998
// Bubble up any other error that is not file not found
999
if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) {
1000
throw error;
1001
}
1002
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
}
1010
1011
// Create directories as needed
1012
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
1013
directory = providerExtUri.joinPath(directory, directoriesToCreate[i]);
1014
1015
try {
1016
await provider.mkdir(directory);
1017
} catch (error) {
1018
if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileExists) {
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
}
1029
}
1030
}
1031
}
1032
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
}
1042
1043
private async doValidateDelete(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<IFileSystemProvider> {
1044
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
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
}
1051
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
}
1057
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
}
1061
1062
// Validate delete
1063
let stat: IStat | undefined = undefined;
1064
try {
1065
stat = await provider.stat(resource);
1066
} catch (error) {
1067
// Handled later
1068
}
1069
1070
if (stat) {
1071
this.throwIfFileIsReadonly(resource, stat);
1072
} else {
1073
throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND);
1074
}
1075
1076
// Validate recursive
1077
const recursive = !!options?.recursive;
1078
if (!recursive) {
1079
const stat = await this.resolve(resource);
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
}
1083
}
1084
1085
return provider;
1086
}
1087
1088
async del(resource: URI, options?: Partial<IFileDeleteOptions>): Promise<void> {
1089
const provider = await this.doValidateDelete(resource, options);
1090
1091
let deleteFileOptions = options;
1092
if (hasFileAtomicDeleteCapability(provider) && !deleteFileOptions?.atomic) {
1093
const enforcedAtomicDelete = provider.enforceAtomicDelete?.(resource);
1094
if (enforcedAtomicDelete) {
1095
deleteFileOptions = { ...options, atomic: enforcedAtomicDelete };
1096
}
1097
}
1098
1099
const useTrash = !!deleteFileOptions?.useTrash;
1100
const recursive = !!deleteFileOptions?.recursive;
1101
const atomic = deleteFileOptions?.atomic ?? false;
1102
1103
// Delete through provider
1104
await provider.delete(resource, { recursive, useTrash, atomic });
1105
1106
// Events
1107
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
1108
}
1109
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
}
1144
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, {
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
}
1170
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();
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) {
1187
dispose(disposable);
1188
} else {
1189
disposeWatch = () => dispose(disposable);
1190
}
1191
} catch (error) {
1192
this.logService.error(error);
1193
}
1194
})();
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>());
1201
disposables.add(this.internalOnDidFilesChange.event(e => {
1202
if (e.correlates(correlationId)) {
1203
fileChangeEmitter.fire(e);
1204
}
1205
}));
1206
1207
const watcher: IFileSystemWatcher = {
1208
onDidChange: fileChangeEmitter.event,
1209
dispose: () => disposables.dispose()
1210
};
1211
1212
return watcher;
1213
}
1214
1215
return disposables;
1216
}
1217
1218
private async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {
1219
const provider = await this.withProvider(resource);
1220
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
});
1249
}
1250
1251
override dispose(): void {
1252
super.dispose();
1253
1254
for (const [, watcher] of this.activeWatchers) {
1255
dispose(watcher.disposable);
1256
}
1257
1258
this.activeWatchers.clear();
1259
}
1260
1261
//#endregion
1262
1263
//#region Helpers
1264
1265
private readonly writeQueue = this._register(new ResourceQueue());
1266
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
}
1289
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
}
1343
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
}
1354
1355
private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> {
1356
let totalBytesWritten = 0;
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
}
1364
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);
1367
}
1368
1369
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
1370
let buffer: VSBuffer;
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
}
1380
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
}
1384
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);
1387
}
1388
1389
private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
1390
let sourceHandle: number | undefined = undefined;
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);
1421
} catch (error) {
1422
throw ensureFileSystemProviderError(error);
1423
} finally {
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
}
1430
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
}
1434
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
}
1438
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
}
1442
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
}
1458
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
}
1467
1468
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {
1469
if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {
1470
throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
1471
}
1472
1473
return provider;
1474
}
1475
1476
private throwIfFileIsReadonly(resource: URI, stat: IStat): void {
1477
if ((stat.permissions ?? 0) & FilePermission.Readonly) {
1478
throw new FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
1479
}
1480
}
1481
1482
private resourceForError(resource: URI): string {
1483
if (resource.scheme === Schemas.file) {
1484
return resource.fsPath;
1485
}
1486
1487
return resource.toString(true);
1488
}
1489
1490
//#endregion
1491
}
1492
1493