Skip to content

Commit 5e94ba4

Browse files
Use the new API for mkdirp().
1 parent 500fb5e commit 5e94ba4

File tree

6 files changed

+150
-60
lines changed

6 files changed

+150
-60
lines changed

src/client/common/platform/fileSystem.ts

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ import {
2222

2323
const ENCODING: string = 'utf8';
2424

25+
const FILE_NOT_FOUND = vscode.FileSystemError.FileNotFound().name;
26+
const FILE_EXISTS = vscode.FileSystemError.FileExists().name;
27+
28+
function isFileNotFoundError(err: Error): boolean {
29+
return err.name === FILE_NOT_FOUND;
30+
}
31+
32+
function isFileExistsError(err: Error): boolean {
33+
return err.name === FILE_EXISTS;
34+
}
35+
2536
function convertFileStat(stat: fsextra.Stats): FileStat {
2637
let fileType = FileType.Unknown;
2738
if (stat.isFile()) {
@@ -39,10 +50,38 @@ function convertFileStat(stat: fsextra.Stats): FileStat {
3950
};
4051
}
4152

53+
interface INodePath {
54+
join(...filenames: string[]): string;
55+
dirname(filename: string): string;
56+
normalize(filename: string): string;
57+
}
58+
59+
// Eventually we will merge PathUtils into FileSystemPath.
60+
61+
export class FileSystemPath implements IFileSystemPath {
62+
constructor(
63+
private readonly isWindows = (getOSType() === OSType.Windows),
64+
private readonly raw: INodePath = fspath
65+
) { }
66+
67+
public join(...filenames: string[]): string {
68+
return this.raw.join(...filenames);
69+
}
70+
71+
public dirname(filename: string): string {
72+
return this.raw.dirname(filename);
73+
}
74+
75+
public normCase(filename: string): string {
76+
filename = this.raw.normalize(filename);
77+
return this.isWindows ? filename.toUpperCase() : filename;
78+
}
79+
}
80+
4281
// This is the parts of the vscode.workspace.fs API that we use here.
4382
interface INewAPI {
4483
copy(source: vscode.Uri, target: vscode.Uri, options?: {overwrite: boolean}): Thenable<void>;
45-
//createDirectory(uri: vscode.Uri): Thenable<void>;
84+
createDirectory(uri: vscode.Uri): Thenable<void>;
4685
delete(uri: vscode.Uri, options?: {recursive: boolean; useTrash: boolean}): Thenable<void>;
4786
readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>;
4887
readFile(uri: vscode.Uri): Thenable<Uint8Array>;
@@ -64,18 +103,22 @@ interface IRawFS {
64103
interface IRawFSExtra {
65104
chmod(filePath: string, mode: string | number): Promise<void>;
66105
lstat(filename: string): Promise<fsextra.Stats>;
67-
mkdirp(dirname: string): Promise<void>;
68106

69107
// non-async
70108
statSync(filename: string): fsextra.Stats;
71109
readFileSync(path: string, encoding: string): string;
72110
}
73111

112+
interface IRawPath {
113+
dirname(filename: string): string;
114+
}
115+
74116
// Later we will drop "FileSystem", switching usage to
75117
// "FileSystemUtils" and then rename "RawFileSystem" to "FileSystem".
76118

77119
export class RawFileSystem implements IRawFileSystem {
78120
constructor(
121+
private readonly path: IRawPath = new FileSystemPath(),
79122
private readonly newapi: INewAPI = vscode.workspace.fs,
80123
private readonly nodefs: IRawFS = fs,
81124
private readonly fsExtra: IRawFSExtra = fsextra
@@ -123,6 +166,37 @@ export class RawFileSystem implements IRawFileSystem {
123166
return this.newapi.readDirectory(uri);
124167
}
125168

169+
public async mkdirp(dirname: string): Promise<void> {
170+
const stack = [dirname];
171+
while (stack.length > 0) {
172+
const current = stack.pop() || '';
173+
const uri = vscode.Uri.file(current);
174+
try {
175+
await this.newapi.createDirectory(uri);
176+
} catch (err) {
177+
if (isFileExistsError(err)) {
178+
// already done!
179+
return;
180+
}
181+
// According to the docs, FileNotFound means the parent
182+
// does not exist.
183+
// See: https://code.visualstudio.com/api/references/vscode-api#FileSystemProvider
184+
if (!isFileNotFoundError(err)) {
185+
// Fail for anything else.
186+
throw err; // re-throw
187+
}
188+
// Try creating the parent first.
189+
const parent = this.path.dirname(current);
190+
if (parent === '' || parent === current) {
191+
// This shouldn't ever happen.
192+
throw err;
193+
}
194+
stack.push(current);
195+
stack.push(parent);
196+
}
197+
}
198+
}
199+
126200
public async copyFile(src: string, dest: string): Promise<void> {
127201
const srcURI = vscode.Uri.file(src);
128202
const destURI = vscode.Uri.file(dest);
@@ -134,10 +208,6 @@ export class RawFileSystem implements IRawFileSystem {
134208
//****************************
135209
// fs-extra
136210

137-
public async mkdirp(dirname: string): Promise<void> {
138-
return this.fsExtra.mkdirp(dirname);
139-
}
140-
141211
public async chmod(filename: string, mode: string | number): Promise<void> {
142212
return this.fsExtra.chmod(filename, mode);
143213
}
@@ -186,29 +256,6 @@ export class RawFileSystem implements IRawFileSystem {
186256
}
187257
}
188258

189-
interface INodePath {
190-
join(...filenames: string[]): string;
191-
normalize(filename: string): string;
192-
}
193-
194-
// Eventually we will merge PathUtils into FileSystemPath.
195-
196-
export class FileSystemPath implements IFileSystemPath {
197-
constructor(
198-
private readonly isWindows = (getOSType() === OSType.Windows),
199-
private readonly raw: INodePath = fspath
200-
) { }
201-
202-
public join(...filenames: string[]): string {
203-
return this.raw.join(...filenames);
204-
}
205-
206-
public normCase(filename: string): string {
207-
filename = this.raw.normalize(filename);
208-
return this.isWindows ? filename.toUpperCase() : filename;
209-
}
210-
}
211-
212259
// We *could* use ICryptUtils, but it's a bit overkill.
213260
function getHashString(data: string): string {
214261
const hash = createHash('sha512')

src/client/common/platform/types.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export type TemporaryDirectory = vscode.Disposable & {
4040
path: string;
4141
};
4242

43+
// Eventually we will merge IPathUtils into IFileSystemPath.
44+
45+
export interface IFileSystemPath {
46+
join(...filenames: string[]): string;
47+
dirname(filename: string): string;
48+
normCase(filename: string): string;
49+
}
50+
4351
export import FileType = vscode.FileType;
4452
export type FileStat = vscode.FileStat;
4553
export type WriteStream = fs.WriteStream;
@@ -67,13 +75,6 @@ export interface IRawFileSystem {
6775
createWriteStream(filename: string): WriteStream;
6876
}
6977

70-
// Eventually we will merge IPathUtils into IFileSystemPath.
71-
72-
export interface IFileSystemPath {
73-
join(...filenames: string[]): string;
74-
normCase(filename: string): string;
75-
}
76-
7778
export const IFileSystemUtils = Symbol('IFileSystemUtils');
7879
export interface IFileSystemUtils {
7980
raw: IRawFileSystem;

src/test/common/platform/filesystem.functional.test.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -107,28 +107,6 @@ suite('Raw FileSystem', () => {
107107
await fix.cleanUp();
108108
});
109109

110-
suite('mkdirp', () => {
111-
test('creates the directory and all missing parents', async () => {
112-
await fix.createDirectory('x');
113-
// x/y, x/y/z, and x/y/z/spam are all missing.
114-
const dirname = await fix.resolve('x/y/z/spam', false);
115-
await ensureDoesNotExist(dirname);
116-
117-
await filesystem.mkdirp(dirname);
118-
119-
await fsextra.stat(dirname); // This should not fail.
120-
});
121-
122-
test('works if the directory already exists', async () => {
123-
const dirname = await fix.createDirectory('spam');
124-
await fsextra.stat(dirname); // This should not fail.
125-
126-
await filesystem.mkdirp(dirname);
127-
128-
await fsextra.stat(dirname); // This should not fail.
129-
});
130-
});
131-
132110
suite('chmod', () => {
133111
async function checkMode(filename: string, expected: number) {
134112
const stat = await fsextra.stat(filename);

src/test/common/platform/filesystem.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,28 @@ suite('Raw FileSystem', () => {
240240
});
241241
});
242242

243+
suite('mkdirp', () => {
244+
test('creates the directory and all missing parents', async () => {
245+
await fix.createDirectory('x');
246+
// x/y, x/y/z, and x/y/z/spam are all missing.
247+
const dirname = await fix.resolve('x/y/z/spam', false);
248+
await ensureDoesNotExist(dirname);
249+
250+
await filesystem.mkdirp(dirname);
251+
252+
await fsextra.stat(dirname); // This should not fail.
253+
});
254+
255+
test('works if the directory already exists', async () => {
256+
const dirname = await fix.createDirectory('spam');
257+
await fsextra.stat(dirname); // This should not fail.
258+
259+
await filesystem.mkdirp(dirname);
260+
261+
await fsextra.stat(dirname); // This should not fail.
262+
});
263+
});
264+
243265
suite('copyFile', () => {
244266
test('the source file gets copied (same directory)', async () => {
245267
const data = '<content>';

src/test/common/platform/filesystem.unit.test.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
interface IRawFS {
2121
// VS Code
2222
copy(source: vscode.Uri, target: vscode.Uri, options?: {overwrite: boolean}): Thenable<void>;
23+
createDirectory(uri: vscode.Uri): Thenable<void>;
2324
delete(uri: vscode.Uri, options?: {recursive: boolean; useTrash: boolean}): Thenable<void>;
2425
readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>;
2526
readFile(uri: vscode.Uri): Thenable<Uint8Array>;
@@ -36,13 +37,13 @@ interface IRawFS {
3637
// "fs-extra"
3738
chmod(filePath: string, mode: string): Promise<void>;
3839
lstat(filename: string): Promise<fsextra.Stats>;
39-
mkdirp(dirname: string): Promise<void>;
4040
statSync(filename: string): fsextra.Stats;
4141
readFileSync(path: string, encoding: string): string;
4242

4343
// node "path"
4444
join(...filenames: string[]): string;
4545
normalize(filename: string): string;
46+
dirname(filename: string): string;
4647
}
4748

4849
suite('Raw FileSystem', () => {
@@ -55,6 +56,7 @@ suite('Raw FileSystem', () => {
5556
//stat = TypeMoq.Mock.ofType<FileStat>(undefined, TypeMoq.MockBehavior.Strict);
5657
oldStat = TypeMoq.Mock.ofType<fsextra.Stats>(undefined, TypeMoq.MockBehavior.Strict);
5758
filesystem = new RawFileSystem(
59+
raw.object,
5860
raw.object,
5961
raw.object,
6062
raw.object
@@ -175,8 +177,34 @@ suite('Raw FileSystem', () => {
175177
suite('mkdirp', () => {
176178
test('wraps the low-level function', async () => {
177179
const dirname = 'x/y/z/spam';
178-
raw.setup(r => r.mkdirp(dirname))
180+
raw.setup(r => r.createDirectory(vscode.Uri.file(dirname)))
181+
.returns(() => Promise.resolve());
182+
183+
await filesystem.mkdirp(dirname);
184+
185+
verifyAll();
186+
});
187+
188+
test('creates missing parent directories', async () => {
189+
const dirname = 'x/y/z/spam';
190+
raw.setup(r => r.createDirectory(vscode.Uri.file(dirname)))
191+
.throws(vscode.FileSystemError.FileNotFound(dirname))
192+
.verifiable(TypeMoq.Times.exactly(2));
193+
raw.setup(r => r.dirname(dirname))
194+
.returns(() => 'x/y/z');
195+
raw.setup(r => r.createDirectory(vscode.Uri.file('x/y/z')))
196+
.throws(vscode.FileSystemError.FileNotFound('x/y/z'))
197+
.verifiable(TypeMoq.Times.exactly(2));
198+
raw.setup(r => r.dirname('x/y/z'))
199+
.returns(() => 'x/y');
200+
raw.setup(r => r.createDirectory(vscode.Uri.file('x/y')))
179201
.returns(() => Promise.resolve());
202+
raw.setup(r => r.createDirectory(vscode.Uri.file('x/y/z')))
203+
.returns(() => Promise.resolve())
204+
.verifiable(TypeMoq.Times.exactly(2));
205+
raw.setup(r => r.createDirectory(vscode.Uri.file(dirname)))
206+
.returns(() => Promise.resolve())
207+
.verifiable(TypeMoq.Times.exactly(2));
180208

181209
await filesystem.mkdirp(dirname);
182210

@@ -431,6 +459,19 @@ suite('FileSystem paths', () => {
431459
});
432460
});
433461

462+
suite('dirname', () => {
463+
test('wraps low-level function', () => {
464+
const filename = 'x/y/z/spam.py';
465+
const expected = 'x/y/z';
466+
raw.setup(r => r.dirname(filename))
467+
.returns(() => expected);
468+
469+
const result = path.dirname(filename);
470+
471+
expect(result).to.equal(expected);
472+
});
473+
});
474+
434475
suite('normCase', () => {
435476
test('wraps low-level function', () => {
436477
const filename = 'x/y/z/spam.py';

src/test/vscode-mock.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ mockedVSCode.CodeActionKind = vscodeMocks.vscMock.CodeActionKind;
8282
mockedVSCode.DebugAdapterExecutable = vscodeMocks.vscMock.DebugAdapterExecutable;
8383
mockedVSCode.DebugAdapterServer = vscodeMocks.vscMock.DebugAdapterServer;
8484
mockedVSCode.FileType = vscodeMocks.vscMock.FileType;
85+
mockedVSCode.FileSystemError = vscodeMocks.vscMockExtHostedTypes.FileSystemError;
8586

8687
// This API is used in src/client/telemetry/telemetry.ts
8788
const extensions = TypeMoq.Mock.ofType<typeof vscode.extensions>();

0 commit comments

Comments
 (0)