Skip to content

Commit 0ace1e0

Browse files
Use the new API for mkdirp().
1 parent bfe7f99 commit 0ace1e0

File tree

6 files changed

+126
-35
lines changed

6 files changed

+126
-35
lines changed

src/client/common/platform/fileSystem.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ import {
2323

2424
const ENCODING: string = 'utf8';
2525

26+
const FILE_NOT_FOUND = vscode.FileSystemError.FileNotFound().name;
27+
const FILE_EXISTS = vscode.FileSystemError.FileExists().name;
28+
29+
function isFileNotFoundError(err: Error): boolean {
30+
return err.name === FILE_NOT_FOUND;
31+
}
32+
33+
function isFileExistsError(err: Error): boolean {
34+
return err.name === FILE_EXISTS;
35+
}
36+
2637
function convertFileStat(stat: fsextra.Stats): FileStat {
2738
let fileType = FileType.Unknown;
2839
if (stat.isFile()) {
@@ -42,6 +53,7 @@ function convertFileStat(stat: fsextra.Stats): FileStat {
4253

4354
interface INodePath {
4455
join(...filenames: string[]): string;
56+
dirname(filename: string): string;
4557
normalize(filename: string): string;
4658
}
4759

@@ -57,6 +69,10 @@ export class FileSystemPath implements IFileSystemPath {
5769
return this.raw.join(...filenames);
5870
}
5971

72+
public dirname(filename: string): string {
73+
return this.raw.dirname(filename);
74+
}
75+
6076
public normCase(filename: string): string {
6177
filename = this.raw.normalize(filename);
6278
return this.isWindows ? filename.toUpperCase() : filename;
@@ -97,7 +113,7 @@ export class TempFileSystem {
97113
// This is the parts of the vscode.workspace.fs API that we use here.
98114
interface INewAPI {
99115
copy(source: vscode.Uri, target: vscode.Uri, options?: {overwrite: boolean}): Thenable<void>;
100-
//createDirectory(uri: vscode.Uri): Thenable<void>;
116+
createDirectory(uri: vscode.Uri): Thenable<void>;
101117
delete(uri: vscode.Uri, options?: {recursive: boolean; useTrash: boolean}): Thenable<void>;
102118
readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>;
103119
readFile(uri: vscode.Uri): Thenable<Uint8Array>;
@@ -119,23 +135,22 @@ interface IRawFS {
119135
interface IRawFSExtra {
120136
chmod(filePath: string, mode: string | number): Promise<void>;
121137
lstat(filename: string): Promise<fsextra.Stats>;
122-
mkdirp(dirname: string): Promise<void>;
123138

124139
// non-async
125140
statSync(filename: string): fsextra.Stats;
126141
readFileSync(path: string, encoding: string): string;
127142
}
128143

129-
//interface IRawPath {
130-
// join(...filenames: string[]): string;
131-
//}
144+
interface IRawPath {
145+
dirname(filename: string): string;
146+
}
132147

133148
// Later we will drop "FileSystem", switching usage to
134149
// "FileSystemUtils" and then rename "RawFileSystem" to "FileSystem".
135150

136151
export class RawFileSystem implements IRawFileSystem {
137152
constructor(
138-
//private readonly path: IRawPath = new FileSystemPath(),
153+
private readonly path: IRawPath = new FileSystemPath(),
139154
private readonly newapi: INewAPI = vscode.workspace.fs,
140155
private readonly nodefs: IRawFS = fs,
141156
private readonly fsExtra: IRawFSExtra = fsextra
@@ -183,6 +198,37 @@ export class RawFileSystem implements IRawFileSystem {
183198
return this.newapi.readDirectory(uri);
184199
}
185200

201+
public async mkdirp(dirname: string): Promise<void> {
202+
const stack = [dirname];
203+
while (stack.length > 0) {
204+
const current = stack.pop() || '';
205+
const uri = vscode.Uri.file(current);
206+
try {
207+
await this.newapi.createDirectory(uri);
208+
} catch (err) {
209+
if (isFileExistsError(err)) {
210+
// already done!
211+
return;
212+
}
213+
// According to the docs, FileNotFound means the parent
214+
// does not exist.
215+
// See: https://code.visualstudio.com/api/references/vscode-api#FileSystemProvider
216+
if (!isFileNotFoundError(err)) {
217+
// Fail for anything else.
218+
throw err; // re-throw
219+
}
220+
// Try creating the parent first.
221+
const parent = this.path.dirname(current);
222+
if (parent === '' || parent === current) {
223+
// This shouldn't ever happen.
224+
throw err;
225+
}
226+
stack.push(current);
227+
stack.push(parent);
228+
}
229+
}
230+
}
231+
186232
public async copyFile(src: string, dest: string): Promise<void> {
187233
const srcURI = vscode.Uri.file(src);
188234
const destURI = vscode.Uri.file(dest);
@@ -194,10 +240,6 @@ export class RawFileSystem implements IRawFileSystem {
194240
//****************************
195241
// fs-extra
196242

197-
public async mkdirp(dirname: string): Promise<void> {
198-
return this.fsExtra.mkdirp(dirname);
199-
}
200-
201243
public async chmod(filename: string, mode: string | number): Promise<void> {
202244
return this.fsExtra.chmod(filename, mode);
203245
}

src/client/common/platform/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ export interface IFileSystemPath {
5050
normCase(filename: string): string;
5151
}
5252

53+
// Eventually we will merge IPathUtils into IFileSystemPath.
54+
55+
export interface IFileSystemPath {
56+
join(...filenames: string[]): string;
57+
dirname(filename: string): string;
58+
normCase(filename: string): string;
59+
}
60+
5361
export import FileType = vscode.FileType;
5462
export type FileStat = vscode.FileStat;
5563
export type WriteStream = fs.WriteStream;

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

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

197-
suite('mkdirp', () => {
198-
test('creates the directory and all missing parents', async () => {
199-
await fix.createDirectory('x');
200-
// x/y, x/y/z, and x/y/z/spam are all missing.
201-
const dirname = await fix.resolve('x/y/z/spam', false);
202-
await ensureDoesNotExist(dirname);
203-
204-
await filesystem.mkdirp(dirname);
205-
206-
await fsextra.stat(dirname); // This should not fail.
207-
});
208-
209-
test('works if the directory already exists', async () => {
210-
const dirname = await fix.createDirectory('spam');
211-
await fsextra.stat(dirname); // This should not fail.
212-
213-
await filesystem.mkdirp(dirname);
214-
215-
await fsextra.stat(dirname); // This should not fail.
216-
});
217-
});
218-
219197
suite('chmod (non-Windows)', () => {
220198
suiteSetup(function () {
221199
// On Windows, chmod won't have any effect on the file itself.

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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { expect } from 'chai';
55
import * as fs from 'fs';
66
import * as fsextra from 'fs-extra';
77
import * as TypeMoq from 'typemoq';
8-
import { Disposable, Uri } from 'vscode';
8+
import { Disposable, FileSystemError, Uri } from 'vscode';
99
import {
1010
FileSystemPath, FileSystemUtils, RawFileSystem, TempFileSystem
1111
} from '../../../client/common/platform/fileSystem';
@@ -22,6 +22,7 @@ type TempCallback = (err: any, path: string, fd: number, cleanupCallback: () =>
2222
interface IRawFS {
2323
// VS Code
2424
copy(source: Uri, target: Uri, options?: {overwrite: boolean}): Thenable<void>;
25+
createDirectory(uri: Uri): Thenable<void>;
2526
delete(uri: Uri, options?: {recursive: boolean; useTrash: boolean}): Thenable<void>;
2627
readDirectory(uri: Uri): Thenable<[string, FileType][]>;
2728
readFile(uri: Uri): Thenable<Uint8Array>;
@@ -38,7 +39,6 @@ interface IRawFS {
3839
// "fs-extra"
3940
chmod(filePath: string, mode: string): Promise<void>;
4041
lstat(filename: string): Promise<fsextra.Stats>;
41-
mkdirp(dirname: string): Promise<void>;
4242
statSync(filename: string): fsextra.Stats;
4343
readFileSync(path: string, encoding: string): string;
4444

@@ -122,6 +122,19 @@ suite('FileSystem paths', () => {
122122
});
123123
});
124124

125+
suite('dirname', () => {
126+
test('wraps low-level function', () => {
127+
const filename = 'x/y/z/spam.py';
128+
const expected = 'x/y/z';
129+
raw.setup(r => r.dirname(filename))
130+
.returns(() => expected);
131+
132+
const result = path.dirname(filename);
133+
134+
expect(result).to.equal(expected);
135+
});
136+
});
137+
125138
suite('normCase', () => {
126139
test('wraps low-level function', () => {
127140
const filename = 'x/y/z/spam.py';
@@ -249,6 +262,7 @@ suite('Raw FileSystem', () => {
249262
//raw.object,
250263
raw.object,
251264
raw.object,
265+
raw.object,
252266
raw.object
253267
);
254268

@@ -367,8 +381,34 @@ suite('Raw FileSystem', () => {
367381
suite('mkdirp', () => {
368382
test('wraps the low-level function', async () => {
369383
const dirname = 'x/y/z/spam';
370-
raw.setup(r => r.mkdirp(dirname))
384+
raw.setup(r => r.createDirectory(Uri.file(dirname)))
385+
.returns(() => Promise.resolve());
386+
387+
await filesystem.mkdirp(dirname);
388+
389+
verifyAll();
390+
});
391+
392+
test('creates missing parent directories', async () => {
393+
const dirname = 'x/y/z/spam';
394+
raw.setup(r => r.createDirectory(Uri.file(dirname)))
395+
.throws(FileSystemError.FileNotFound(dirname))
396+
.verifiable(TypeMoq.Times.exactly(2));
397+
raw.setup(r => r.dirname(dirname))
398+
.returns(() => 'x/y/z');
399+
raw.setup(r => r.createDirectory(Uri.file('x/y/z')))
400+
.throws(FileSystemError.FileNotFound('x/y/z'))
401+
.verifiable(TypeMoq.Times.exactly(2));
402+
raw.setup(r => r.dirname('x/y/z'))
403+
.returns(() => 'x/y');
404+
raw.setup(r => r.createDirectory(Uri.file('x/y')))
371405
.returns(() => Promise.resolve());
406+
raw.setup(r => r.createDirectory(Uri.file('x/y/z')))
407+
.returns(() => Promise.resolve())
408+
.verifiable(TypeMoq.Times.exactly(2));
409+
raw.setup(r => r.createDirectory(Uri.file(dirname)))
410+
.returns(() => Promise.resolve())
411+
.verifiable(TypeMoq.Times.exactly(2));
372412

373413
await filesystem.mkdirp(dirname);
374414

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)