Skip to content

Commit a2cefcc

Browse files
author
Eric Snow
authored
Consolidate some filesystem utils. (microsoft#15389)
As part of the work on improving isStandardPythonBinary(), we make use of several filesystem-related utilities. They are encapsulated here and will be used in a succeeding PR.
1 parent 48bec93 commit a2cefcc

5 files changed

Lines changed: 155 additions & 67 deletions

File tree

.eslintignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,6 @@ src/client/common/platform/errors.ts
513513
src/client/common/platform/fs-temp.ts
514514
src/client/common/platform/fs-paths.ts
515515
src/client/common/platform/platformService.ts
516-
src/client/common/platform/types.ts
517516
src/client/common/platform/registry.ts
518517
src/client/common/platform/pathUtils.ts
519518
src/client/common/persistentState.ts

src/client/common/platform/fileSystem.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { promisify } from 'util';
1212
import * as vscode from 'vscode';
1313
import '../extensions';
1414
import { traceError } from '../logger';
15+
import { convertFileType } from '../utils/filesystem';
1516
import { createDirNotEmptyError, isFileExistsError, isFileNotFoundError, isNoPermissionsError } from './errors';
1617
import { FileSystemPaths, FileSystemPathUtils } from './fs-paths';
1718
import { TemporaryFileSystem } from './fs-temp';
@@ -31,30 +32,6 @@ import {
3132

3233
const ENCODING = 'utf8';
3334

34-
interface IKnowsFileType {
35-
isFile(): boolean;
36-
isDirectory(): boolean;
37-
isSymbolicLink(): boolean;
38-
}
39-
40-
// This helper function determines the file type of the given stats
41-
// object. The type follows the convention of node's fs module, where
42-
// a file has exactly one type. Symlinks are not resolved.
43-
export function convertFileType(info: IKnowsFileType): FileType {
44-
if (info.isFile()) {
45-
return FileType.File;
46-
}
47-
if (info.isDirectory()) {
48-
return FileType.Directory;
49-
}
50-
if (info.isSymbolicLink()) {
51-
// The caller is responsible for combining this ("logical or")
52-
// with File or Directory as necessary.
53-
return FileType.SymbolicLink;
54-
}
55-
return FileType.Unknown;
56-
}
57-
5835
export function convertStat(old: fs.Stats, filetype: FileType): FileStat {
5936
return {
6037
type: filetype,
@@ -550,7 +527,7 @@ export class FileSystem implements IFileSystem {
550527
}
551528

552529
// eslint-disable-next-line @typescript-eslint/ban-types
553-
public async writeFile(filename: string, data: {}): Promise<void> {
530+
public async writeFile(filename: string, data: string | Buffer): Promise<void> {
554531
return this.utils.raw.writeText(filename, data);
555532
}
556533

src/client/common/platform/types.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { SemVer } from 'semver';
77
import * as vscode from 'vscode';
88
import { Architecture, OSType } from '../utils/platform';
99

10-
//===========================
10+
//= ==========================
1111
// registry
1212

1313
export enum RegistryHive {
@@ -21,7 +21,7 @@ export interface IRegistry {
2121
getValue(key: string, hive: RegistryHive, arch?: Architecture, name?: string): Promise<string | undefined | null>;
2222
}
2323

24-
//===========================
24+
//= ==========================
2525
// platform
2626

2727
export const IsWindows = Symbol('IS_WINDOWS');
@@ -41,7 +41,7 @@ export interface IPlatformService {
4141
getVersion(): Promise<SemVer>;
4242
}
4343

44-
//===========================
44+
//= ==========================
4545
// temp FS
4646

4747
export type TemporaryFile = { filePath: string } & vscode.Disposable;
@@ -51,7 +51,7 @@ export interface ITempFileSystem {
5151
createFile(suffix: string, mode?: number): Promise<TemporaryFile>;
5252
}
5353

54-
//===========================
54+
//= ==========================
5555
// FS paths
5656

5757
// The low-level file path operations used by the extension.
@@ -87,19 +87,15 @@ export interface IFileSystemPathUtils {
8787
getDisplayName(pathValue: string, cwd?: string): string;
8888
}
8989

90-
//===========================
90+
//= ==========================
9191
// filesystem operations
9292

93+
// We could use FileType from utils/filesystem.ts, but it's simpler this way.
9394
export import FileType = vscode.FileType;
9495
export import FileStat = vscode.FileStat;
9596
export type ReadStream = fs.ReadStream;
9697
export type WriteStream = fs.WriteStream;
9798

98-
export type DirEntry = {
99-
filename: string;
100-
filetype: FileType;
101-
};
102-
10399
// The low-level filesystem operations on which the extension depends.
104100
export interface IRawFileSystem {
105101
// Get information about a file (resolve symlinks).
@@ -111,23 +107,23 @@ export interface IRawFileSystem {
111107
// Move the file to a different location (and/or rename it).
112108
move(src: string, tgt: string): Promise<void>;
113109

114-
//***********************
110+
//* **********************
115111
// files
116112

117113
// Return the raw bytes of the given file.
118114
readData(filename: string): Promise<Buffer>;
119115
// Return the text of the given file (decoded from UTF-8).
120116
readText(filename: string): Promise<string>;
121117
// Write the given text to the file (UTF-8 encoded).
122-
writeText(filename: string, data: {}): Promise<void>;
118+
writeText(filename: string, data: string | Buffer): Promise<void>;
123119
// Write the given text to the end of the file (UTF-8 encoded).
124120
appendText(filename: string, text: string): Promise<void>;
125121
// Copy a file.
126122
copyFile(src: string, dest: string): Promise<void>;
127123
// Delete a file.
128124
rmfile(filename: string): Promise<void>;
129125

130-
//***********************
126+
//* **********************
131127
// directories
132128

133129
// Create the directory and any missing parent directories.
@@ -139,7 +135,7 @@ export interface IRawFileSystem {
139135
// Return the contents of the directory.
140136
listdir(dirname: string): Promise<[string, FileType][]>;
141137

142-
//***********************
138+
//* **********************
143139
// not async
144140

145141
// Get information about a file (resolve symlinks).
@@ -159,14 +155,14 @@ export interface IFileSystemUtils {
159155
readonly pathUtils: IFileSystemPathUtils;
160156
readonly tmp: ITempFileSystem;
161157

162-
//***********************
158+
//* **********************
163159
// aliases
164160

165161
createDirectory(dirname: string): Promise<void>;
166162
deleteDirectory(dirname: string): Promise<void>;
167163
deleteFile(filename: string): Promise<void>;
168164

169-
//***********************
165+
//* **********************
170166
// helpers
171167

172168
// Determine if the file exists, optionally requiring the type.
@@ -188,7 +184,7 @@ export interface IFileSystemUtils {
188184
// Get the paths of all files matching the pattern.
189185
search(globPattern: string): Promise<string[]>;
190186

191-
//***********************
187+
//* **********************
192188
// helpers (non-async)
193189

194190
fileExistsSync(path: string): boolean;

src/client/common/utils/filesystem.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
import * as fs from 'fs';
45
import * as path from 'path';
6+
import * as vscode from 'vscode';
7+
import { logError } from '../../logging';
58
import { getOSType, OSType } from './platform';
69

710
/**
@@ -28,3 +31,139 @@ export function areSameFilename(filename1: string, filename2: string): boolean {
2831
const norm2 = normalizeFilename(filename2);
2932
return norm1 === norm2;
3033
}
34+
35+
export import FileType = vscode.FileType;
36+
37+
export type DirEntry = {
38+
filename: string;
39+
filetype: FileType;
40+
};
41+
42+
interface IKnowsFileType {
43+
isFile(): boolean;
44+
isDirectory(): boolean;
45+
isSymbolicLink(): boolean;
46+
}
47+
48+
// This helper function determines the file type of the given stats
49+
// object. The type follows the convention of node's fs module, where
50+
// a file has exactly one type. Symlinks are not resolved.
51+
export function convertFileType(info: IKnowsFileType): FileType {
52+
if (info.isFile()) {
53+
return FileType.File;
54+
}
55+
if (info.isDirectory()) {
56+
return FileType.Directory;
57+
}
58+
if (info.isSymbolicLink()) {
59+
// The caller is responsible for combining this ("logical or")
60+
// with File or Directory as necessary.
61+
return FileType.SymbolicLink;
62+
}
63+
return FileType.Unknown;
64+
}
65+
66+
/**
67+
* Identify the file type for the given file.
68+
*/
69+
export async function getFileType(
70+
filename: string,
71+
opts: {
72+
ignoreErrors: boolean;
73+
} = { ignoreErrors: true },
74+
): Promise<FileType | undefined> {
75+
let stat: fs.Stats;
76+
try {
77+
stat = await fs.promises.lstat(filename);
78+
} catch (err) {
79+
if (err.code === 'ENOENT') {
80+
return undefined;
81+
}
82+
if (opts.ignoreErrors) {
83+
logError(`lstat() failed for "${filename}" (${err})`);
84+
return FileType.Unknown;
85+
}
86+
throw err; // re-throw
87+
}
88+
return convertFileType(stat);
89+
}
90+
91+
function normalizeFileTypes(filetypes: FileType | FileType[] | undefined): FileType[] | undefined {
92+
if (filetypes === undefined) {
93+
return undefined;
94+
}
95+
if (Array.isArray(filetypes)) {
96+
if (filetypes.length === 0) {
97+
return undefined;
98+
}
99+
return filetypes;
100+
}
101+
return [filetypes];
102+
}
103+
104+
async function resolveFile(
105+
file: string | DirEntry,
106+
opts: {
107+
ensure?: boolean;
108+
onMissing?: FileType;
109+
} = {},
110+
): Promise<DirEntry | undefined> {
111+
let filename: string;
112+
if (typeof file !== 'string') {
113+
if (!opts.ensure) {
114+
if (opts.onMissing === undefined) {
115+
return file;
116+
}
117+
// At least make sure it exists.
118+
if ((await getFileType(file.filename)) !== undefined) {
119+
return file;
120+
}
121+
}
122+
filename = file.filename;
123+
} else {
124+
filename = file;
125+
}
126+
127+
const filetype = (await getFileType(filename)) || opts.onMissing;
128+
if (filetype === undefined) {
129+
return undefined;
130+
}
131+
return { filename, filetype };
132+
}
133+
134+
type FileFilterFunc = (file: string | DirEntry) => Promise<boolean>;
135+
136+
export function getFileFilter(
137+
opts: {
138+
ignoreMissing?: boolean;
139+
ignoreFileType?: FileType | FileType[];
140+
ensureEntry?: boolean;
141+
} = {
142+
ignoreMissing: true,
143+
},
144+
): FileFilterFunc | undefined {
145+
const ignoreFileType = normalizeFileTypes(opts.ignoreFileType);
146+
147+
if (!opts.ignoreMissing && !ignoreFileType) {
148+
// Do not filter.
149+
return undefined;
150+
}
151+
152+
async function filterFile(file: string | DirEntry): Promise<boolean> {
153+
let entry = await resolveFile(file, { ensure: opts.ensureEntry });
154+
if (!entry) {
155+
if (opts.ignoreMissing) {
156+
return false;
157+
}
158+
const filename = typeof file === 'string' ? file : file.filename;
159+
entry = { filename, filetype: FileType.Unknown };
160+
}
161+
if (ignoreFileType) {
162+
if (ignoreFileType.includes(entry!.filetype)) {
163+
return false;
164+
}
165+
}
166+
return true;
167+
}
168+
return filterFile;
169+
}

src/client/pythonEnvironments/common/commonUtils.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
import * as fs from 'fs';
55
import * as path from 'path';
6-
import { convertFileType } from '../../common/platform/fileSystem';
7-
import { DirEntry, FileType } from '../../common/platform/types';
6+
import { convertFileType, DirEntry, FileType, getFileType } from '../../common/utils/filesystem';
87
import { getOSType, OSType } from '../../common/utils/platform';
98
import { logError } from '../../logging';
109
import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../base/info';
@@ -172,28 +171,6 @@ async function readDirEntries(
172171
return entries;
173172
}
174173

175-
async function getFileType(
176-
filename: string,
177-
opts: {
178-
ignoreErrors: boolean;
179-
} = { ignoreErrors: true },
180-
): Promise<FileType | undefined> {
181-
let stat: fs.Stats;
182-
try {
183-
stat = await fs.promises.lstat(filename);
184-
} catch (err) {
185-
if (err.code === 'ENOENT') {
186-
return undefined;
187-
}
188-
if (opts.ignoreErrors) {
189-
logError(`lstat() failed for "${filename}" (${err})`);
190-
return FileType.Unknown;
191-
}
192-
throw err; // re-throw
193-
}
194-
return convertFileType(stat);
195-
}
196-
197174
function matchFile(
198175
filename: string,
199176
filterFile: FileFilterFunc | undefined,

0 commit comments

Comments
 (0)