Skip to content

Commit 2b34212

Browse files
Factor out FileSystemUtils (#9808)
(for #8995) This is one of the last parts to be un-reverted for the FS changes.
1 parent fe3d415 commit 2b34212

File tree

6 files changed

+1407
-169
lines changed

6 files changed

+1407
-169
lines changed

src/client/common/platform/fileSystem.ts

Lines changed: 187 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,22 @@ import { createDeferred } from '../utils/async';
1313
import { isFileNotFoundError, isNoPermissionsError } from './errors';
1414
import { FileSystemPaths, FileSystemPathUtils } from './fs-paths';
1515
import { TemporaryFileSystem } from './fs-temp';
16-
// prettier-ignore
1716
import {
18-
FileStat, FileType,
19-
IFileSystem, IFileSystemPaths, IRawFileSystem,
20-
ReadStream, TemporaryFile, WriteStream
17+
FileStat,
18+
FileType,
19+
IFileSystem,
20+
IFileSystemPaths,
21+
IFileSystemPathUtils,
22+
IFileSystemUtils,
23+
IRawFileSystem,
24+
ITempFileSystem,
25+
ReadStream,
26+
TemporaryFile,
27+
WriteStream
2128
} from './types';
2229

2330
const ENCODING: string = 'utf8';
2431

25-
const globAsync = promisify(glob);
26-
2732
// This helper function determines the file type of the given stats
2833
// object. The type follows the convention of node's fs module, where
2934
// a file has exactly one type. Symlinks are not resolved.
@@ -272,62 +277,51 @@ export class RawFileSystem implements IRawFileSystem {
272277
}
273278

274279
//==========================================
275-
// filesystem "utils" (& legacy aliases)
280+
// filesystem "utils"
276281

277-
@injectable()
278-
export class FileSystem implements IFileSystem {
279-
// We expose this for the sake of functional tests that do not have
280-
// access to the actual "vscode" namespace.
281-
protected raw: RawFileSystem;
282-
private readonly paths: IFileSystemPaths;
283-
private readonly pathUtils: FileSystemPathUtils;
284-
private readonly tmp: TemporaryFileSystem;
285-
constructor() {
286-
this.paths = FileSystemPaths.withDefaults();
287-
this.pathUtils = FileSystemPathUtils.withDefaults(this.paths);
288-
this.tmp = TemporaryFileSystem.withDefaults();
289-
this.raw = RawFileSystem.withDefaults(this.paths);
290-
}
291-
292-
//=================================
293-
// path-related
294-
295-
public get directorySeparatorChar(): string {
296-
return this.paths.sep;
297-
}
298-
299-
public arePathsSame(path1: string, path2: string): boolean {
300-
return this.pathUtils.arePathsSame(path1, path2);
301-
}
302-
303-
//=================================
304-
// "raw" operations
305-
306-
public async stat(filename: string): Promise<FileStat> {
307-
return this.raw.stat(filename);
308-
}
309-
310-
public async lstat(filename: string): Promise<FileStat> {
311-
return this.raw.lstat(filename);
312-
}
282+
// This is the parts of the 'fs-extra' module that we use in RawFileSystem.
283+
interface IFSExtraForUtils {
284+
open(path: string, flags: string | number, mode?: string | number | null): Promise<number>;
285+
close(fd: number): Promise<void>;
286+
unlink(path: string): Promise<void>;
287+
existsSync(path: string): boolean;
288+
}
313289

314-
public async readFile(filePath: string): Promise<string> {
315-
return this.raw.readText(filePath);
316-
}
317-
public readFileSync(filePath: string): string {
318-
return this.raw.readTextSync(filePath);
319-
}
320-
public async readData(filePath: string): Promise<Buffer> {
321-
return this.raw.readData(filePath);
290+
// High-level filesystem operations used by the extension.
291+
export class FileSystemUtils implements IFileSystemUtils {
292+
constructor(
293+
public readonly raw: IRawFileSystem,
294+
public readonly pathUtils: IFileSystemPathUtils,
295+
public readonly paths: IFileSystemPaths,
296+
public readonly tmp: ITempFileSystem,
297+
// tslint:disable-next-line:no-shadowed-variable
298+
private readonly fs: IFSExtraForUtils,
299+
private readonly getHash: (data: string) => string,
300+
private readonly globFiles: (pat: string, options?: { cwd: string }) => Promise<string[]>
301+
) {}
302+
// Create a new object using common-case default values.
303+
public static withDefaults(
304+
raw?: IRawFileSystem,
305+
pathUtils?: IFileSystemPathUtils,
306+
tmp?: ITempFileSystem,
307+
fsDeps?: IFSExtraForUtils,
308+
getHash?: (data: string) => string,
309+
globFiles?: (pat: string, options?: { cwd: string }) => Promise<string[]>
310+
): FileSystemUtils {
311+
pathUtils = pathUtils || FileSystemPathUtils.withDefaults();
312+
return new FileSystemUtils(
313+
raw || RawFileSystem.withDefaults(pathUtils.paths),
314+
pathUtils,
315+
pathUtils.paths,
316+
tmp || TemporaryFileSystem.withDefaults(),
317+
fsDeps || fs,
318+
getHash || getHashString,
319+
globFiles || promisify(glob)
320+
);
322321
}
323322

324-
public async writeFile(filePath: string, text: string, _options: string | fs.WriteFileOptions = { encoding: 'utf8' }): Promise<void> {
325-
// tslint:disable-next-line:no-suspicious-comment
326-
// TODO (GH-8542) For now we ignore the options, since all call
327-
// sites already match the defaults. Later we will fix the call
328-
// sites.
329-
return this.raw.writeText(filePath, text);
330-
}
323+
//****************************
324+
// aliases
331325

332326
public async createDirectory(directoryPath: string): Promise<void> {
333327
return this.raw.mkdirp(directoryPath);
@@ -337,48 +331,12 @@ export class FileSystem implements IFileSystem {
337331
return this.raw.rmtree(directoryPath);
338332
}
339333

340-
public async listdir(dirname: string): Promise<[string, FileType][]> {
341-
// prettier-ignore
342-
return this.raw.listdir(dirname)
343-
.catch(async err => {
344-
// We're only preserving pre-existng behavior here...
345-
if (!(await this.pathExists(dirname))) {
346-
return [];
347-
}
348-
throw err; // re-throw
349-
});
350-
}
351-
352-
public async appendFile(filename: string, text: string): Promise<void> {
353-
return this.raw.appendText(filename, text);
354-
}
355-
356-
public async copyFile(src: string, dest: string): Promise<void> {
357-
return this.raw.copyFile(src, dest);
358-
}
359-
360334
public async deleteFile(filename: string): Promise<void> {
361335
return this.raw.rmfile(filename);
362336
}
363337

364-
public async chmod(filePath: string, mode: string | number): Promise<void> {
365-
return this.raw.chmod(filePath, mode);
366-
}
367-
368-
public async move(src: string, tgt: string) {
369-
await this.raw.move(src, tgt);
370-
}
371-
372-
public createReadStream(filePath: string): ReadStream {
373-
return this.raw.createReadStream(filePath);
374-
}
375-
376-
public createWriteStream(filePath: string): WriteStream {
377-
return this.raw.createWriteStream(filePath);
378-
}
379-
380-
//=================================
381-
// utils
338+
//****************************
339+
// helpers
382340

383341
// prettier-ignore
384342
public async pathExists(
@@ -409,13 +367,21 @@ export class FileSystem implements IFileSystem {
409367
public async fileExists(filename: string): Promise<boolean> {
410368
return this.pathExists(filename, FileType.File);
411369
}
412-
public fileExistsSync(filePath: string): boolean {
413-
return fs.existsSync(filePath);
414-
}
415370
public async directoryExists(dirname: string): Promise<boolean> {
416371
return this.pathExists(dirname, FileType.Directory);
417372
}
418373

374+
public async listdir(dirname: string): Promise<[string, FileType][]> {
375+
// prettier-ignore
376+
return this.raw.listdir(dirname)
377+
.catch(async err => {
378+
// We're only preserving pre-existng behavior here...
379+
if (!(await this.pathExists(dirname))) {
380+
return [];
381+
}
382+
throw err; // re-throw
383+
});
384+
}
419385
public async getSubDirectories(dirname: string): Promise<string[]> {
420386
// prettier-ignore
421387
return filterByFileType(
@@ -431,11 +397,31 @@ export class FileSystem implements IFileSystem {
431397
).map(([filename, _fileType]) => filename);
432398
}
433399

400+
public async isDirReadonly(dirname: string): Promise<boolean> {
401+
const filePath = `${dirname}${this.paths.sep}___vscpTest___`;
402+
const flags = fs.constants.O_CREAT | fs.constants.O_RDWR;
403+
let fd: number;
404+
try {
405+
fd = await this.fs.open(filePath, flags);
406+
} catch (err) {
407+
if (isNoPermissionsError(err)) {
408+
return true;
409+
}
410+
throw err; // re-throw
411+
}
412+
// Clean resources in the background.
413+
this.fs
414+
.close(fd)
415+
.finally(() => this.fs.unlink(filePath))
416+
.ignoreErrors();
417+
return false;
418+
}
419+
434420
public async getFileHash(filename: string): Promise<string> {
435421
// The reason for lstat rather than stat is not clear...
436422
const stat = await this.raw.lstat(filename);
437423
const data = `${stat.ctime}-${stat.mtime}`;
438-
return getHashString(data);
424+
return this.getHash(data);
439425
}
440426

441427
public async search(globPattern: string, cwd?: string): Promise<string[]> {
@@ -444,42 +430,118 @@ export class FileSystem implements IFileSystem {
444430
const options = {
445431
cwd: cwd
446432
};
447-
found = await globAsync(globPattern, options);
433+
found = await this.globFiles(globPattern, options);
448434
} else {
449-
found = await globAsync(globPattern);
435+
found = await this.globFiles(globPattern);
450436
}
451437
return Array.isArray(found) ? found : [];
452438
}
453439

454-
public createTemporaryFile(extension: string): Promise<TemporaryFile> {
455-
return this.tmp.createFile(extension);
456-
}
440+
//****************************
441+
// helpers (non-async)
457442

458-
public async isDirReadonly(dirname: string): Promise<boolean> {
459-
const filePath = `${dirname}${this.paths.sep}___vscpTest___`;
460-
const flags = fs.constants.O_CREAT | fs.constants.O_RDWR;
461-
let fd: number;
462-
try {
463-
fd = await fs.open(filePath, flags);
464-
// Clean resources in the background.
465-
fs.close(fd)
466-
.finally(() => fs.unlink(filePath))
467-
.ignoreErrors();
468-
} catch (err) {
469-
if (isNoPermissionsError(err)) {
470-
return true;
471-
}
472-
throw err; // re-throw
473-
}
474-
return false;
443+
public fileExistsSync(filePath: string): boolean {
444+
return this.fs.existsSync(filePath);
475445
}
476446
}
477447

478448
// We *could* use ICryptoUtils, but it's a bit overkill, issue tracked
479449
// in https://github.com/microsoft/vscode-python/issues/8438.
480450
function getHashString(data: string): string {
481-
// prettier-ignore
482-
const hash = createHash('sha512')
483-
.update(data);
451+
const hash = createHash('sha512');
452+
hash.update(data);
484453
return hash.digest('hex');
485454
}
455+
456+
//==========================================
457+
// legacy filesystem API
458+
459+
// more aliases (to cause less churn)
460+
@injectable()
461+
export class FileSystem implements IFileSystem {
462+
// We expose this for the sake of functional tests that do not have
463+
// access to the actual "vscode" namespace.
464+
protected utils: FileSystemUtils;
465+
constructor() {
466+
this.utils = FileSystemUtils.withDefaults();
467+
}
468+
469+
public get directorySeparatorChar(): string {
470+
return this.utils.paths.sep;
471+
}
472+
public arePathsSame(path1: string, path2: string): boolean {
473+
return this.utils.pathUtils.arePathsSame(path1, path2);
474+
}
475+
public async stat(filename: string): Promise<FileStat> {
476+
return this.utils.raw.stat(filename);
477+
}
478+
public async createDirectory(dirname: string): Promise<void> {
479+
return this.utils.createDirectory(dirname);
480+
}
481+
public async deleteDirectory(dirname: string): Promise<void> {
482+
return this.utils.deleteDirectory(dirname);
483+
}
484+
public async listdir(dirname: string): Promise<[string, FileType][]> {
485+
return this.utils.listdir(dirname);
486+
}
487+
public async readFile(filePath: string): Promise<string> {
488+
return this.utils.raw.readText(filePath);
489+
}
490+
public async readData(filePath: string): Promise<Buffer> {
491+
return this.utils.raw.readData(filePath);
492+
}
493+
public async writeFile(filename: string, data: {}): Promise<void> {
494+
return this.utils.raw.writeText(filename, data);
495+
}
496+
public async appendFile(filename: string, text: string): Promise<void> {
497+
return this.utils.raw.appendText(filename, text);
498+
}
499+
public async copyFile(src: string, dest: string): Promise<void> {
500+
return this.utils.raw.copyFile(src, dest);
501+
}
502+
public async deleteFile(filename: string): Promise<void> {
503+
return this.utils.deleteFile(filename);
504+
}
505+
public async chmod(filename: string, mode: string): Promise<void> {
506+
return this.utils.raw.chmod(filename, mode);
507+
}
508+
public async move(src: string, tgt: string) {
509+
await this.utils.raw.move(src, tgt);
510+
}
511+
public readFileSync(filePath: string): string {
512+
return this.utils.raw.readTextSync(filePath);
513+
}
514+
public createReadStream(filePath: string): ReadStream {
515+
return this.utils.raw.createReadStream(filePath);
516+
}
517+
public createWriteStream(filePath: string): WriteStream {
518+
return this.utils.raw.createWriteStream(filePath);
519+
}
520+
public async fileExists(filename: string): Promise<boolean> {
521+
return this.utils.fileExists(filename);
522+
}
523+
public fileExistsSync(filename: string): boolean {
524+
return this.utils.fileExistsSync(filename);
525+
}
526+
public async directoryExists(dirname: string): Promise<boolean> {
527+
return this.utils.directoryExists(dirname);
528+
}
529+
public async getSubDirectories(dirname: string): Promise<string[]> {
530+
return this.utils.getSubDirectories(dirname);
531+
}
532+
public async getFiles(dirname: string): Promise<string[]> {
533+
return this.utils.getFiles(dirname);
534+
}
535+
public async getFileHash(filename: string): Promise<string> {
536+
return this.utils.getFileHash(filename);
537+
}
538+
public async search(globPattern: string, cwd?: string): Promise<string[]> {
539+
return this.utils.search(globPattern, cwd);
540+
}
541+
public async createTemporaryFile(suffix: string): Promise<TemporaryFile> {
542+
return this.utils.tmp.createFile(suffix);
543+
}
544+
public async isDirReadonly(dirname: string): Promise<boolean> {
545+
return this.utils.isDirReadonly(dirname);
546+
}
547+
}

0 commit comments

Comments
 (0)