Skip to content

Commit 134e152

Browse files
authored
Implement fs save options in exthost notebook. (microsoft#186805)
1 parent 77dc507 commit 134e152

File tree

2 files changed

+101
-2
lines changed

2 files changed

+101
-2
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ export class ExtHostConsumerFileSystem {
250250
this._fileSystemProvider.set(scheme, { impl: provider, extUri: options?.isCaseSensitive ? extUri : extUriIgnorePathCase, isReadonly: !!options?.isReadonly });
251251
return toDisposable(() => this._fileSystemProvider.delete(scheme));
252252
}
253+
254+
getFileSystemProviderExtUri(scheme: string) {
255+
return this._fileSystemProvider.get(scheme)?.extUri ?? extUri;
256+
}
253257
}
254258

255259
export interface IExtHostConsumerFileSystem extends ExtHostConsumerFileSystem { }

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

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

6+
import { localize } from 'vs/nls';
67
import { VSBuffer } from 'vs/base/common/buffer';
78
import { CancellationToken } from 'vs/base/common/cancellation';
89
import { Emitter, Event } from 'vs/base/common/event';
@@ -29,8 +30,8 @@ import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'
2930
import { ExtHostNotebookEditor } from './extHostNotebookEditor';
3031
import { onUnexpectedExternalError } from 'vs/base/common/errors';
3132
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
32-
import { basename } from 'vs/base/common/resources';
3333
import { filter } from 'vs/base/common/objects';
34+
import { Schemas } from 'vs/base/common/network';
3435

3536

3637

@@ -322,6 +323,17 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
322323
throw new Error('Document version mismatch');
323324
}
324325

326+
if (!this._extHostFileSystem.value.isWritableFileSystem(uri.scheme)) {
327+
throw new files.FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this._resourceForError(uri)), files.FileOperationResult.FILE_PERMISSION_DENIED);
328+
}
329+
330+
// validate write
331+
const statBeforeWrite = await this._validateWriteFile(uri, options);
332+
333+
if (!statBeforeWrite) {
334+
await this._mkdirp(uri);
335+
}
336+
325337
const data: vscode.NotebookData = {
326338
metadata: filter(document.apiNotebook.metadata, key => !(serializer.options?.transientDocumentMetadata ?? {})[key]),
327339
cells: [],
@@ -344,10 +356,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
344356

345357
const bytes = await serializer.serializer.serializeNotebook(data, token);
346358
await this._extHostFileSystem.value.writeFile(uri, bytes);
359+
const providerExtUri = this._extHostFileSystem.getFileSystemProviderExtUri(uri.scheme);
347360
const stat = await this._extHostFileSystem.value.stat(uri);
348361

349362
const fileStats = {
350-
name: basename(uri), // providerExtUri.basename(resource)
363+
name: providerExtUri.basename(uri),
351364
isFile: (stat.type & files.FileType.File) !== 0,
352365
isDirectory: (stat.type & files.FileType.Directory) !== 0,
353366
isSymbolicLink: (stat.type & files.FileType.SymbolicLink) !== 0,
@@ -363,6 +376,88 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
363376
return fileStats;
364377
}
365378

379+
private async _validateWriteFile(uri: URI, options: files.IWriteFileOptions) {
380+
// File system provider registered in Extension Host doesn't have unlock or atomic support
381+
// Validate via file stat meta data
382+
const stat = await this._extHostFileSystem.value.stat(uri);
383+
384+
// File cannot be directory
385+
if ((stat.type & files.FileType.Directory) !== 0) {
386+
throw new files.FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this._resourceForError(uri)), files.FileOperationResult.FILE_IS_DIRECTORY, options);
387+
}
388+
389+
// File cannot be readonly
390+
if ((stat.permissions ?? 0) & files.FilePermission.Readonly) {
391+
throw new files.FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this._resourceForError(uri)), files.FileOperationResult.FILE_PERMISSION_DENIED);
392+
}
393+
394+
// Dirty write prevention
395+
if (
396+
typeof options?.mtime === 'number' && typeof options.etag === 'string' && options.etag !== files.ETAG_DISABLED &&
397+
typeof stat.mtime === 'number' && typeof stat.size === 'number' &&
398+
options.mtime < stat.mtime && options.etag !== files.etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size })
399+
) {
400+
throw new files.FileOperationError(localize('fileModifiedError', "File Modified Since"), files.FileOperationResult.FILE_MODIFIED_SINCE, options);
401+
}
402+
403+
return stat;
404+
}
405+
406+
private async _mkdirp(uri: URI) {
407+
const providerExtUri = this._extHostFileSystem.getFileSystemProviderExtUri(uri.scheme);
408+
let directory = providerExtUri.dirname(uri);
409+
410+
const directoriesToCreate: string[] = [];
411+
412+
while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {
413+
try {
414+
const stat = await this._extHostFileSystem.value.stat(directory);
415+
if ((stat.type & files.FileType.Directory) === 0) {
416+
throw new Error(localize('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", this._resourceForError(directory)));
417+
}
418+
419+
break; // we have hit a directory that exists -> good
420+
} catch (error) {
421+
422+
// Bubble up any other error that is not file not found
423+
if (files.toFileSystemProviderErrorCode(error) !== files.FileSystemProviderErrorCode.FileNotFound) {
424+
throw error;
425+
}
426+
427+
// Upon error, remember directories that need to be created
428+
directoriesToCreate.push(providerExtUri.basename(directory));
429+
430+
// Continue up
431+
directory = providerExtUri.dirname(directory);
432+
}
433+
}
434+
435+
// Create directories as needed
436+
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
437+
directory = providerExtUri.joinPath(directory, directoriesToCreate[i]);
438+
439+
try {
440+
await this._extHostFileSystem.value.createDirectory(directory);
441+
} catch (error) {
442+
if (files.toFileSystemProviderErrorCode(error) !== files.FileSystemProviderErrorCode.FileExists) {
443+
// For mkdirp() we tolerate that the mkdir() call fails
444+
// in case the folder already exists. This follows node.js
445+
// own implementation of fs.mkdir({ recursive: true }) and
446+
// reduces the chances of race conditions leading to errors
447+
// if multiple calls try to create the same folders
448+
// As such, we only throw an error here if it is other than
449+
// the fact that the file already exists.
450+
// (see also https://github.com/microsoft/vscode/issues/89834)
451+
throw error;
452+
}
453+
}
454+
}
455+
}
456+
457+
private _resourceForError(uri: URI): string {
458+
return uri.scheme === Schemas.file ? uri.fsPath : uri.toString();
459+
}
460+
366461
// --- open, save, saveAs, backup
367462

368463

0 commit comments

Comments
 (0)