33 * Licensed under the MIT License. See License.txt in the project root for license information.
44 *--------------------------------------------------------------------------------------------*/
55
6+ import { localize } from 'vs/nls' ;
67import { VSBuffer } from 'vs/base/common/buffer' ;
78import { CancellationToken } from 'vs/base/common/cancellation' ;
89import { Emitter , Event } from 'vs/base/common/event' ;
@@ -29,8 +30,8 @@ import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'
2930import { ExtHostNotebookEditor } from './extHostNotebookEditor' ;
3031import { onUnexpectedExternalError } from 'vs/base/common/errors' ;
3132import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer' ;
32- import { basename } from 'vs/base/common/resources' ;
3333import { 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