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