Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/node/pfs.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 * as fs from 'fs';
7
import { tmpdir } from 'os';
8
import { promisify } from 'util';
9
import { ResourceQueue, timeout } from '../common/async.js';
10
import { isEqualOrParent, isRootOrDriveLetter, randomPath } from '../common/extpath.js';
11
import { normalizeNFC } from '../common/normalization.js';
12
import { basename, dirname, join, normalize, sep } from '../common/path.js';
13
import { isLinux, isMacintosh, isWindows } from '../common/platform.js';
14
import { extUriBiasedIgnorePathCase } from '../common/resources.js';
15
import { URI } from '../common/uri.js';
16
import { CancellationToken } from '../common/cancellation.js';
17
import { rtrim } from '../common/strings.js';
18
19
//#region rimraf
20
21
export enum RimRafMode {
22
23
/**
24
* Slow version that unlinks each file and folder.
25
*/
26
UNLINK,
27
28
/**
29
* Fast version that first moves the file/folder
30
* into a temp directory and then deletes that
31
* without waiting for it.
32
*/
33
MOVE
34
}
35
36
/**
37
* Allows to delete the provided path (either file or folder) recursively
38
* with the options:
39
* - `UNLINK`: direct removal from disk
40
* - `MOVE`: faster variant that first moves the target to temp dir and then
41
* deletes it in the background without waiting for that to finish.
42
* the optional `moveToPath` allows to override where to rename the
43
* path to before deleting it.
44
*/
45
async function rimraf(path: string, mode: RimRafMode.UNLINK): Promise<void>;
46
async function rimraf(path: string, mode: RimRafMode.MOVE, moveToPath?: string): Promise<void>;
47
async function rimraf(path: string, mode?: RimRafMode, moveToPath?: string): Promise<void>;
48
async function rimraf(path: string, mode = RimRafMode.UNLINK, moveToPath?: string): Promise<void> {
49
if (isRootOrDriveLetter(path)) {
50
throw new Error('rimraf - will refuse to recursively delete root');
51
}
52
53
// delete: via rm
54
if (mode === RimRafMode.UNLINK) {
55
return rimrafUnlink(path);
56
}
57
58
// delete: via move
59
return rimrafMove(path, moveToPath);
60
}
61
62
async function rimrafMove(path: string, moveToPath = randomPath(tmpdir())): Promise<void> {
63
try {
64
try {
65
await fs.promises.rename(path, moveToPath);
66
} catch (error) {
67
if (error.code === 'ENOENT') {
68
return; // ignore - path to delete did not exist
69
}
70
71
return rimrafUnlink(path); // otherwise fallback to unlink
72
}
73
74
// Delete but do not return as promise
75
rimrafUnlink(moveToPath).catch(error => {/* ignore */ });
76
} catch (error) {
77
if (error.code !== 'ENOENT') {
78
throw error;
79
}
80
}
81
}
82
83
async function rimrafUnlink(path: string): Promise<void> {
84
return fs.promises.rm(path, { recursive: true, force: true, maxRetries: 3 });
85
}
86
87
//#endregion
88
89
//#region readdir with NFC support (macos)
90
91
export interface IDirent {
92
name: string;
93
94
isFile(): boolean;
95
isDirectory(): boolean;
96
isSymbolicLink(): boolean;
97
}
98
99
/**
100
* Drop-in replacement of `fs.readdir` with support
101
* for converting from macOS NFD unicon form to NFC
102
* (https://github.com/nodejs/node/issues/2165)
103
*/
104
async function readdir(path: string): Promise<string[]>;
105
async function readdir(path: string, options: { withFileTypes: true }): Promise<IDirent[]>;
106
async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> {
107
try {
108
return await doReaddir(path, options);
109
} catch (error) {
110
// TODO@bpasero workaround for #252361 that should be removed
111
// once the upstream issue in node.js is resolved. Adds a trailing
112
// dot to a root drive letter path (G:\ => G:\.) as a workaround.
113
if (error.code === 'ENOENT' && isWindows && isRootOrDriveLetter(path)) {
114
try {
115
return await doReaddir(`${path}.`, options);
116
} catch (e) {
117
// ignore
118
}
119
}
120
throw error;
121
}
122
}
123
124
async function doReaddir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> {
125
return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path)));
126
}
127
128
async function safeReaddirWithFileTypes(path: string): Promise<IDirent[]> {
129
try {
130
return await fs.promises.readdir(path, { withFileTypes: true });
131
} catch (error) {
132
console.warn('[node.js fs] readdir with filetypes failed with error: ', error);
133
}
134
135
// Fallback to manually reading and resolving each
136
// children of the folder in case we hit an error
137
// previously.
138
// This can only really happen on exotic file systems
139
// such as explained in #115645 where we get entries
140
// from `readdir` that we can later not `lstat`.
141
const result: IDirent[] = [];
142
const children = await readdir(path);
143
for (const child of children) {
144
let isFile = false;
145
let isDirectory = false;
146
let isSymbolicLink = false;
147
148
try {
149
const lstat = await fs.promises.lstat(join(path, child));
150
151
isFile = lstat.isFile();
152
isDirectory = lstat.isDirectory();
153
isSymbolicLink = lstat.isSymbolicLink();
154
} catch (error) {
155
console.warn('[node.js fs] unexpected error from lstat after readdir: ', error);
156
}
157
158
result.push({
159
name: child,
160
isFile: () => isFile,
161
isDirectory: () => isDirectory,
162
isSymbolicLink: () => isSymbolicLink
163
});
164
}
165
166
return result;
167
}
168
169
function handleDirectoryChildren(children: string[]): string[];
170
function handleDirectoryChildren(children: IDirent[]): IDirent[];
171
function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[];
172
function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[] {
173
return children.map(child => {
174
175
// Mac: uses NFD unicode form on disk, but we want NFC
176
// See also https://github.com/nodejs/node/issues/2165
177
178
if (typeof child === 'string') {
179
return isMacintosh ? normalizeNFC(child) : child;
180
}
181
182
child.name = isMacintosh ? normalizeNFC(child.name) : child.name;
183
184
return child;
185
});
186
}
187
188
/**
189
* A convenience method to read all children of a path that
190
* are directories.
191
*/
192
async function readDirsInDir(dirPath: string): Promise<string[]> {
193
const children = await readdir(dirPath);
194
const directories: string[] = [];
195
196
for (const child of children) {
197
if (await SymlinkSupport.existsDirectory(join(dirPath, child))) {
198
directories.push(child);
199
}
200
}
201
202
return directories;
203
}
204
205
//#endregion
206
207
//#region whenDeleted()
208
209
/**
210
* A `Promise` that resolves when the provided `path`
211
* is deleted from disk.
212
*/
213
export function whenDeleted(path: string, intervalMs = 1000): Promise<void> {
214
return new Promise<void>(resolve => {
215
let running = false;
216
const interval = setInterval(() => {
217
if (!running) {
218
running = true;
219
fs.access(path, err => {
220
running = false;
221
222
if (err) {
223
clearInterval(interval);
224
resolve(undefined);
225
}
226
});
227
}
228
}, intervalMs);
229
});
230
}
231
232
//#endregion
233
234
//#region Methods with symbolic links support
235
236
export namespace SymlinkSupport {
237
238
export interface IStats {
239
240
// The stats of the file. If the file is a symbolic
241
// link, the stats will be of that target file and
242
// not the link itself.
243
// If the file is a symbolic link pointing to a non
244
// existing file, the stat will be of the link and
245
// the `dangling` flag will indicate this.
246
stat: fs.Stats;
247
248
// Will be provided if the resource is a symbolic link
249
// on disk. Use the `dangling` flag to find out if it
250
// points to a resource that does not exist on disk.
251
symbolicLink?: { dangling: boolean };
252
}
253
254
/**
255
* Resolves the `fs.Stats` of the provided path. If the path is a
256
* symbolic link, the `fs.Stats` will be from the target it points
257
* to. If the target does not exist, `dangling: true` will be returned
258
* as `symbolicLink` value.
259
*/
260
export async function stat(path: string): Promise<IStats> {
261
262
// First stat the link
263
let lstats: fs.Stats | undefined;
264
try {
265
lstats = await fs.promises.lstat(path);
266
267
// Return early if the stat is not a symbolic link at all
268
if (!lstats.isSymbolicLink()) {
269
return { stat: lstats };
270
}
271
} catch (error) {
272
/* ignore - use stat() instead */
273
}
274
275
// If the stat is a symbolic link or failed to stat, use fs.stat()
276
// which for symbolic links will stat the target they point to
277
try {
278
const stats = await fs.promises.stat(path);
279
280
return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined };
281
} catch (error) {
282
283
// If the link points to a nonexistent file we still want
284
// to return it as result while setting dangling: true flag
285
if (error.code === 'ENOENT' && lstats) {
286
return { stat: lstats, symbolicLink: { dangling: true } };
287
}
288
289
// Windows: workaround a node.js bug where reparse points
290
// are not supported (https://github.com/nodejs/node/issues/36790)
291
if (isWindows && error.code === 'EACCES') {
292
try {
293
const stats = await fs.promises.stat(await fs.promises.readlink(path));
294
295
return { stat: stats, symbolicLink: { dangling: false } };
296
} catch (error) {
297
298
// If the link points to a nonexistent file we still want
299
// to return it as result while setting dangling: true flag
300
if (error.code === 'ENOENT' && lstats) {
301
return { stat: lstats, symbolicLink: { dangling: true } };
302
}
303
304
throw error;
305
}
306
}
307
308
throw error;
309
}
310
}
311
312
/**
313
* Figures out if the `path` exists and is a file with support
314
* for symlinks.
315
*
316
* Note: this will return `false` for a symlink that exists on
317
* disk but is dangling (pointing to a nonexistent path).
318
*
319
* Use `exists` if you only care about the path existing on disk
320
* or not without support for symbolic links.
321
*/
322
export async function existsFile(path: string): Promise<boolean> {
323
try {
324
const { stat, symbolicLink } = await SymlinkSupport.stat(path);
325
326
return stat.isFile() && symbolicLink?.dangling !== true;
327
} catch (error) {
328
// Ignore, path might not exist
329
}
330
331
return false;
332
}
333
334
/**
335
* Figures out if the `path` exists and is a directory with support for
336
* symlinks.
337
*
338
* Note: this will return `false` for a symlink that exists on
339
* disk but is dangling (pointing to a nonexistent path).
340
*
341
* Use `exists` if you only care about the path existing on disk
342
* or not without support for symbolic links.
343
*/
344
export async function existsDirectory(path: string): Promise<boolean> {
345
try {
346
const { stat, symbolicLink } = await SymlinkSupport.stat(path);
347
348
return stat.isDirectory() && symbolicLink?.dangling !== true;
349
} catch (error) {
350
// Ignore, path might not exist
351
}
352
353
return false;
354
}
355
}
356
357
//#endregion
358
359
//#region Write File
360
361
// According to node.js docs (https://nodejs.org/docs/v14.16.0/api/fs.html#fs_fs_writefile_file_data_options_callback)
362
// it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return.
363
// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly.
364
const writeQueues = new ResourceQueue();
365
366
/**
367
* Same as `fs.writeFile` but with an additional call to
368
* `fs.fdatasync` after writing to ensure changes are
369
* flushed to disk.
370
*
371
* In addition, multiple writes to the same path are queued.
372
*/
373
function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise<void>;
374
function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise<void>;
375
function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise<void>;
376
function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void>;
377
function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void> {
378
return writeQueues.queueFor(URI.file(path), () => {
379
const ensuredOptions = ensureWriteOptions(options);
380
381
return new Promise((resolve, reject) => doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve()));
382
}, extUriBiasedIgnorePathCase);
383
}
384
385
interface IWriteFileOptions {
386
mode?: number;
387
flag?: string;
388
}
389
390
interface IEnsuredWriteFileOptions extends IWriteFileOptions {
391
mode: number;
392
flag: string;
393
}
394
395
let canFlush = true;
396
export function configureFlushOnWrite(enabled: boolean): void {
397
canFlush = enabled;
398
}
399
400
// Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk
401
// We do this in cases where we want to make sure the data is really on disk and
402
// not in some cache.
403
//
404
// See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194
405
function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, options: IEnsuredWriteFileOptions, callback: (error: Error | null) => void): void {
406
if (!canFlush) {
407
return fs.writeFile(path, data, { mode: options.mode, flag: options.flag }, callback);
408
}
409
410
// Open the file with same flags and mode as fs.writeFile()
411
fs.open(path, options.flag, options.mode, (openError, fd) => {
412
if (openError) {
413
return callback(openError);
414
}
415
416
// It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open!
417
fs.writeFile(fd, data, writeError => {
418
if (writeError) {
419
return fs.close(fd, () => callback(writeError)); // still need to close the handle on error!
420
}
421
422
// Flush contents (not metadata) of the file to disk
423
// https://github.com/microsoft/vscode/issues/9589
424
fs.fdatasync(fd, (syncError: Error | null) => {
425
426
// In some exotic setups it is well possible that node fails to sync
427
// In that case we disable flushing and warn to the console
428
if (syncError) {
429
console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError);
430
configureFlushOnWrite(false);
431
}
432
433
return fs.close(fd, closeError => callback(closeError));
434
});
435
});
436
});
437
}
438
439
/**
440
* Same as `fs.writeFileSync` but with an additional call to
441
* `fs.fdatasyncSync` after writing to ensure changes are
442
* flushed to disk.
443
*
444
* @deprecated always prefer async variants over sync!
445
*/
446
export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void {
447
const ensuredOptions = ensureWriteOptions(options);
448
449
if (!canFlush) {
450
return fs.writeFileSync(path, data, { mode: ensuredOptions.mode, flag: ensuredOptions.flag });
451
}
452
453
// Open the file with same flags and mode as fs.writeFile()
454
const fd = fs.openSync(path, ensuredOptions.flag, ensuredOptions.mode);
455
456
try {
457
458
// It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open!
459
fs.writeFileSync(fd, data);
460
461
// Flush contents (not metadata) of the file to disk
462
try {
463
fs.fdatasyncSync(fd); // https://github.com/microsoft/vscode/issues/9589
464
} catch (syncError) {
465
console.warn('[node.js fs] fdatasyncSync is now disabled for this session because it failed: ', syncError);
466
configureFlushOnWrite(false);
467
}
468
} finally {
469
fs.closeSync(fd);
470
}
471
}
472
473
function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptions {
474
if (!options) {
475
return { mode: 0o666 /* default node.js mode for files */, flag: 'w' };
476
}
477
478
return {
479
mode: typeof options.mode === 'number' ? options.mode : 0o666 /* default node.js mode for files */,
480
flag: typeof options.flag === 'string' ? options.flag : 'w'
481
};
482
}
483
484
//#endregion
485
486
//#region Move / Copy
487
488
/**
489
* A drop-in replacement for `fs.rename` that:
490
* - allows to move across multiple disks
491
* - attempts to retry the operation for certain error codes on Windows
492
*/
493
async function rename(source: string, target: string, windowsRetryTimeout: number | false = 60000): Promise<void> {
494
if (source === target) {
495
return; // simulate node.js behaviour here and do a no-op if paths match
496
}
497
498
try {
499
if (isWindows && typeof windowsRetryTimeout === 'number') {
500
// On Windows, a rename can fail when either source or target
501
// is locked by AV software.
502
await renameWithRetry(source, target, Date.now(), windowsRetryTimeout);
503
} else {
504
await fs.promises.rename(source, target);
505
}
506
} catch (error) {
507
// In two cases we fallback to classic copy and delete:
508
//
509
// 1.) The EXDEV error indicates that source and target are on different devices
510
// In this case, fallback to using a copy() operation as there is no way to
511
// rename() between different devices.
512
//
513
// 2.) The user tries to rename a file/folder that ends with a dot. This is not
514
// really possible to move then, at least on UNC devices.
515
if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) {
516
await copy(source, target, { preserveSymlinks: false /* copying to another device */ });
517
await rimraf(source, RimRafMode.MOVE);
518
} else {
519
throw error;
520
}
521
}
522
}
523
524
async function renameWithRetry(source: string, target: string, startTime: number, retryTimeout: number, attempt = 0): Promise<void> {
525
try {
526
return await fs.promises.rename(source, target);
527
} catch (error) {
528
if (error.code !== 'EACCES' && error.code !== 'EPERM' && error.code !== 'EBUSY') {
529
throw error; // only for errors we think are temporary
530
}
531
532
if (Date.now() - startTime >= retryTimeout) {
533
console.error(`[node.js fs] rename failed after ${attempt} retries with error: ${error}`);
534
535
throw error; // give up after configurable timeout
536
}
537
538
if (attempt === 0) {
539
let abortRetry = false;
540
try {
541
const { stat } = await SymlinkSupport.stat(target);
542
if (!stat.isFile()) {
543
abortRetry = true; // if target is not a file, EPERM error may be raised and we should not attempt to retry
544
}
545
} catch (error) {
546
// Ignore
547
}
548
549
if (abortRetry) {
550
throw error;
551
}
552
}
553
554
// Delay with incremental backoff up to 100ms
555
await timeout(Math.min(100, attempt * 10));
556
557
// Attempt again
558
return renameWithRetry(source, target, startTime, retryTimeout, attempt + 1);
559
}
560
}
561
562
interface ICopyPayload {
563
readonly root: { source: string; target: string };
564
readonly options: { preserveSymlinks: boolean };
565
readonly handledSourcePaths: Set<string>;
566
}
567
568
/**
569
* Recursively copies all of `source` to `target`.
570
*
571
* The options `preserveSymlinks` configures how symbolic
572
* links should be handled when encountered. Set to
573
* `false` to not preserve them and `true` otherwise.
574
*/
575
async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise<void> {
576
return doCopy(source, target, { root: { source, target }, options, handledSourcePaths: new Set<string>() });
577
}
578
579
// When copying a file or folder, we want to preserve the mode
580
// it had and as such provide it when creating. However, modes
581
// can go beyond what we expect (see link below), so we mask it.
582
// (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588)
583
const COPY_MODE_MASK = 0o777;
584
585
async function doCopy(source: string, target: string, payload: ICopyPayload): Promise<void> {
586
587
// Keep track of paths already copied to prevent
588
// cycles from symbolic links to cause issues
589
if (payload.handledSourcePaths.has(source)) {
590
return;
591
} else {
592
payload.handledSourcePaths.add(source);
593
}
594
595
const { stat, symbolicLink } = await SymlinkSupport.stat(source);
596
597
// Symlink
598
if (symbolicLink) {
599
600
// Try to re-create the symlink unless `preserveSymlinks: false`
601
if (payload.options.preserveSymlinks) {
602
try {
603
return await doCopySymlink(source, target, payload);
604
} catch (error) {
605
// in any case of an error fallback to normal copy via dereferencing
606
}
607
}
608
609
if (symbolicLink.dangling) {
610
return; // skip dangling symbolic links from here on (https://github.com/microsoft/vscode/issues/111621)
611
}
612
}
613
614
// Folder
615
if (stat.isDirectory()) {
616
return doCopyDirectory(source, target, stat.mode & COPY_MODE_MASK, payload);
617
}
618
619
// File or file-like
620
else {
621
return doCopyFile(source, target, stat.mode & COPY_MODE_MASK);
622
}
623
}
624
625
async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise<void> {
626
627
// Create folder
628
await fs.promises.mkdir(target, { recursive: true, mode });
629
630
// Copy each file recursively
631
const files = await readdir(source);
632
for (const file of files) {
633
await doCopy(join(source, file), join(target, file), payload);
634
}
635
}
636
637
async function doCopyFile(source: string, target: string, mode: number): Promise<void> {
638
639
// Copy file
640
await fs.promises.copyFile(source, target);
641
642
// restore mode (https://github.com/nodejs/node/issues/1104)
643
await fs.promises.chmod(target, mode);
644
}
645
646
async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise<void> {
647
648
// Figure out link target
649
let linkTarget = await fs.promises.readlink(source);
650
651
// Special case: the symlink points to a target that is
652
// actually within the path that is being copied. In that
653
// case we want the symlink to point to the target and
654
// not the source
655
if (isEqualOrParent(linkTarget, payload.root.source, !isLinux)) {
656
linkTarget = join(payload.root.target, linkTarget.substr(payload.root.source.length + 1));
657
}
658
659
// Create symlink
660
await fs.promises.symlink(linkTarget, target);
661
}
662
663
//#endregion
664
665
//#region Path resolvers
666
667
/**
668
* Given an absolute, normalized, and existing file path 'realcase' returns the
669
* exact path that the file has on disk.
670
* On a case insensitive file system, the returned path might differ from the original
671
* path by character casing.
672
* On a case sensitive file system, the returned path will always be identical to the
673
* original path.
674
* In case of errors, null is returned. But you cannot use this function to verify that
675
* a path exists.
676
*
677
* realcase does not handle '..' or '.' path segments and it does not take the locale into account.
678
*/
679
export async function realcase(path: string, token?: CancellationToken): Promise<string | null> {
680
if (isLinux) {
681
// This method is unsupported on OS that have case sensitive
682
// file system where the same path can exist in different forms
683
// (see also https://github.com/microsoft/vscode/issues/139709)
684
return path;
685
}
686
687
const dir = dirname(path);
688
if (path === dir) { // end recursion
689
return path;
690
}
691
692
const name = (basename(path) /* can be '' for windows drive letters */ || path).toLowerCase();
693
try {
694
if (token?.isCancellationRequested) {
695
return null;
696
}
697
698
const entries = await Promises.readdir(dir);
699
const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search
700
if (found.length === 1) {
701
// on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition
702
const prefix = await realcase(dir, token); // recurse
703
if (prefix) {
704
return join(prefix, found[0]);
705
}
706
} else if (found.length > 1) {
707
// must be a case sensitive $filesystem
708
const ix = found.indexOf(name);
709
if (ix >= 0) { // case sensitive
710
const prefix = await realcase(dir, token); // recurse
711
if (prefix) {
712
return join(prefix, found[ix]);
713
}
714
}
715
}
716
} catch (error) {
717
// silently ignore error
718
}
719
720
return null;
721
}
722
723
async function realpath(path: string): Promise<string> {
724
try {
725
// DO NOT USE `fs.promises.realpath` here as it internally
726
// calls `fs.native.realpath` which will result in subst
727
// drives to be resolved to their target on Windows
728
// https://github.com/microsoft/vscode/issues/118562
729
return await promisify(fs.realpath)(path);
730
} catch (error) {
731
732
// We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization
733
// we now do a similar normalization and then try again if we can access the path with read
734
// permissions at least. If that succeeds, we return that path.
735
// fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is
736
// to not resolve links but to simply see if the path is read accessible or not.
737
const normalizedPath = normalizePath(path);
738
739
await fs.promises.access(normalizedPath, fs.constants.R_OK);
740
741
return normalizedPath;
742
}
743
}
744
745
/**
746
* @deprecated always prefer async variants over sync!
747
*/
748
export function realpathSync(path: string): string {
749
try {
750
return fs.realpathSync(path);
751
} catch (error) {
752
753
// We hit an error calling fs.realpathSync(). Since fs.realpathSync() is doing some path normalization
754
// we now do a similar normalization and then try again if we can access the path with read
755
// permissions at least. If that succeeds, we return that path.
756
// fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is
757
// to not resolve links but to simply see if the path is read accessible or not.
758
const normalizedPath = normalizePath(path);
759
760
fs.accessSync(normalizedPath, fs.constants.R_OK); // throws in case of an error
761
762
return normalizedPath;
763
}
764
}
765
766
function normalizePath(path: string): string {
767
return rtrim(normalize(path), sep);
768
}
769
770
//#endregion
771
772
//#region Promise based fs methods
773
774
/**
775
* Some low level `fs` methods provided as `Promises` similar to
776
* `fs.promises` but with notable differences, either implemented
777
* by us or by restoring the original callback based behavior.
778
*
779
* At least `realpath` is implemented differently in the promise
780
* based implementation compared to the callback based one. The
781
* promise based implementation actually calls `fs.realpath.native`.
782
* (https://github.com/microsoft/vscode/issues/118562)
783
*/
784
export const Promises = new class {
785
786
//#region Implemented by node.js
787
788
get read() {
789
790
// Not using `promisify` here for a reason: the return
791
// type is not an object as indicated by TypeScript but
792
// just the bytes read, so we create our own wrapper.
793
794
return (fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null) => {
795
return new Promise<{ bytesRead: number; buffer: Uint8Array }>((resolve, reject) => {
796
fs.read(fd, buffer, offset, length, position, (err, bytesRead, buffer) => {
797
if (err) {
798
return reject(err);
799
}
800
801
return resolve({ bytesRead, buffer });
802
});
803
});
804
};
805
}
806
807
get write() {
808
809
// Not using `promisify` here for a reason: the return
810
// type is not an object as indicated by TypeScript but
811
// just the bytes written, so we create our own wrapper.
812
813
return (fd: number, buffer: Uint8Array, offset: number | undefined | null, length: number | undefined | null, position: number | undefined | null) => {
814
return new Promise<{ bytesWritten: number; buffer: Uint8Array }>((resolve, reject) => {
815
fs.write(fd, buffer, offset, length, position, (err, bytesWritten, buffer) => {
816
if (err) {
817
return reject(err);
818
}
819
820
return resolve({ bytesWritten, buffer });
821
});
822
});
823
};
824
}
825
826
get fdatasync() { return promisify(fs.fdatasync); } // not exposed as API in 22.x yet
827
828
get open() { return promisify(fs.open); } // changed to return `FileHandle` in promise API
829
get close() { return promisify(fs.close); } // not exposed as API due to the `FileHandle` return type of `open`
830
831
get ftruncate() { return promisify(fs.ftruncate); } // not exposed as API in 22.x yet
832
833
//#endregion
834
835
//#region Implemented by us
836
837
async exists(path: string): Promise<boolean> {
838
try {
839
await fs.promises.access(path);
840
841
return true;
842
} catch {
843
return false;
844
}
845
}
846
847
get readdir() { return readdir; }
848
get readDirsInDir() { return readDirsInDir; }
849
850
get writeFile() { return writeFile; }
851
852
get rm() { return rimraf; }
853
854
get rename() { return rename; }
855
get copy() { return copy; }
856
857
get realpath() { return realpath; } // `fs.promises.realpath` will use `fs.realpath.native` which we do not want
858
859
//#endregion
860
};
861
862
//#endregion
863
864