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