Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/files/node/diskFileSystemProvider.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 { Stats, promises } from 'fs';
7
import { Barrier, retry } from '../../../base/common/async.js';
8
import { ResourceMap } from '../../../base/common/map.js';
9
import { VSBuffer } from '../../../base/common/buffer.js';
10
import { CancellationToken } from '../../../base/common/cancellation.js';
11
import { Event } from '../../../base/common/event.js';
12
import { isEqual } from '../../../base/common/extpath.js';
13
import { DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
14
import { basename, dirname, join } from '../../../base/common/path.js';
15
import { isLinux, isWindows } from '../../../base/common/platform.js';
16
import { extUriBiasedIgnorePathCase, joinPath, basename as resourcesBasename, dirname as resourcesDirname } from '../../../base/common/resources.js';
17
import { newWriteableStream, ReadableStreamEvents } from '../../../base/common/stream.js';
18
import { URI } from '../../../base/common/uri.js';
19
import { IDirent, Promises, RimRafMode, SymlinkSupport } from '../../../base/node/pfs.js';
20
import { localize } from '../../../nls.js';
21
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileChange, IFileSystemProviderWithFileRealpathCapability } from '../common/files.js';
22
import { readFileIntoStream } from '../common/io.js';
23
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage } from '../common/watcher.js';
24
import { ILogService } from '../../log/common/log.js';
25
import { AbstractDiskFileSystemProvider, IDiskFileSystemProviderOptions } from '../common/diskFileSystemProvider.js';
26
import { UniversalWatcherClient } from './watcher/watcherClient.js';
27
import { NodeJSWatcherClient } from './watcher/nodejs/nodejsClient.js';
28
29
export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements
30
IFileSystemProviderWithFileReadWriteCapability,
31
IFileSystemProviderWithOpenReadWriteCloseCapability,
32
IFileSystemProviderWithFileReadStreamCapability,
33
IFileSystemProviderWithFileFolderCopyCapability,
34
IFileSystemProviderWithFileAtomicReadCapability,
35
IFileSystemProviderWithFileAtomicWriteCapability,
36
IFileSystemProviderWithFileAtomicDeleteCapability,
37
IFileSystemProviderWithFileCloneCapability,
38
IFileSystemProviderWithFileRealpathCapability {
39
40
private static TRACE_LOG_RESOURCE_LOCKS = false; // not enabled by default because very spammy
41
42
constructor(
43
logService: ILogService,
44
options?: IDiskFileSystemProviderOptions
45
) {
46
super(logService, options);
47
}
48
49
//#region File Capabilities
50
51
readonly onDidChangeCapabilities = Event.None;
52
53
private _capabilities: FileSystemProviderCapabilities | undefined;
54
get capabilities(): FileSystemProviderCapabilities {
55
if (!this._capabilities) {
56
this._capabilities =
57
FileSystemProviderCapabilities.FileReadWrite |
58
FileSystemProviderCapabilities.FileOpenReadWriteClose |
59
FileSystemProviderCapabilities.FileReadStream |
60
FileSystemProviderCapabilities.FileFolderCopy |
61
FileSystemProviderCapabilities.FileWriteUnlock |
62
FileSystemProviderCapabilities.FileAtomicRead |
63
FileSystemProviderCapabilities.FileAtomicWrite |
64
FileSystemProviderCapabilities.FileAtomicDelete |
65
FileSystemProviderCapabilities.FileClone |
66
FileSystemProviderCapabilities.FileRealpath;
67
68
if (isLinux) {
69
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
70
}
71
}
72
73
return this._capabilities;
74
}
75
76
//#endregion
77
78
//#region File Metadata Resolving
79
80
async stat(resource: URI): Promise<IStat> {
81
try {
82
const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
83
84
return {
85
type: this.toType(stat, symbolicLink),
86
ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time
87
mtime: stat.mtime.getTime(),
88
size: stat.size,
89
permissions: (stat.mode & 0o200) === 0 ? FilePermission.Locked : undefined
90
};
91
} catch (error) {
92
throw this.toFileSystemProviderError(error);
93
}
94
}
95
96
private async statIgnoreError(resource: URI): Promise<IStat | undefined> {
97
try {
98
return await this.stat(resource);
99
} catch (error) {
100
return undefined;
101
}
102
}
103
104
async realpath(resource: URI): Promise<string> {
105
const filePath = this.toFilePath(resource);
106
107
return Promises.realpath(filePath);
108
}
109
110
async readdir(resource: URI): Promise<[string, FileType][]> {
111
try {
112
const children = await Promises.readdir(this.toFilePath(resource), { withFileTypes: true });
113
114
const result: [string, FileType][] = [];
115
await Promise.all(children.map(async child => {
116
try {
117
let type: FileType;
118
if (child.isSymbolicLink()) {
119
type = (await this.stat(joinPath(resource, child.name))).type; // always resolve target the link points to if any
120
} else {
121
type = this.toType(child);
122
}
123
124
result.push([child.name, type]);
125
} catch (error) {
126
this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied
127
}
128
}));
129
130
return result;
131
} catch (error) {
132
throw this.toFileSystemProviderError(error);
133
}
134
}
135
136
private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType {
137
138
// Signal file type by checking for file / directory, except:
139
// - symbolic links pointing to nonexistent files are FileType.Unknown
140
// - files that are neither file nor directory are FileType.Unknown
141
let type: FileType;
142
if (symbolicLink?.dangling) {
143
type = FileType.Unknown;
144
} else if (entry.isFile()) {
145
type = FileType.File;
146
} else if (entry.isDirectory()) {
147
type = FileType.Directory;
148
} else {
149
type = FileType.Unknown;
150
}
151
152
// Always signal symbolic link as file type additionally
153
if (symbolicLink) {
154
type |= FileType.SymbolicLink;
155
}
156
157
return type;
158
}
159
160
//#endregion
161
162
//#region File Reading/Writing
163
164
private readonly resourceLocks = new ResourceMap<Barrier>(resource => extUriBiasedIgnorePathCase.getComparisonKey(resource));
165
166
private async createResourceLock(resource: URI): Promise<IDisposable> {
167
const filePath = this.toFilePath(resource);
168
this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - request to acquire resource lock (${filePath})`);
169
170
// Await pending locks for resource. It is possible for a new lock being
171
// added right after opening, so we have to loop over locks until no lock
172
// remains.
173
let existingLock: Barrier | undefined = undefined;
174
while (existingLock = this.resourceLocks.get(resource)) {
175
this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - waiting for resource lock to be released (${filePath})`);
176
await existingLock.wait();
177
}
178
179
// Store new
180
const newLock = new Barrier();
181
this.resourceLocks.set(resource, newLock);
182
183
this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - new resource lock created (${filePath})`);
184
185
return toDisposable(() => {
186
this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock dispose() (${filePath})`);
187
188
// Delete lock if it is still ours
189
if (this.resourceLocks.get(resource) === newLock) {
190
this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock removed from resource-lock map (${filePath})`);
191
this.resourceLocks.delete(resource);
192
}
193
194
// Open lock
195
this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock barrier open() (${filePath})`);
196
newLock.open();
197
});
198
}
199
200
async readFile(resource: URI, options?: IFileAtomicReadOptions): Promise<Uint8Array> {
201
let lock: IDisposable | undefined = undefined;
202
try {
203
if (options?.atomic) {
204
this.traceLock(`[Disk FileSystemProvider]: atomic read operation started (${this.toFilePath(resource)})`);
205
206
// When the read should be atomic, make sure
207
// to await any pending locks for the resource
208
// and lock for the duration of the read.
209
lock = await this.createResourceLock(resource);
210
}
211
212
const filePath = this.toFilePath(resource);
213
214
return await promises.readFile(filePath);
215
} catch (error) {
216
throw this.toFileSystemProviderError(error);
217
} finally {
218
lock?.dispose();
219
}
220
}
221
222
private traceLock(msg: string): void {
223
if (DiskFileSystemProvider.TRACE_LOG_RESOURCE_LOCKS) {
224
this.logService.trace(msg);
225
}
226
}
227
228
readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
229
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
230
231
readFileIntoStream(this, resource, stream, data => data.buffer, {
232
...opts,
233
bufferSize: 256 * 1024 // read into chunks of 256kb each to reduce IPC overhead
234
}, token);
235
236
return stream;
237
}
238
239
async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
240
if (opts?.atomic !== false && opts?.atomic?.postfix && await this.canWriteFileAtomic(resource)) {
241
return this.doWriteFileAtomic(resource, joinPath(resourcesDirname(resource), `${resourcesBasename(resource)}${opts.atomic.postfix}`), content, opts);
242
} else {
243
return this.doWriteFile(resource, content, opts);
244
}
245
}
246
247
private async canWriteFileAtomic(resource: URI): Promise<boolean> {
248
try {
249
const filePath = this.toFilePath(resource);
250
const { symbolicLink } = await SymlinkSupport.stat(filePath);
251
if (symbolicLink) {
252
// atomic writes are unsupported for symbolic links because
253
// we need to ensure that the `rename` operation is atomic
254
// and that only works if the link is on the same disk.
255
// Since we do not know where the symbolic link points to
256
// we refuse to write atomically.
257
return false;
258
}
259
} catch (error) {
260
// ignore stat errors here and just proceed trying to write
261
}
262
263
return true; // atomic writing supported
264
}
265
266
private async doWriteFileAtomic(resource: URI, tempResource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
267
268
// Ensure to create locks for all resources involved
269
// since atomic write involves mutiple disk operations
270
// and resources.
271
272
const locks = new DisposableStore();
273
274
try {
275
locks.add(await this.createResourceLock(resource));
276
locks.add(await this.createResourceLock(tempResource));
277
278
// Write to temp resource first
279
await this.doWriteFile(tempResource, content, opts, true /* disable write lock */);
280
281
try {
282
283
// Rename over existing to ensure atomic replace
284
await this.rename(tempResource, resource, { overwrite: true });
285
286
} catch (error) {
287
288
// Cleanup in case of rename error
289
try {
290
await this.delete(tempResource, { recursive: false, useTrash: false, atomic: false });
291
} catch (error) {
292
// ignore - we want the outer error to bubble up
293
}
294
295
throw error;
296
}
297
} finally {
298
locks.dispose();
299
}
300
}
301
302
private async doWriteFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions, disableWriteLock?: boolean): Promise<void> {
303
let handle: number | undefined = undefined;
304
try {
305
const filePath = this.toFilePath(resource);
306
307
// Validate target unless { create: true, overwrite: true }
308
if (!opts.create || !opts.overwrite) {
309
const fileExists = await Promises.exists(filePath);
310
if (fileExists) {
311
if (!opts.overwrite) {
312
throw createFileSystemProviderError(localize('fileExists', "File already exists"), FileSystemProviderErrorCode.FileExists);
313
}
314
} else {
315
if (!opts.create) {
316
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
317
}
318
}
319
}
320
321
// Open
322
handle = await this.open(resource, { create: true, unlock: opts.unlock }, disableWriteLock);
323
324
// Write content at once
325
await this.write(handle, 0, content, 0, content.byteLength);
326
} catch (error) {
327
throw await this.toFileSystemProviderWriteError(resource, error);
328
} finally {
329
if (typeof handle === 'number') {
330
await this.close(handle);
331
}
332
}
333
}
334
335
private readonly mapHandleToPos = new Map<number, number>();
336
private readonly mapHandleToLock = new Map<number, IDisposable>();
337
338
private readonly writeHandles = new Map<number, URI>();
339
340
private static canFlush: boolean = true;
341
342
static configureFlushOnWrite(enabled: boolean): void {
343
DiskFileSystemProvider.canFlush = enabled;
344
}
345
346
async open(resource: URI, opts: IFileOpenOptions, disableWriteLock?: boolean): Promise<number> {
347
const filePath = this.toFilePath(resource);
348
349
// Writes: guard multiple writes to the same resource
350
// behind a single lock to prevent races when writing
351
// from multiple places at the same time to the same file
352
let lock: IDisposable | undefined = undefined;
353
if (isFileOpenForWriteOptions(opts) && !disableWriteLock) {
354
lock = await this.createResourceLock(resource);
355
}
356
357
let fd: number | undefined = undefined;
358
try {
359
360
// Determine whether to unlock the file (write only)
361
if (isFileOpenForWriteOptions(opts) && opts.unlock) {
362
try {
363
const { stat } = await SymlinkSupport.stat(filePath);
364
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
365
await promises.chmod(filePath, stat.mode | 0o200);
366
}
367
} catch (error) {
368
if (error.code !== 'ENOENT') {
369
this.logService.trace(error); // log errors but do not give up writing
370
}
371
}
372
}
373
374
// Windows gets special treatment (write only)
375
if (isWindows && isFileOpenForWriteOptions(opts)) {
376
try {
377
378
// We try to use 'r+' for opening (which will fail if the file does not exist)
379
// to prevent issues when saving hidden files or preserving alternate data
380
// streams.
381
// Related issues:
382
// - https://github.com/microsoft/vscode/issues/931
383
// - https://github.com/microsoft/vscode/issues/6363
384
fd = await Promises.open(filePath, 'r+');
385
386
// The flag 'r+' will not truncate the file, so we have to do this manually
387
await Promises.ftruncate(fd, 0);
388
} catch (error) {
389
if (error.code !== 'ENOENT') {
390
this.logService.trace(error); // log errors but do not give up writing
391
}
392
393
// Make sure to close the file handle if we have one
394
if (typeof fd === 'number') {
395
try {
396
await Promises.close(fd);
397
} catch (error) {
398
this.logService.trace(error); // log errors but do not give up writing
399
}
400
401
// Reset `fd` to be able to try again with 'w'
402
fd = undefined;
403
}
404
}
405
}
406
407
if (typeof fd !== 'number') {
408
fd = await Promises.open(filePath, isFileOpenForWriteOptions(opts) ?
409
// We take `opts.create` as a hint that the file is opened for writing
410
// as such we use 'w' to truncate an existing or create the
411
// file otherwise. we do not allow reading.
412
'w' :
413
// Otherwise we assume the file is opened for reading
414
// as such we use 'r' to neither truncate, nor create
415
// the file.
416
'r'
417
);
418
}
419
420
} catch (error) {
421
422
// Release lock because we have no valid handle
423
// if we did open a lock during this operation
424
lock?.dispose();
425
426
// Rethrow as file system provider error
427
if (isFileOpenForWriteOptions(opts)) {
428
throw await this.toFileSystemProviderWriteError(resource, error);
429
} else {
430
throw this.toFileSystemProviderError(error);
431
}
432
}
433
434
// Remember this handle to track file position of the handle
435
// we init the position to 0 since the file descriptor was
436
// just created and the position was not moved so far (see
437
// also http://man7.org/linux/man-pages/man2/open.2.html -
438
// "The file offset is set to the beginning of the file.")
439
this.mapHandleToPos.set(fd, 0);
440
441
// remember that this handle was used for writing
442
if (isFileOpenForWriteOptions(opts)) {
443
this.writeHandles.set(fd, resource);
444
}
445
446
if (lock) {
447
const previousLock = this.mapHandleToLock.get(fd);
448
449
// Remember that this handle has an associated lock
450
this.traceLock(`[Disk FileSystemProvider]: open() - storing lock for handle ${fd} (${filePath})`);
451
this.mapHandleToLock.set(fd, lock);
452
453
// There is a slight chance that a resource lock for a
454
// handle was not yet disposed when we acquire a new
455
// lock, so we must ensure to dispose the previous lock
456
// before storing a new one for the same handle, other
457
// wise we end up in a deadlock situation
458
// https://github.com/microsoft/vscode/issues/142462
459
if (previousLock) {
460
this.traceLock(`[Disk FileSystemProvider]: open() - disposing a previous lock that was still stored on same handle ${fd} (${filePath})`);
461
previousLock.dispose();
462
}
463
}
464
465
return fd;
466
}
467
468
async close(fd: number): Promise<void> {
469
470
// It is very important that we keep any associated lock
471
// for the file handle before attempting to call `fs.close(fd)`
472
// because of a possible race condition: as soon as a file
473
// handle is released, the OS may assign the same handle to
474
// the next `fs.open` call and as such it is possible that our
475
// lock is getting overwritten
476
const lockForHandle = this.mapHandleToLock.get(fd);
477
478
try {
479
480
// Remove this handle from map of positions
481
this.mapHandleToPos.delete(fd);
482
483
// If a handle is closed that was used for writing, ensure
484
// to flush the contents to disk if possible.
485
if (this.writeHandles.delete(fd) && DiskFileSystemProvider.canFlush) {
486
try {
487
await Promises.fdatasync(fd); // https://github.com/microsoft/vscode/issues/9589
488
} catch (error) {
489
// In some exotic setups it is well possible that node fails to sync
490
// In that case we disable flushing and log the error to our logger
491
DiskFileSystemProvider.configureFlushOnWrite(false);
492
this.logService.error(error);
493
}
494
}
495
496
return await Promises.close(fd);
497
} catch (error) {
498
throw this.toFileSystemProviderError(error);
499
} finally {
500
if (lockForHandle) {
501
if (this.mapHandleToLock.get(fd) === lockForHandle) {
502
this.traceLock(`[Disk FileSystemProvider]: close() - resource lock removed from handle-lock map ${fd}`);
503
this.mapHandleToLock.delete(fd); // only delete from map if this is still our lock!
504
}
505
506
this.traceLock(`[Disk FileSystemProvider]: close() - disposing lock for handle ${fd}`);
507
lockForHandle.dispose();
508
}
509
}
510
}
511
512
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
513
const normalizedPos = this.normalizePos(fd, pos);
514
515
let bytesRead: number | null = null;
516
try {
517
bytesRead = (await Promises.read(fd, data, offset, length, normalizedPos)).bytesRead;
518
} catch (error) {
519
throw this.toFileSystemProviderError(error);
520
} finally {
521
this.updatePos(fd, normalizedPos, bytesRead);
522
}
523
524
return bytesRead;
525
}
526
527
private normalizePos(fd: number, pos: number): number | null {
528
529
// When calling fs.read/write we try to avoid passing in the "pos" argument and
530
// rather prefer to pass in "null" because this avoids an extra seek(pos)
531
// call that in some cases can even fail (e.g. when opening a file over FTP -
532
// see https://github.com/microsoft/vscode/issues/73884).
533
//
534
// as such, we compare the passed in position argument with our last known
535
// position for the file descriptor and use "null" if they match.
536
if (pos === this.mapHandleToPos.get(fd)) {
537
return null;
538
}
539
540
return pos;
541
}
542
543
private updatePos(fd: number, pos: number | null, bytesLength: number | null): void {
544
const lastKnownPos = this.mapHandleToPos.get(fd);
545
if (typeof lastKnownPos === 'number') {
546
547
// pos !== null signals that previously a position was used that is
548
// not null. node.js documentation explains, that in this case
549
// the internal file pointer is not moving and as such we do not move
550
// our position pointer.
551
//
552
// Docs: "If position is null, data will be read from the current file position,
553
// and the file position will be updated. If position is an integer, the file position
554
// will remain unchanged."
555
if (typeof pos === 'number') {
556
// do not modify the position
557
}
558
559
// bytesLength = number is a signal that the read/write operation was
560
// successful and as such we need to advance the position in the Map
561
//
562
// Docs (http://man7.org/linux/man-pages/man2/read.2.html):
563
// "On files that support seeking, the read operation commences at the
564
// file offset, and the file offset is incremented by the number of
565
// bytes read."
566
//
567
// Docs (http://man7.org/linux/man-pages/man2/write.2.html):
568
// "For a seekable file (i.e., one to which lseek(2) may be applied, for
569
// example, a regular file) writing takes place at the file offset, and
570
// the file offset is incremented by the number of bytes actually
571
// written."
572
else if (typeof bytesLength === 'number') {
573
this.mapHandleToPos.set(fd, lastKnownPos + bytesLength);
574
}
575
576
// bytesLength = null signals an error in the read/write operation
577
// and as such we drop the handle from the Map because the position
578
// is unspecificed at this point.
579
else {
580
this.mapHandleToPos.delete(fd);
581
}
582
}
583
}
584
585
async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
586
587
// We know at this point that the file to write to is truncated and thus empty
588
// if the write now fails, the file remains empty. as such we really try hard
589
// to ensure the write succeeds by retrying up to three times.
590
return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);
591
}
592
593
private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
594
const normalizedPos = this.normalizePos(fd, pos);
595
596
let bytesWritten: number | null = null;
597
try {
598
bytesWritten = (await Promises.write(fd, data, offset, length, normalizedPos)).bytesWritten;
599
} catch (error) {
600
throw await this.toFileSystemProviderWriteError(this.writeHandles.get(fd), error);
601
} finally {
602
this.updatePos(fd, normalizedPos, bytesWritten);
603
}
604
605
return bytesWritten;
606
}
607
608
//#endregion
609
610
//#region Move/Copy/Delete/Create Folder
611
612
async mkdir(resource: URI): Promise<void> {
613
try {
614
await promises.mkdir(this.toFilePath(resource));
615
} catch (error) {
616
throw this.toFileSystemProviderError(error);
617
}
618
}
619
620
async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
621
try {
622
const filePath = this.toFilePath(resource);
623
if (opts.recursive) {
624
let rmMoveToPath: string | undefined = undefined;
625
if (opts?.atomic !== false && opts.atomic.postfix) {
626
rmMoveToPath = join(dirname(filePath), `${basename(filePath)}${opts.atomic.postfix}`);
627
}
628
629
await Promises.rm(filePath, RimRafMode.MOVE, rmMoveToPath);
630
} else {
631
try {
632
await promises.unlink(filePath);
633
} catch (unlinkError) {
634
635
// `fs.unlink` will throw when used on directories
636
// we try to detect this error and then see if the
637
// provided resource is actually a directory. in that
638
// case we use `fs.rmdir` to delete the directory.
639
640
if (unlinkError.code === 'EPERM' || unlinkError.code === 'EISDIR') {
641
let isDirectory = false;
642
try {
643
const { stat, symbolicLink } = await SymlinkSupport.stat(filePath);
644
isDirectory = stat.isDirectory() && !symbolicLink;
645
} catch (statError) {
646
// ignore
647
}
648
649
if (isDirectory) {
650
await promises.rmdir(filePath);
651
} else {
652
throw unlinkError;
653
}
654
} else {
655
throw unlinkError;
656
}
657
}
658
}
659
} catch (error) {
660
throw this.toFileSystemProviderError(error);
661
}
662
}
663
664
async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
665
const fromFilePath = this.toFilePath(from);
666
const toFilePath = this.toFilePath(to);
667
668
if (fromFilePath === toFilePath) {
669
return; // simulate node.js behaviour here and do a no-op if paths match
670
}
671
672
try {
673
674
// Validate the move operation can perform
675
await this.validateMoveCopy(from, to, 'move', opts.overwrite);
676
677
// Rename
678
await Promises.rename(fromFilePath, toFilePath);
679
} catch (error) {
680
681
// Rewrite some typical errors that can happen especially around symlinks
682
// to something the user can better understand
683
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
684
error = new Error(localize('moveError', "Unable to move '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
685
}
686
687
throw this.toFileSystemProviderError(error);
688
}
689
}
690
691
async copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
692
const fromFilePath = this.toFilePath(from);
693
const toFilePath = this.toFilePath(to);
694
695
if (fromFilePath === toFilePath) {
696
return; // simulate node.js behaviour here and do a no-op if paths match
697
}
698
699
try {
700
701
// Validate the copy operation can perform
702
await this.validateMoveCopy(from, to, 'copy', opts.overwrite);
703
704
// Copy
705
await Promises.copy(fromFilePath, toFilePath, { preserveSymlinks: true });
706
} catch (error) {
707
708
// Rewrite some typical errors that can happen especially around symlinks
709
// to something the user can better understand
710
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
711
error = new Error(localize('copyError', "Unable to copy '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
712
}
713
714
throw this.toFileSystemProviderError(error);
715
}
716
}
717
718
private async validateMoveCopy(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<void> {
719
const fromFilePath = this.toFilePath(from);
720
const toFilePath = this.toFilePath(to);
721
722
let isSameResourceWithDifferentPathCase = false;
723
const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
724
if (!isPathCaseSensitive) {
725
isSameResourceWithDifferentPathCase = isEqual(fromFilePath, toFilePath, true /* ignore case */);
726
}
727
728
if (isSameResourceWithDifferentPathCase) {
729
730
// You cannot copy the same file to the same location with different
731
// path case unless you are on a case sensitive file system
732
if (mode === 'copy') {
733
throw createFileSystemProviderError(localize('fileCopyErrorPathCase', "File cannot be copied to same path with different path case"), FileSystemProviderErrorCode.FileExists);
734
}
735
736
// You can move the same file to the same location with different
737
// path case on case insensitive file systems
738
else if (mode === 'move') {
739
return;
740
}
741
}
742
743
// Here we have to see if the target to move/copy to exists or not.
744
// We need to respect the `overwrite` option to throw in case the
745
// target exists.
746
747
const fromStat = await this.statIgnoreError(from);
748
if (!fromStat) {
749
throw createFileSystemProviderError(localize('fileMoveCopyErrorNotFound', "File to move/copy does not exist"), FileSystemProviderErrorCode.FileNotFound);
750
}
751
752
const toStat = await this.statIgnoreError(to);
753
if (!toStat) {
754
return; // target does not exist so we are good
755
}
756
757
if (!overwrite) {
758
throw createFileSystemProviderError(localize('fileMoveCopyErrorExists', "File at target already exists and thus will not be moved/copied to unless overwrite is specified"), FileSystemProviderErrorCode.FileExists);
759
}
760
761
// Handle existing target for move/copy
762
if ((fromStat.type & FileType.File) !== 0 && (toStat.type & FileType.File) !== 0) {
763
return; // node.js can move/copy a file over an existing file without having to delete it first
764
} else {
765
await this.delete(to, { recursive: true, useTrash: false, atomic: false });
766
}
767
}
768
769
//#endregion
770
771
//#region Clone File
772
773
async cloneFile(from: URI, to: URI): Promise<void> {
774
return this.doCloneFile(from, to, false /* optimistically assume parent folders exist */);
775
}
776
777
private async doCloneFile(from: URI, to: URI, mkdir: boolean): Promise<void> {
778
const fromFilePath = this.toFilePath(from);
779
const toFilePath = this.toFilePath(to);
780
781
const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
782
if (isEqual(fromFilePath, toFilePath, !isPathCaseSensitive)) {
783
return; // cloning is only supported `from` and `to` are different files
784
}
785
786
// Implement clone by using `fs.copyFile`, however setup locks
787
// for both `from` and `to` because node.js does not ensure
788
// this to be an atomic operation
789
790
const locks = new DisposableStore();
791
792
try {
793
locks.add(await this.createResourceLock(from));
794
locks.add(await this.createResourceLock(to));
795
796
if (mkdir) {
797
await promises.mkdir(dirname(toFilePath), { recursive: true });
798
}
799
800
await promises.copyFile(fromFilePath, toFilePath);
801
} catch (error) {
802
if (error.code === 'ENOENT' && !mkdir) {
803
return this.doCloneFile(from, to, true);
804
}
805
806
throw this.toFileSystemProviderError(error);
807
} finally {
808
locks.dispose();
809
}
810
}
811
812
//#endregion
813
814
//#region File Watching
815
816
protected createUniversalWatcher(
817
onChange: (changes: IFileChange[]) => void,
818
onLogMessage: (msg: ILogMessage) => void,
819
verboseLogging: boolean
820
): AbstractUniversalWatcherClient {
821
return new UniversalWatcherClient(changes => onChange(changes), msg => onLogMessage(msg), verboseLogging);
822
}
823
824
protected createNonRecursiveWatcher(
825
onChange: (changes: IFileChange[]) => void,
826
onLogMessage: (msg: ILogMessage) => void,
827
verboseLogging: boolean
828
): AbstractNonRecursiveWatcherClient {
829
return new NodeJSWatcherClient(changes => onChange(changes), msg => onLogMessage(msg), verboseLogging);
830
}
831
832
//#endregion
833
834
//#region Helpers
835
836
private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError {
837
if (error instanceof FileSystemProviderError) {
838
return error; // avoid double conversion
839
}
840
841
let resultError: Error | string = error;
842
let code: FileSystemProviderErrorCode;
843
switch (error.code) {
844
case 'ENOENT':
845
code = FileSystemProviderErrorCode.FileNotFound;
846
break;
847
case 'EISDIR':
848
code = FileSystemProviderErrorCode.FileIsADirectory;
849
break;
850
case 'ENOTDIR':
851
code = FileSystemProviderErrorCode.FileNotADirectory;
852
break;
853
case 'EEXIST':
854
code = FileSystemProviderErrorCode.FileExists;
855
break;
856
case 'EPERM':
857
case 'EACCES':
858
code = FileSystemProviderErrorCode.NoPermissions;
859
break;
860
case 'ERR_UNC_HOST_NOT_ALLOWED':
861
resultError = `${error.message}. Please update the 'security.allowedUNCHosts' setting if you want to allow this host.`;
862
code = FileSystemProviderErrorCode.Unknown;
863
break;
864
default:
865
code = FileSystemProviderErrorCode.Unknown;
866
}
867
868
return createFileSystemProviderError(resultError, code);
869
}
870
871
private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {
872
let fileSystemProviderWriteError = this.toFileSystemProviderError(error);
873
874
// If the write error signals permission issues, we try
875
// to read the file's mode to see if the file is write
876
// locked.
877
if (resource && fileSystemProviderWriteError.code === FileSystemProviderErrorCode.NoPermissions) {
878
try {
879
const { stat } = await SymlinkSupport.stat(this.toFilePath(resource));
880
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
881
fileSystemProviderWriteError = createFileSystemProviderError(error, FileSystemProviderErrorCode.FileWriteLocked);
882
}
883
} catch (error) {
884
this.logService.trace(error); // ignore - return original error
885
}
886
}
887
888
return fileSystemProviderWriteError;
889
}
890
891
//#endregion
892
}
893
894