3
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
4
*--------------------------------------------------------------------------------------------*/
5
5
6
+ import { localize } from 'vs/nls' ;
6
7
import { VSBuffer } from 'vs/base/common/buffer' ;
7
8
import { CancellationToken } from 'vs/base/common/cancellation' ;
8
9
import { Emitter , Event } from 'vs/base/common/event' ;
@@ -29,8 +30,8 @@ import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'
29
30
import { ExtHostNotebookEditor } from './extHostNotebookEditor' ;
30
31
import { onUnexpectedExternalError } from 'vs/base/common/errors' ;
31
32
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer' ;
32
- import { basename } from 'vs/base/common/resources' ;
33
33
import { filter } from 'vs/base/common/objects' ;
34
+ import { Schemas } from 'vs/base/common/network' ;
34
35
35
36
36
37
@@ -322,6 +323,17 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
322
323
throw new Error ( 'Document version mismatch' ) ;
323
324
}
324
325
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
+
325
337
const data : vscode . NotebookData = {
326
338
metadata : filter ( document . apiNotebook . metadata , key => ! ( serializer . options ?. transientDocumentMetadata ?? { } ) [ key ] ) ,
327
339
cells : [ ] ,
@@ -344,10 +356,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
344
356
345
357
const bytes = await serializer . serializer . serializeNotebook ( data , token ) ;
346
358
await this . _extHostFileSystem . value . writeFile ( uri , bytes ) ;
359
+ const providerExtUri = this . _extHostFileSystem . getFileSystemProviderExtUri ( uri . scheme ) ;
347
360
const stat = await this . _extHostFileSystem . value . stat ( uri ) ;
348
361
349
362
const fileStats = {
350
- name : basename ( uri ) , // providerExtUri.basename(resource)
363
+ name : providerExtUri . basename ( uri ) ,
351
364
isFile : ( stat . type & files . FileType . File ) !== 0 ,
352
365
isDirectory : ( stat . type & files . FileType . Directory ) !== 0 ,
353
366
isSymbolicLink : ( stat . type & files . FileType . SymbolicLink ) !== 0 ,
@@ -363,6 +376,88 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
363
376
return fileStats ;
364
377
}
365
378
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
+
366
461
// --- open, save, saveAs, backup
367
462
368
463
0 commit comments