-
Notifications
You must be signed in to change notification settings - Fork 681
Expand file tree
/
Copy pathFileSystem.ts
More file actions
1747 lines (1576 loc) · 59 KB
/
FileSystem.ts
File metadata and controls
1747 lines (1576 loc) · 59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as nodeJsPath from 'node:path';
import * as fs from 'node:fs';
import * as fsPromises from 'node:fs/promises';
import * as fsx from 'fs-extra';
import { Text, type NewlineKind, Encoding } from './Text';
import { PosixModeBits } from './PosixModeBits';
/**
* An alias for the Node.js `fs.Stats` object.
*
* @remarks
* This avoids the need to import the `fs` package when using the {@link FileSystem} API.
* @public
*/
export type FileSystemStats = fs.Stats;
/**
* An alias for the Node.js `fs.Dirent` object.
*
* @remarks
* This avoids the need to import the `fs` package when using the {@link FileSystem} API.
* @public
*/
export type FolderItem = fs.Dirent;
/**
* An alias for the Node.js `fs.ReadStream` object.
*
* @remarks
* This avoids the need to import the `fs` package when using the {@link FileSystem} API.
* @public
*/
export type FileSystemReadStream = fs.ReadStream;
/**
* An alias for the Node.js `fs.WriteStream` object.
*
* @remarks
* This avoids the need to import the `fs` package when using the {@link FileSystem} API.
* @public
*/
export type FileSystemWriteStream = fs.WriteStream;
// The PosixModeBits are intended to be used with bitwise operations.
/* eslint-disable no-bitwise */
/**
* The options for {@link FileSystem.readFolderItems} and {@link FileSystem.readFolderItemNames}.
* @public
*/
export interface IFileSystemReadFolderOptions {
/**
* If true, returns the absolute paths of the files in the folder.
* @defaultValue false
*/
absolutePaths?: boolean;
}
/**
* @public
*/
export interface IFileSystemWriteFileOptionsBase {
/**
* If true, will ensure the folder is created before writing the file.
* @defaultValue false
*/
ensureFolderExists?: boolean;
}
/**
* The options for {@link FileSystem.writeBuffersToFile}
* @public
*/
export interface IFileSystemWriteBinaryFileOptions extends IFileSystemWriteFileOptionsBase {}
/**
* The options for {@link FileSystem.writeFile}
* @public
*/
export interface IFileSystemWriteFileOptions extends IFileSystemWriteBinaryFileOptions {
/**
* If specified, will normalize line endings to the specified style of newline.
* @defaultValue `undefined` which means no conversion will be performed
*/
convertLineEndings?: NewlineKind;
/**
* If specified, will change the encoding of the file that will be written.
* @defaultValue "utf8"
*/
encoding?: Encoding;
}
/**
* The options for {@link FileSystem.readFile}
* @public
*/
export interface IFileSystemReadFileOptions {
/**
* If specified, will change the encoding of the file that will be written.
* @defaultValue Encoding.Utf8
*/
encoding?: Encoding;
/**
* If specified, will normalize line endings to the specified style of newline.
* @defaultValue `undefined` which means no conversion will be performed
*/
convertLineEndings?: NewlineKind;
}
/**
* The options for {@link FileSystem.move}
* @public
*/
export interface IFileSystemMoveOptions extends IFileSystemWriteFileOptionsBase {
/**
* The path of the existing object to be moved.
* The path may be absolute or relative.
*/
sourcePath: string;
/**
* The new path for the object.
* The path may be absolute or relative.
*/
destinationPath: string;
/**
* If true, will overwrite the file if it already exists.
* @defaultValue true
*/
overwrite?: boolean;
}
/**
* @public
*/
export interface IFileSystemCopyFileBaseOptions {
/**
* The path of the existing object to be copied.
* The path may be absolute or relative.
*/
sourcePath: string;
/**
* Specifies what to do if the destination path already exists.
* @defaultValue {@link AlreadyExistsBehavior.Overwrite}
*/
alreadyExistsBehavior?: AlreadyExistsBehavior;
}
/**
* The options for {@link FileSystem.copyFile}
* @public
*/
export interface IFileSystemCopyFileOptions extends IFileSystemCopyFileBaseOptions {
/**
* The path that the object will be copied to.
* The path may be absolute or relative.
*/
destinationPath: string;
}
/**
* Specifies the behavior of APIs such as {@link FileSystem.copyFile} or
* {@link FileSystem.createSymbolicLinkFile} when the output file path already exists.
*
* @remarks
* For {@link FileSystem.copyFile} and related APIs, the "output file path" is
* {@link IFileSystemCopyFileOptions.destinationPath}.
*
* For {@link FileSystem.createSymbolicLinkFile} and related APIs, the "output file path" is
* {@link IFileSystemCreateLinkOptions.newLinkPath}.
*
* @public
*/
export enum AlreadyExistsBehavior {
/**
* If the output file path already exists, try to overwrite the existing object.
*
* @remarks
* If overwriting the object would require recursively deleting a folder tree,
* then the operation will fail. As an example, suppose {@link FileSystem.copyFile}
* is copying a single file `/a/b/c` to the destination path `/d/e`, and `/d/e` is a
* nonempty folder. In this situation, an error will be reported; specifying
* `AlreadyExistsBehavior.Overwrite` does not help. Empty folders can be overwritten
* depending on the details of the implementation.
*/
Overwrite = 'overwrite',
/**
* If the output file path already exists, the operation will fail, and an error
* will be reported.
*/
Error = 'error',
/**
* If the output file path already exists, skip this item, and continue the operation.
*/
Ignore = 'ignore'
}
/**
* Callback function type for {@link IFileSystemCopyFilesAsyncOptions.filter}
* @public
*/
export type FileSystemCopyFilesAsyncFilter = (
sourcePath: string,
destinationPath: string
) => Promise<boolean>;
/**
* Callback function type for {@link IFileSystemCopyFilesOptions.filter}
* @public
*/
export type FileSystemCopyFilesFilter = (sourcePath: string, destinationPath: string) => boolean;
/**
* The options for {@link FileSystem.copyFilesAsync}
* @public
*/
export interface IFileSystemCopyFilesAsyncOptions {
/**
* The starting path of the file or folder to be copied.
* The path may be absolute or relative.
*/
sourcePath: string;
/**
* The path that the files will be copied to.
* The path may be absolute or relative.
*/
destinationPath: string;
/**
* If true, then when copying symlinks, copy the target object instead of copying the link.
*/
dereferenceSymlinks?: boolean;
/**
* Specifies what to do if a destination path already exists.
*
* @remarks
* This setting is applied individually for each file being copied.
* For example, `AlreadyExistsBehavior.Overwrite` will not recursively delete a folder
* whose path corresponds to an individual file that is being copied to that location.
*/
alreadyExistsBehavior?: AlreadyExistsBehavior;
/**
* If true, then the target object will be assigned "last modification" and "last access" timestamps
* that are the same as the source. Otherwise, the OS default timestamps are assigned.
*/
preserveTimestamps?: boolean;
/**
* A callback that will be invoked for each path that is copied. The callback can return `false`
* to cause the object to be excluded from the operation.
*/
filter?: FileSystemCopyFilesAsyncFilter | FileSystemCopyFilesFilter;
}
/**
* The options for {@link FileSystem.copyFiles}
* @public
*/
export interface IFileSystemCopyFilesOptions extends IFileSystemCopyFilesAsyncOptions {
/** {@inheritdoc IFileSystemCopyFilesAsyncOptions.filter} */
filter?: FileSystemCopyFilesFilter; // narrow the type to exclude FileSystemCopyFilesAsyncFilter
}
/**
* The options for {@link FileSystem.createWriteStream}
* @public
*/
export interface IFileSystemCreateWriteStreamOptions extends IFileSystemWriteFileOptionsBase {}
/**
* The options for {@link FileSystem.deleteFile}
* @public
*/
export interface IFileSystemDeleteFileOptions {
/**
* If true, will throw an exception if the file did not exist before `deleteFile()` was called.
* @defaultValue false
*/
throwIfNotExists?: boolean;
}
/**
* The options for {@link FileSystem.updateTimes}
* Both times must be specified.
* @public
*/
export interface IFileSystemUpdateTimeParameters {
/**
* The POSIX epoch time or Date when this was last accessed.
*/
accessedTime: number | Date;
/**
* The POSIX epoch time or Date when this was last modified
*/
modifiedTime: number | Date;
}
/**
* The options for {@link FileSystem.createSymbolicLinkJunction}, {@link FileSystem.createSymbolicLinkFile},
* {@link FileSystem.createSymbolicLinkFolder}, and {@link FileSystem.createHardLink}.
*
* @public
*/
export interface IFileSystemCreateLinkOptions {
/**
* The newly created symbolic link will point to `linkTargetPath` as its target.
*/
linkTargetPath: string;
/**
* The newly created symbolic link will have this path.
*/
newLinkPath: string;
/**
* Specifies what to do if the path to create already exists.
* The default is `AlreadyExistsBehavior.Error`.
*/
alreadyExistsBehavior?: AlreadyExistsBehavior;
}
interface IInternalFileSystemCreateLinkOptions extends IFileSystemCreateLinkOptions {
/**
* Specifies if the link target must exist.
*/
linkTargetMustExist?: boolean;
}
const MOVE_DEFAULT_OPTIONS: Partial<IFileSystemMoveOptions> = {
overwrite: true,
ensureFolderExists: false
};
const READ_FOLDER_DEFAULT_OPTIONS: Partial<IFileSystemReadFolderOptions> = {
absolutePaths: false
};
const WRITE_FILE_DEFAULT_OPTIONS: Partial<IFileSystemWriteFileOptions> = {
ensureFolderExists: false,
convertLineEndings: undefined,
encoding: Encoding.Utf8
};
const APPEND_TO_FILE_DEFAULT_OPTIONS: Partial<IFileSystemWriteFileOptions> = {
...WRITE_FILE_DEFAULT_OPTIONS
};
const READ_FILE_DEFAULT_OPTIONS: Partial<IFileSystemReadFileOptions> = {
encoding: Encoding.Utf8,
convertLineEndings: undefined
};
const COPY_FILE_DEFAULT_OPTIONS: Partial<IFileSystemCopyFileOptions> = {
alreadyExistsBehavior: AlreadyExistsBehavior.Overwrite
};
const COPY_FILES_DEFAULT_OPTIONS: Partial<IFileSystemCopyFilesOptions> = {
alreadyExistsBehavior: AlreadyExistsBehavior.Overwrite
};
const DELETE_FILE_DEFAULT_OPTIONS: Partial<IFileSystemDeleteFileOptions> = {
throwIfNotExists: false
};
/**
* The FileSystem API provides a complete set of recommended operations for interacting with the file system.
*
* @remarks
* We recommend to use this instead of the native `fs` API, because `fs` is a minimal set of low-level
* primitives that must be mapped for each supported operating system. The FileSystem API takes a
* philosophical approach of providing "one obvious way" to do each operation. We also prefer synchronous
* operations except in cases where there would be a clear performance benefit for using async, since synchronous
* code is much easier to read and debug. Also, indiscriminate parallelism has been seen to actually worsen
* performance, versus improving it.
*
* Note that in the documentation, we refer to "filesystem objects", this can be a
* file, folder, symbolic link, hard link, directory junction, etc.
*
* @public
*/
export class FileSystem {
// ===============
// COMMON OPERATIONS
// ===============
/**
* Returns true if the path exists on disk.
* Behind the scenes it uses `fs.existsSync()`.
* @remarks
* There is a debate about the fact that after `fs.existsSync()` returns true,
* the file might be deleted before fs.readSync() is called, which would imply that everybody
* should catch a `readSync()` exception, and nobody should ever use `fs.existsSync()`.
* We find this to be unpersuasive, since "unexceptional exceptions" really hinder the
* break-on-exception debugging experience. Also, throwing/catching is generally slow.
* @param path - The absolute or relative path to the filesystem object.
*/
public static exists(path: string): boolean {
return FileSystem._wrapException(() => {
return fsx.existsSync(path);
});
}
/**
* An async version of {@link FileSystem.exists}.
*/
public static async existsAsync(path: string): Promise<boolean> {
return await FileSystem._wrapExceptionAsync(() => {
return new Promise<boolean>((resolve: (result: boolean) => void) => {
fsx.exists(path, resolve);
});
});
}
/**
* Gets the statistics for a particular filesystem object.
* If the path is a link, this function follows the link and returns statistics about the link target.
* Behind the scenes it uses `fs.statSync()`.
* @param path - The absolute or relative path to the filesystem object.
*/
public static getStatistics(path: string): FileSystemStats {
return FileSystem._wrapException(() => {
return fsx.statSync(path);
});
}
/**
* An async version of {@link FileSystem.getStatistics}.
*/
public static async getStatisticsAsync(path: string): Promise<FileSystemStats> {
return await FileSystem._wrapExceptionAsync(() => {
return fsx.stat(path);
});
}
/**
* Updates the accessed and modified timestamps of the filesystem object referenced by path.
* Behind the scenes it uses `fs.utimesSync()`.
* The caller should specify both times in the `times` parameter.
* @param path - The path of the file that should be modified.
* @param times - The times that the object should be updated to reflect.
*/
public static updateTimes(path: string, times: IFileSystemUpdateTimeParameters): void {
return FileSystem._wrapException(() => {
fsx.utimesSync(path, times.accessedTime, times.modifiedTime);
});
}
/**
* An async version of {@link FileSystem.updateTimes}.
*/
public static async updateTimesAsync(path: string, times: IFileSystemUpdateTimeParameters): Promise<void> {
await FileSystem._wrapExceptionAsync(() => {
// This cast is needed because the fs-extra typings require both parameters
// to have the same type (number or Date), whereas Node.js does not require that.
return fsx.utimes(path, times.accessedTime as number, times.modifiedTime as number);
});
}
/**
* Changes the permissions (i.e. file mode bits) for a filesystem object.
* Behind the scenes it uses `fs.chmodSync()`.
* @param path - The absolute or relative path to the object that should be updated.
* @param modeBits - POSIX-style file mode bits specified using the {@link PosixModeBits} enum
*/
public static changePosixModeBits(path: string, modeBits: PosixModeBits): void {
FileSystem._wrapException(() => {
fs.chmodSync(path, modeBits);
});
}
/**
* An async version of {@link FileSystem.changePosixModeBits}.
*/
public static async changePosixModeBitsAsync(path: string, mode: PosixModeBits): Promise<void> {
await FileSystem._wrapExceptionAsync(() => {
return fsx.chmod(path, mode);
});
}
/**
* Retrieves the permissions (i.e. file mode bits) for a filesystem object.
* Behind the scenes it uses `fs.chmodSync()`.
* @param path - The absolute or relative path to the object that should be updated.
*
* @remarks
* This calls {@link FileSystem.getStatistics} to get the POSIX mode bits.
* If statistics in addition to the mode bits are needed, it is more efficient
* to call {@link FileSystem.getStatistics} directly instead.
*/
public static getPosixModeBits(path: string): PosixModeBits {
return FileSystem._wrapException(() => {
return FileSystem.getStatistics(path).mode;
});
}
/**
* An async version of {@link FileSystem.getPosixModeBits}.
*/
public static async getPosixModeBitsAsync(path: string): Promise<PosixModeBits> {
return await FileSystem._wrapExceptionAsync(async () => {
return (await FileSystem.getStatisticsAsync(path)).mode;
});
}
/**
* Returns a 10-character string representation of a PosixModeBits value similar to what
* would be displayed by a command such as "ls -l" on a POSIX-like operating system.
* @remarks
* For example, `PosixModeBits.AllRead | PosixModeBits.AllWrite` would be formatted as "-rw-rw-rw-".
* @param modeBits - POSIX-style file mode bits specified using the {@link PosixModeBits} enum
*/
public static formatPosixModeBits(modeBits: PosixModeBits): string {
let result: string = '-'; // (later we may add support for additional states such as S_IFDIR or S_ISUID)
result += modeBits & PosixModeBits.UserRead ? 'r' : '-';
result += modeBits & PosixModeBits.UserWrite ? 'w' : '-';
result += modeBits & PosixModeBits.UserExecute ? 'x' : '-';
result += modeBits & PosixModeBits.GroupRead ? 'r' : '-';
result += modeBits & PosixModeBits.GroupWrite ? 'w' : '-';
result += modeBits & PosixModeBits.GroupExecute ? 'x' : '-';
result += modeBits & PosixModeBits.OthersRead ? 'r' : '-';
result += modeBits & PosixModeBits.OthersWrite ? 'w' : '-';
result += modeBits & PosixModeBits.OthersExecute ? 'x' : '-';
return result;
}
/**
* Moves a file. The folder must exist, unless the `ensureFolderExists` option is provided.
* Behind the scenes it uses `fs-extra.moveSync()`
*/
public static move(options: IFileSystemMoveOptions): void {
FileSystem._wrapException(() => {
options = {
...MOVE_DEFAULT_OPTIONS,
...options
};
try {
fsx.moveSync(options.sourcePath, options.destinationPath, { overwrite: options.overwrite });
} catch (error) {
if (options.ensureFolderExists) {
if (!FileSystem.isNotExistError(error as Error)) {
throw error;
}
const folderPath: string = nodeJsPath.dirname(options.destinationPath);
FileSystem.ensureFolder(folderPath);
fsx.moveSync(options.sourcePath, options.destinationPath, { overwrite: options.overwrite });
} else {
throw error;
}
}
});
}
/**
* An async version of {@link FileSystem.move}.
*/
public static async moveAsync(options: IFileSystemMoveOptions): Promise<void> {
await FileSystem._wrapExceptionAsync(async () => {
options = {
...MOVE_DEFAULT_OPTIONS,
...options
};
try {
await fsx.move(options.sourcePath, options.destinationPath, { overwrite: options.overwrite });
} catch (error) {
if (options.ensureFolderExists) {
if (!FileSystem.isNotExistError(error as Error)) {
throw error;
}
const folderPath: string = nodeJsPath.dirname(options.destinationPath);
await FileSystem.ensureFolderAsync(nodeJsPath.dirname(folderPath));
await fsx.move(options.sourcePath, options.destinationPath, { overwrite: options.overwrite });
} else {
throw error;
}
}
});
}
// ===============
// FOLDER OPERATIONS
// ===============
/**
* Recursively creates a folder at a given path.
* Behind the scenes is uses `fs-extra.ensureDirSync()`.
* @remarks
* Throws an exception if anything in the folderPath is not a folder.
* @param folderPath - The absolute or relative path of the folder which should be created.
*/
public static ensureFolder(folderPath: string): void {
FileSystem._wrapException(() => {
fsx.ensureDirSync(folderPath);
});
}
/**
* An async version of {@link FileSystem.ensureFolder}.
*/
public static async ensureFolderAsync(folderPath: string): Promise<void> {
await FileSystem._wrapExceptionAsync(() => {
return fsx.ensureDir(folderPath);
});
}
/**
* Reads the names of folder entries, not including "." or "..".
* Behind the scenes it uses `fs.readdirSync()`.
* @param folderPath - The absolute or relative path to the folder which should be read.
* @param options - Optional settings that can change the behavior. Type: `IReadFolderOptions`
*/
public static readFolderItemNames(folderPath: string, options?: IFileSystemReadFolderOptions): string[] {
return FileSystem._wrapException(() => {
options = {
...READ_FOLDER_DEFAULT_OPTIONS,
...options
};
const fileNames: string[] = fsx.readdirSync(folderPath);
if (options.absolutePaths) {
return fileNames.map((fileName) => nodeJsPath.resolve(folderPath, fileName));
} else {
return fileNames;
}
});
}
/**
* An async version of {@link FileSystem.readFolderItemNames}.
*/
public static async readFolderItemNamesAsync(
folderPath: string,
options?: IFileSystemReadFolderOptions
): Promise<string[]> {
return await FileSystem._wrapExceptionAsync(async () => {
options = {
...READ_FOLDER_DEFAULT_OPTIONS,
...options
};
const fileNames: string[] = await fsx.readdir(folderPath);
if (options.absolutePaths) {
return fileNames.map((fileName) => nodeJsPath.resolve(folderPath, fileName));
} else {
return fileNames;
}
});
}
/**
* Reads the contents of the folder, not including "." or "..", returning objects including the
* entry names and types.
* Behind the scenes it uses `fs.readdirSync()`.
* @param folderPath - The absolute or relative path to the folder which should be read.
* @param options - Optional settings that can change the behavior. Type: `IReadFolderOptions`
*/
public static readFolderItems(folderPath: string, options?: IFileSystemReadFolderOptions): FolderItem[] {
return FileSystem._wrapException(() => {
options = {
...READ_FOLDER_DEFAULT_OPTIONS,
...options
};
const folderEntries: FolderItem[] = fsx.readdirSync(folderPath, { withFileTypes: true });
if (options.absolutePaths) {
return folderEntries.map((folderEntry) => {
folderEntry.name = nodeJsPath.resolve(folderPath, folderEntry.name);
return folderEntry;
});
} else {
return folderEntries;
}
});
}
/**
* An async version of {@link FileSystem.readFolderItems}.
*/
public static async readFolderItemsAsync(
folderPath: string,
options?: IFileSystemReadFolderOptions
): Promise<FolderItem[]> {
return await FileSystem._wrapExceptionAsync(async () => {
options = {
...READ_FOLDER_DEFAULT_OPTIONS,
...options
};
const folderEntries: FolderItem[] = await fsPromises.readdir(folderPath, { withFileTypes: true });
if (options.absolutePaths) {
return folderEntries.map((folderEntry) => {
folderEntry.name = nodeJsPath.resolve(folderPath, folderEntry.name);
return folderEntry;
});
} else {
return folderEntries;
}
});
}
/**
* Deletes a folder, including all of its contents.
* Behind the scenes is uses `fs-extra.removeSync()`.
* @remarks
* Does not throw if the folderPath does not exist.
* @param folderPath - The absolute or relative path to the folder which should be deleted.
*/
public static deleteFolder(folderPath: string): void {
FileSystem._wrapException(() => {
fsx.removeSync(folderPath);
});
}
/**
* An async version of {@link FileSystem.deleteFolder}.
*/
public static async deleteFolderAsync(folderPath: string): Promise<void> {
await FileSystem._wrapExceptionAsync(() => {
return fsx.remove(folderPath);
});
}
/**
* Deletes the content of a folder, but not the folder itself. Also ensures the folder exists.
* Behind the scenes it uses `fs-extra.emptyDirSync()`.
* @remarks
* This is a workaround for a common race condition, where the virus scanner holds a lock on the folder
* for a brief period after it was deleted, causing EBUSY errors for any code that tries to recreate the folder.
* @param folderPath - The absolute or relative path to the folder which should have its contents deleted.
*/
public static ensureEmptyFolder(folderPath: string): void {
FileSystem._wrapException(() => {
fsx.emptyDirSync(folderPath);
});
}
/**
* An async version of {@link FileSystem.ensureEmptyFolder}.
*/
public static async ensureEmptyFolderAsync(folderPath: string): Promise<void> {
await FileSystem._wrapExceptionAsync(() => {
return fsx.emptyDir(folderPath);
});
}
// ===============
// FILE OPERATIONS
// ===============
/**
* Writes a text string to a file on disk, overwriting the file if it already exists.
* Behind the scenes it uses `fs.writeFileSync()`.
* @remarks
* Throws an error if the folder doesn't exist, unless {@link IFileSystemWriteFileOptionsBase.ensureFolderExists}
* is set to `true`.
* @param filePath - The absolute or relative path of the file.
* @param contents - The text that should be written to the file.
* @param options - Optional settings that can change the behavior.
*/
public static writeFile(
filePath: string,
contents: string | Buffer,
options?: IFileSystemWriteFileOptions
): void {
FileSystem._wrapException(() => {
options = {
...WRITE_FILE_DEFAULT_OPTIONS,
...options
};
if (options.convertLineEndings) {
contents = Text.convertTo(contents.toString(), options.convertLineEndings);
}
try {
fsx.writeFileSync(filePath, contents, { encoding: options.encoding });
} catch (error) {
if (options.ensureFolderExists) {
if (!FileSystem.isNotExistError(error as Error)) {
throw error;
}
const folderPath: string = nodeJsPath.dirname(filePath);
FileSystem.ensureFolder(folderPath);
fsx.writeFileSync(filePath, contents, { encoding: options.encoding });
} else {
throw error;
}
}
});
}
/**
* Writes the contents of multiple Uint8Arrays to a file on disk, overwriting the file if it already exists.
* Behind the scenes it uses `fs.writevSync()`.
*
* This API is useful for writing large files efficiently, especially if the input is being concatenated from
* multiple sources.
*
* @remarks
* Throws an error if the folder doesn't exist, unless {@link IFileSystemWriteFileOptionsBase.ensureFolderExists}
* is set to `true`.
* @param filePath - The absolute or relative path of the file.
* @param contents - The content that should be written to the file.
* @param options - Optional settings that can change the behavior.
*/
public static writeBuffersToFile(
filePath: string,
contents: ReadonlyArray<NodeJS.ArrayBufferView>,
options?: IFileSystemWriteBinaryFileOptions
): void {
FileSystem._wrapException(() => {
// Need a mutable copy of the iterable to handle incomplete writes,
// since writev() doesn't take an argument for where to start writing.
const toCopy: NodeJS.ArrayBufferView[] = [...contents];
let fd: number | undefined;
try {
fd = fsx.openSync(filePath, 'w');
} catch (error) {
if (!options?.ensureFolderExists || !FileSystem.isNotExistError(error as Error)) {
throw error;
}
const folderPath: string = nodeJsPath.dirname(filePath);
FileSystem.ensureFolder(folderPath);
fd = fsx.openSync(filePath, 'w');
}
try {
// In practice this loop will have exactly 1 iteration, but the spec allows
// for a writev call to write fewer bytes than requested
while (toCopy.length) {
let bytesWritten: number = fsx.writevSync(fd, toCopy);
let buffersWritten: number = 0;
while (buffersWritten < toCopy.length) {
const bytesInCurrentBuffer: number = toCopy[buffersWritten].byteLength;
if (bytesWritten < bytesInCurrentBuffer) {
// This buffer was partially written.
const currentToCopy: NodeJS.ArrayBufferView = toCopy[buffersWritten];
toCopy[buffersWritten] = new Uint8Array(
currentToCopy.buffer,
currentToCopy.byteOffset + bytesWritten,
currentToCopy.byteLength - bytesWritten
);
break;
}
bytesWritten -= bytesInCurrentBuffer;
buffersWritten++;
}
if (buffersWritten > 0) {
// Avoid cost of shifting the array more than needed.
toCopy.splice(0, buffersWritten);
}
}
} finally {
fsx.closeSync(fd);
}
});
}
/**
* An async version of {@link FileSystem.writeFile}.
*/
public static async writeFileAsync(
filePath: string,
contents: string | Buffer,
options?: IFileSystemWriteFileOptions
): Promise<void> {
await FileSystem._wrapExceptionAsync(async () => {
options = {
...WRITE_FILE_DEFAULT_OPTIONS,
...options
};
if (options.convertLineEndings) {
contents = Text.convertTo(contents.toString(), options.convertLineEndings);
}
try {
await fsx.writeFile(filePath, contents, { encoding: options.encoding });
} catch (error) {
if (options.ensureFolderExists) {
if (!FileSystem.isNotExistError(error as Error)) {
throw error;
}
const folderPath: string = nodeJsPath.dirname(filePath);
await FileSystem.ensureFolderAsync(folderPath);
await fsx.writeFile(filePath, contents, { encoding: options.encoding });
} else {
throw error;
}
}
});
}
/**
* An async version of {@link FileSystem.writeBuffersToFile}.
*/
public static async writeBuffersToFileAsync(
filePath: string,
contents: ReadonlyArray<NodeJS.ArrayBufferView>,
options?: IFileSystemWriteBinaryFileOptions
): Promise<void> {
await FileSystem._wrapExceptionAsync(async () => {
// Need a mutable copy of the iterable to handle incomplete writes,
// since writev() doesn't take an argument for where to start writing.
const toCopy: NodeJS.ArrayBufferView[] = [...contents];
let handle: fsPromises.FileHandle | undefined;
try {
handle = await fsPromises.open(filePath, 'w');
} catch (error) {
if (!options?.ensureFolderExists || !FileSystem.isNotExistError(error as Error)) {
throw error;
}
const folderPath: string = nodeJsPath.dirname(filePath);
await FileSystem.ensureFolderAsync(folderPath);
handle = await fsPromises.open(filePath, 'w');
}
try {
// In practice this loop will have exactly 1 iteration, but the spec allows
// for a writev call to write fewer bytes than requested
while (toCopy.length) {
let bytesWritten: number = (await handle.writev(toCopy)).bytesWritten;
let buffersWritten: number = 0;
while (buffersWritten < toCopy.length) {
const bytesInCurrentBuffer: number = toCopy[buffersWritten].byteLength;
if (bytesWritten < bytesInCurrentBuffer) {
// This buffer was partially written.
const currentToCopy: NodeJS.ArrayBufferView = toCopy[buffersWritten];
toCopy[buffersWritten] = new Uint8Array(
currentToCopy.buffer,
currentToCopy.byteOffset + bytesWritten,
currentToCopy.byteLength - bytesWritten
);
break;
}
bytesWritten -= bytesInCurrentBuffer;
buffersWritten++;
}
if (buffersWritten > 0) {
// Avoid cost of shifting the array more than needed.
toCopy.splice(0, buffersWritten);
}
}
} finally {
await handle.close();
}
});
}
/**
* Writes a text string to a file on disk, appending to the file if it already exists.
* Behind the scenes it uses `fs.appendFileSync()`.
* @remarks
* Throws an error if the folder doesn't exist, unless {@link IFileSystemWriteFileOptionsBase.ensureFolderExists}
* is set to `true`.
* @param filePath - The absolute or relative path of the file.
* @param contents - The text that should be written to the file.
* @param options - Optional settings that can change the behavior.
*/
public static appendToFile(
filePath: string,
contents: string | Buffer,
options?: IFileSystemWriteFileOptions
): void {
FileSystem._wrapException(() => {
options = {
...APPEND_TO_FILE_DEFAULT_OPTIONS,
...options
};