Skip to content

Commit 052ac9c

Browse files
authored
files - allow more file operations to run in the extension host (microsoft#172345) (microsoft#185988)
* files - allow more file operations to run in the extension host (microsoft#172345) * fix tests * tests * tests * tests
1 parent 6b53305 commit 052ac9c

File tree

4 files changed

+123
-17
lines changed

4 files changed

+123
-17
lines changed

extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ suite('vscode API - workspace-fs', () => {
187187

188188
test('vscode.workspace.fs error reporting is weird #132981', async function () {
189189

190-
191190
const uri = await createRandomFile();
192191

193192
const source = vscode.Uri.joinPath(uri, `./${Math.random().toString(16).slice(2, 8)}`);
@@ -217,4 +216,48 @@ suite('vscode API - workspace-fs', () => {
217216
assert.strictEqual(err.code, 'FileNotFound');
218217
}
219218
});
219+
220+
test('fs.createFolder creates recursively', async function () {
221+
222+
const folder = root.with({ path: posix.join(root.path, 'deeply', 'nested', 'folder') });
223+
await vscode.workspace.fs.createDirectory(folder);
224+
225+
let stat = await vscode.workspace.fs.stat(folder);
226+
assert.strictEqual(stat.type, vscode.FileType.Directory);
227+
228+
await vscode.workspace.fs.delete(folder, { recursive: true, useTrash: false });
229+
230+
await vscode.workspace.fs.createDirectory(folder); // calling on existing folder is also ok!
231+
232+
const file = root.with({ path: posix.join(folder.path, 'file.txt') });
233+
await vscode.workspace.fs.writeFile(file, Buffer.from('Hello World'));
234+
const folder2 = root.with({ path: posix.join(file.path, 'invalid') });
235+
let e;
236+
try {
237+
await vscode.workspace.fs.createDirectory(folder2); // cannot create folder on file path
238+
} catch (error) {
239+
e = error;
240+
}
241+
assert.ok(e);
242+
243+
const folder3 = root.with({ path: posix.join(root.path, 'DEEPLY', 'NESTED', 'FOLDER') });
244+
await vscode.workspace.fs.createDirectory(folder3); // calling on different cased folder is ok!
245+
stat = await vscode.workspace.fs.stat(folder3);
246+
assert.strictEqual(stat.type, vscode.FileType.Directory);
247+
248+
await vscode.workspace.fs.delete(folder, { recursive: true, useTrash: false });
249+
});
250+
251+
test('fs.writeFile creates parents recursively', async function () {
252+
253+
const folder = root.with({ path: posix.join(root.path, 'other-deeply', 'nested', 'folder') });
254+
const file = root.with({ path: posix.join(folder.path, 'file.txt') });
255+
256+
await vscode.workspace.fs.writeFile(file, Buffer.from('Hello World'));
257+
258+
const stat = await vscode.workspace.fs.stat(file);
259+
assert.strictEqual(stat.type, vscode.FileType.File);
260+
261+
await vscode.workspace.fs.delete(folder, { recursive: true, useTrash: false });
262+
});
220263
});

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
10201020
registerFileSystemProvider(scheme, provider, options) {
10211021
return combinedDisposable(
10221022
extHostFileSystem.registerFileSystemProvider(extension, scheme, provider, options),
1023-
extHostConsumerFileSystem.addFileSystemProvider(scheme, provider)
1023+
extHostConsumerFileSystem.addFileSystemProvider(scheme, provider, options)
10241024
);
10251025
},
10261026
get fs() {

src/vs/workbench/api/common/extHostFileSystemConsumer.ts

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { MainContext, MainThreadFileSystemShape } from './extHost.protocol';
7-
import * as vscode from 'vscode';
7+
import type * as vscode from 'vscode';
88
import * as files from 'vs/platform/files/common/files';
99
import { FileSystemError } from 'vs/workbench/api/common/extHostTypes';
1010
import { VSBuffer } from 'vs/base/common/buffer';
1111
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1212
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
1313
import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
1414
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
15+
import { ResourceQueue } from 'vs/base/common/async';
16+
import { IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources';
17+
import { Schemas } from 'vs/base/common/network';
18+
import { IMarkdownString } from 'vs/base/common/htmlContent';
1519

1620
export class ExtHostConsumerFileSystem {
1721

@@ -20,7 +24,9 @@ export class ExtHostConsumerFileSystem {
2024
readonly value: vscode.FileSystem;
2125

2226
private readonly _proxy: MainThreadFileSystemShape;
23-
private readonly _fileSystemProvider = new Map<string, vscode.FileSystemProvider>();
27+
private readonly _fileSystemProvider = new Map<string, { impl: vscode.FileSystemProvider; extUri: IExtUri; isReadonly: boolean }>();
28+
29+
private readonly writeQueue = new ResourceQueue();
2430

2531
constructor(
2632
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@@ -38,7 +44,7 @@ export class ExtHostConsumerFileSystem {
3844
if (provider) {
3945
// use shortcut
4046
await that._proxy.$ensureActivation(uri.scheme);
41-
stat = await provider.stat(uri);
47+
stat = await provider.impl.stat(uri);
4248
} else {
4349
stat = await that._proxy.$stat(uri);
4450
}
@@ -60,7 +66,7 @@ export class ExtHostConsumerFileSystem {
6066
if (provider) {
6167
// use shortcut
6268
await that._proxy.$ensureActivation(uri.scheme);
63-
return (await provider.readDirectory(uri)).slice(); // safe-copy
69+
return (await provider.impl.readDirectory(uri)).slice(); // safe-copy
6470
} else {
6571
return await that._proxy.$readdir(uri);
6672
}
@@ -70,8 +76,14 @@ export class ExtHostConsumerFileSystem {
7076
},
7177
async createDirectory(uri: vscode.Uri): Promise<void> {
7278
try {
73-
// no shortcut: does mkdirp
74-
return await that._proxy.$mkdir(uri);
79+
const provider = that._fileSystemProvider.get(uri.scheme);
80+
if (provider && !provider.isReadonly) {
81+
// use shortcut
82+
await that._proxy.$ensureActivation(uri.scheme);
83+
return await that.mkdirp(provider.impl, provider.extUri, uri);
84+
} else {
85+
return await that._proxy.$mkdir(uri);
86+
}
7587
} catch (err) {
7688
return ExtHostConsumerFileSystem._handleError(err);
7789
}
@@ -82,7 +94,7 @@ export class ExtHostConsumerFileSystem {
8294
if (provider) {
8395
// use shortcut
8496
await that._proxy.$ensureActivation(uri.scheme);
85-
return (await provider.readFile(uri)).slice(); // safe-copy
97+
return (await provider.impl.readFile(uri)).slice(); // safe-copy
8698
} else {
8799
const buff = await that._proxy.$readFile(uri);
88100
return buff.buffer;
@@ -93,19 +105,26 @@ export class ExtHostConsumerFileSystem {
93105
},
94106
async writeFile(uri: vscode.Uri, content: Uint8Array): Promise<void> {
95107
try {
96-
// no shortcut: does mkdirp
97-
return await that._proxy.$writeFile(uri, VSBuffer.wrap(content));
108+
const provider = that._fileSystemProvider.get(uri.scheme);
109+
if (provider && !provider.isReadonly) {
110+
// use shortcut
111+
await that._proxy.$ensureActivation(uri.scheme);
112+
await that.mkdirp(provider.impl, provider.extUri, provider.extUri.dirname(uri));
113+
return await that.writeQueue.queueFor(uri).queue(() => Promise.resolve(provider.impl.writeFile(uri, content, { create: true, overwrite: true })));
114+
} else {
115+
return await that._proxy.$writeFile(uri, VSBuffer.wrap(content));
116+
}
98117
} catch (err) {
99118
return ExtHostConsumerFileSystem._handleError(err);
100119
}
101120
},
102121
async delete(uri: vscode.Uri, options?: { recursive?: boolean; useTrash?: boolean }): Promise<void> {
103122
try {
104123
const provider = that._fileSystemProvider.get(uri.scheme);
105-
if (provider) {
124+
if (provider && !provider.isReadonly) {
106125
// use shortcut
107126
await that._proxy.$ensureActivation(uri.scheme);
108-
return await provider.delete(uri, { recursive: false, ...options });
127+
return await provider.impl.delete(uri, { recursive: false, ...options });
109128
} else {
110129
return await that._proxy.$delete(uri, { recursive: false, useTrash: false, atomic: false, ...options });
111130
}
@@ -139,6 +158,49 @@ export class ExtHostConsumerFileSystem {
139158
});
140159
}
141160

161+
private async mkdirp(provider: vscode.FileSystemProvider, providerExtUri: IExtUri, directory: vscode.Uri): Promise<void> {
162+
const directoriesToCreate: string[] = [];
163+
164+
while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {
165+
try {
166+
const stat = await provider.stat(directory);
167+
if ((stat.type & files.FileType.Directory) === 0) {
168+
throw FileSystemError.FileExists(`Unable to create folder '${directory.scheme === Schemas.file ? directory.fsPath : directory.toString(true)}' that already exists but is not a directory`);
169+
}
170+
171+
break; // we have hit a directory that exists -> good
172+
} catch (error) {
173+
if (files.toFileSystemProviderErrorCode(error) !== files.FileSystemProviderErrorCode.FileNotFound) {
174+
throw error;
175+
}
176+
177+
// further go up and remember to create this directory
178+
directoriesToCreate.push(providerExtUri.basename(directory));
179+
directory = providerExtUri.dirname(directory);
180+
}
181+
}
182+
183+
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
184+
directory = providerExtUri.joinPath(directory, directoriesToCreate[i]);
185+
186+
try {
187+
await provider.createDirectory(directory);
188+
} catch (error) {
189+
if (files.toFileSystemProviderErrorCode(error) !== files.FileSystemProviderErrorCode.FileExists) {
190+
// For mkdirp() we tolerate that the mkdir() call fails
191+
// in case the folder already exists. This follows node.js
192+
// own implementation of fs.mkdir({ recursive: true }) and
193+
// reduces the chances of race conditions leading to errors
194+
// if multiple calls try to create the same folders
195+
// As such, we only throw an error here if it is other than
196+
// the fact that the file already exists.
197+
// (see also https://github.com/microsoft/vscode/issues/89834)
198+
throw error;
199+
}
200+
}
201+
}
202+
}
203+
142204
private static _handleError(err: any): never {
143205
// desired error type
144206
if (err instanceof FileSystemError) {
@@ -184,8 +246,8 @@ export class ExtHostConsumerFileSystem {
184246

185247
// ---
186248

187-
addFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider): IDisposable {
188-
this._fileSystemProvider.set(scheme, provider);
249+
addFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options?: { isCaseSensitive?: boolean; isReadonly?: boolean | IMarkdownString }): IDisposable {
250+
this._fileSystemProvider.set(scheme, { impl: provider, extUri: options?.isCaseSensitive ? extUri : extUriIgnorePathCase, isReadonly: !!options?.isReadonly });
189251
return toDisposable(() => this._fileSystemProvider.delete(scheme));
190252
}
191253
}

src/vs/workbench/api/node/extHostDiskFileSystemProvider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as vscode from 'vscode';
6+
import type * as vscode from 'vscode';
77
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
88
import { Schemas } from 'vs/base/common/network';
99
import { ILogService } from 'vs/platform/log/common/log';
1010
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
1111
import { FilePermission } from 'vs/platform/files/common/files';
12+
import { isLinux } from 'vs/base/common/platform';
1213

1314
export class ExtHostDiskFileSystemProvider {
1415

@@ -20,7 +21,7 @@ export class ExtHostDiskFileSystemProvider {
2021
// Register disk file system provider so that certain
2122
// file operations can execute fast within the extension
2223
// host without roundtripping.
23-
extHostConsumerFileSystem.addFileSystemProvider(Schemas.file, new DiskFileSystemProviderAdapter(logService));
24+
extHostConsumerFileSystem.addFileSystemProvider(Schemas.file, new DiskFileSystemProviderAdapter(logService), { isCaseSensitive: isLinux });
2425
}
2526
}
2627

0 commit comments

Comments
 (0)