/********************************************************************************
 * Copyright (C) 2020 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/ import { injectable , inject , postConstruct } from '@theia/core/shared/inversify' ; import { basename , dirname , normalize , join } from 'path' ; import { v4 } from 'uuid' ; import * as os from 'os' ; import * as fs from 'fs' ; import { mkdir , open , close , read , write , fdatasync , Stats , lstat , stat , readdir , readFile , exists , chmod , rmdir , unlink , rename , futimes , truncate } from 'fs' ; import { promisify } from 'util' ; import URI from '@theia/core/lib/common/uri' ; import { Path } from '@theia/core/lib/common/path' ; import { FileUri } from '@theia/core/lib/node/file-uri' ; import { Event , Emitter } from '@theia/core/lib/common/event' ; import { Disposable , DisposableCollection } from '@theia/core/lib/common/disposable' ; import { OS , isWindows } from '@theia/core/lib/common/os' ; import { retry } from '@theia/core/lib/common/promise-util' ; import { FileSystemProviderWithFileReadWriteCapability , FileSystemProviderWithOpenReadWriteCloseCapability , FileSystemProviderWithFileFolderCopyCapability , FileSystemProviderCapabilities , Stat , FileType , FileWriteOptions , createFileSystemProviderError , FileSystemProviderErrorCode , FileOpenOptions , FileDeleteOptions , FileOverwriteOptions , FileSystemProviderError , FileChange , WatchOptions , FileUpdateOptions , FileUpdateResult , FileReadStreamOptions } from '../common/files' ; import { FileSystemWatcherServer } from '../common/filesystem-watcher-protocol' ; import trash = require ( 'trash' ) ; import { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-languageserver-protocol' ; import { TextDocument } from 'vscode-languageserver-textdocument' ; import { EncodingService } from '@theia/core/lib/common/encoding-service' ; import { BinaryBuffer } from '@theia/core/lib/common/buffer' ; import { ReadableStreamEvents , newWriteableStream } from '@theia/core/lib/common/stream' ; import { CancellationToken } from '@theia/core/lib/common/cancellation' ; import { readFileIntoStream } from '../common/io' ; export namespace DiskFileSystemProvider { export interface StatAndLink { stat : fs . Stats ; symbolicLink ? : { dangling : boolean ; } ; } } @ injectable ( ) export class DiskFileSystemProvider implements Disposable , FileSystemProviderWithFileReadWriteCapability , FileSystemProviderWithOpenReadWriteCloseCapability , FileSystemProviderWithFileFolderCopyCapability { private readonly BUFFER_SIZE = 64 * 1024 ; private readonly onDidChangeFileEmitter = new Emitter < readonly FileChange [ ] > ( ) ; readonly onDidChangeFile = this . onDidChangeFileEmitter . event ; private readonly onFileWatchErrorEmitter = new Emitter < void > ( ) ; readonly onFileWatchError = this . onFileWatchErrorEmitter . event ; protected readonly toDispose = new DisposableCollection ( this . onDidChangeFileEmitter ) ; @ inject ( FileSystemWatcherServer ) protected readonly watcher : FileSystemWatcherServer ; @ inject ( EncodingService ) protected readonly encodingService : EncodingService ; @ postConstruct ( ) protected init ( ) { this . toDispose . push ( this . watcher ) ; this . watcher . setClient ( { onDidFilesChanged : params => this . onDidChangeFileEmitter . fire ( params . changes . map ( ( { uri , type } ) => ( { resource : new URI ( uri ) , type } ) ) ) , onError : ( ) => this . onFileWatchErrorEmitter . fire ( ) } ) ; } readonly onDidChangeCapabilities = Event . None ; protected _capabilities : FileSystemProviderCapabilities | undefined ; get capabilities ( ) : FileSystemProviderCapabilities { if ( ! this . _capabilities ) { this . _capabilities = FileSystemProviderCapabilities . FileReadWrite | FileSystemProviderCapabilities . FileOpenReadWriteClose | FileSystemProviderCapabilities . FileReadStream | FileSystemProviderCapabilities . FileFolderCopy | FileSystemProviderCapabilities . Access | FileSystemProviderCapabilities . Trash | FileSystemProviderCapabilities . Update ; if ( OS . type ( ) === OS . Type . Linux ) { this . _capabilities |= FileSystemProviderCapabilities . PathCaseSensitive ; } } return this . _capabilities ; } async stat ( resource ) { try { const { stat , symbolicLink } = await this . statLink ( this . toFilePath ( resource ) ) ; return { type : this . toType ( stat , symbolicLink ) , ctime : stat . birthtime . getTime ( ) , mtime : stat . mtime . getTime ( ) , size : stat . size } ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } async access ( resource , mode ? ) { try { await promisify ( fs . access ) ( this . toFilePath ( resource ) , mode ) ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } async fsPath ( resource ) { return FileUri . fsPath ( resource ) ; } protected async statLink ( path ) { let lstats ; try { lstats = await promisify ( lstat ) ( path ) ; if ( ! lstats . isSymbolicLink ( ) ) { return { stat : lstats } ; } } catch ( error ) { } try { const stats = await promisify ( stat ) ( path ) ; return { stat : stats , symbolicLink : lstats ?. isSymbolicLink ( ) ? { dangling : false } : undefined } ; } catch ( error ) { if ( error . code === 'ENOENT' && lstats ) { return { stat : lstats , symbolicLink : { dangling : true } } ; } throw error ; } } async readdir ( resource ) { try { const children = await promisify ( fs . readdir ) ( this . toFilePath ( resource ) ) ; const result = [ ] ; await Promise . all ( children . map ( async ( child ) => { try { const stat = await this . stat ( resource . resolve ( child ) ) ; result . push ( [ child , stat . type ] ) ; } catch ( error ) { console . trace ( error ) ; } } ) ) ; return result ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } private toType ( entry , symbolicLink ? ) { let type ; if ( symbolicLink ?. dangling ) { type = FileType . Unknown ; } else if ( entry . isFile ( ) ) { type = FileType . File ; } else if ( entry . isDirectory ( ) ) { type = FileType . Directory ; } else { type = FileType . Unknown ; } if ( symbolicLink ) { type |= FileType . SymbolicLink ; } return type ; } async readFile ( resource ) { try { const filePath = this . toFilePath ( resource ) ; return await promisify ( readFile ) ( filePath ) ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } readFileStream ( resource , opts , token ) { const stream = newWriteableStream < Uint8Array > ( data => BinaryBuffer . concat ( data . map ( data => BinaryBuffer . wrap ( data ) ) ) . buffer ) ; readFileIntoStream ( this , resource , stream , data => data . buffer , { ... opts , bufferSize : this . BUFFER_SIZE } , token ) ; return stream ; } async writeFile ( resource , content , opts ) { let handle = undefined ; try { const filePath = this . toFilePath ( resource ) ; if ( ! opts . create || ! opts . overwrite ) { const fileExists = await promisify ( exists ) ( filePath ) ; if ( fileExists ) { if ( ! opts . overwrite ) { throw createFileSystemProviderError ( 'File already exists' , FileSystemProviderErrorCode . FileExists ) ; } } else { if ( ! opts . create ) { throw createFileSystemProviderError ( 'File does not exist' , FileSystemProviderErrorCode . FileNotFound ) ; } } } handle = await this . open ( resource , { create : true } ) ; await this . write ( handle , 0 , content , 0 , content . byteLength ) ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } finally { if ( typeof handle === 'number' ) { await this . close ( handle ) ; } } } private mapHandleToPos : Map < number , number > = new Map ( ) ; private writeHandles : Set < number > = new Set ( ) ; private canFlush : boolean = true ; async open ( resource , opts ) { try { const filePath = this . toFilePath ( resource ) ; let flags = undefined ; if ( opts . create ) { if ( isWindows && await promisify ( exists ) ( filePath ) ) { try { await promisify ( truncate ) ( filePath , 0 ) ; flags = 'r+' ; } catch ( error ) { console . trace ( error ) ; } } if ( ! flags ) { flags = 'w' ; } } else { flags = 'r' ; } const handle = await promisify ( open ) ( filePath , flags ) ; this . mapHandleToPos . set ( handle , 0 ) ; if ( opts . create ) { this . writeHandles . add ( handle ) ; } return handle ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } async close ( fd ) { try { this . mapHandleToPos . delete ( fd ) ; if ( this . writeHandles . delete ( fd ) && this . canFlush ) { try { await promisify ( fdatasync ) ( fd ) ; } catch ( error ) { this . canFlush = false ; console . error ( error ) ; } } return await promisify ( close ) ( fd ) ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } async read ( fd , pos , data , offset , length ) { const normalizedPos = this . normalizePos ( fd , pos ) ; let bytesRead = null ; try { const result = await promisify ( read ) ( fd , data , offset , length , normalizedPos ) ; if ( typeof result === 'number' ) { bytesRead = result ; } else { bytesRead = result . bytesRead ; } return bytesRead ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } finally { this . updatePos ( fd , normalizedPos , bytesRead ) ; } } private normalizePos ( fd , pos ) { if ( pos === this . mapHandleToPos . get ( fd ) ) { return null ; } return pos ; } private updatePos ( fd , pos , bytesLength ) { const lastKnownPos = this . mapHandleToPos . get ( fd ) ; if ( typeof lastKnownPos === 'number' ) { if ( typeof pos === 'number' ) { } else if ( typeof bytesLength === 'number' ) { this . mapHandleToPos . set ( fd , lastKnownPos + bytesLength ) ; } else { this . mapHandleToPos . delete ( fd ) ; } } } async write ( fd , pos , data , offset , length ) { return retry ( ( ) => this . doWrite ( fd , pos , data , offset , length ) , 100 , 3 ) ; } private async doWrite ( fd , pos , data , offset , length ) { const normalizedPos = this . normalizePos ( fd , pos ) ; let bytesWritten = null ; try { const result = await promisify ( write ) ( fd , data , offset , length , normalizedPos ) ; if ( typeof result === 'number' ) { bytesWritten = result ; } else { bytesWritten = result . bytesWritten ; } return bytesWritten ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } finally { this . updatePos ( fd , normalizedPos , bytesWritten ) ; } } async mkdir ( resource ) { try { await promisify ( mkdir ) ( this . toFilePath ( resource ) ) ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } async delete ( resource , opts ) { try { const filePath = this . toFilePath ( resource ) ; await this . doDelete ( filePath , opts ) ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } protected async doDelete ( filePath , opts ) { if ( ! opts . useTrash ) { if ( opts . recursive ) { await this . rimraf ( filePath ) ; } else { await promisify ( unlink ) ( filePath ) ; } } else { await trash ( filePath ) ; } } protected rimraf ( path ) { if ( new Path ( path ) . isRoot ) { throw new Error ( 'rimraf - will refuse to recursively delete root' ) ; } return this . rimrafMove ( path ) ; } protected async rimrafMove ( path ) { try { const pathInTemp = join ( os . tmpdir ( ) , v4 ( ) ) ; try { await promisify ( rename ) ( path , pathInTemp ) ; } catch ( error ) { return this . rimrafUnlink ( path ) ; } this . rimrafUnlink ( pathInTemp ) ; } catch ( error ) { if ( error . code !== 'ENOENT' ) { throw error ; } } } protected async rimrafUnlink ( path ) { try { const stat = await promisify ( lstat ) ( path ) ; if ( stat . isDirectory ( ) && ! stat . isSymbolicLink ( ) ) { const children = await promisify ( readdir ) ( path ) ; await Promise . all ( children . map ( child => this . rimrafUnlink ( join ( path , child ) ) ) ) ; await promisify ( rmdir ) ( path ) ; } else { const mode = stat . mode ; if ( ! ( mode & 128 ) ) { await promisify ( chmod ) ( path , mode | 128 ) ; } return promisify ( unlink ) ( path ) ; } } catch ( error ) { if ( error . code !== 'ENOENT' ) { throw error ; } } } async rename ( from , to , opts ) { const fromFilePath = this . toFilePath ( from ) ; const toFilePath = this . toFilePath ( to ) ; if ( fromFilePath === toFilePath ) { return ; } try { await this . validateTargetDeleted ( from , to , 'move' , opts . overwrite ) ; await this . move ( fromFilePath , toFilePath ) ; } catch ( error ) { if ( error . code === 'EINVAL' || error . code === 'EBUSY' || error . code === 'ENAMETOOLONG' ) { error = new Error ( `Unable to move '${ basename ( fromFilePath ) }' into '${ basename ( dirname ( toFilePath ) ) }' (${ error . toString ( ) }).` ) ; } throw this . toFileSystemProviderError ( error ) ; } } protected async move ( source , target ) { if ( source === target ) { return Promise . resolve ( ) ; } async function updateMtime ( path ) { const stat = await promisify ( lstat ) ( path ) ; if ( stat . isDirectory ( ) || stat . isSymbolicLink ( ) ) { return Promise . resolve ( ) ; } const fd = await promisify ( open ) ( path , 'a' ) ; try { await promisify ( futimes ) ( fd , stat . atime , new Date ( ) ) ; } catch ( error ) { } return promisify ( close ) ( fd ) ; } try { await promisify ( rename ) ( source , target ) ; await updateMtime ( target ) ; } catch ( error ) { if ( source . toLowerCase ( ) !== target . toLowerCase ( ) && error . code === 'EXDEV' || source . endsWith ( '.' ) ) { await this . doCopy ( source , target ) ; await this . rimraf ( source ) ; await updateMtime ( target ) ; } else { throw error ; } } } async copy ( from , to , opts ) { const fromFilePath = this . toFilePath ( from ) ; const toFilePath = this . toFilePath ( to ) ; if ( fromFilePath === toFilePath ) { return ; } try { await this . validateTargetDeleted ( from , to , 'copy' , opts . overwrite ) ; await this . doCopy ( fromFilePath , toFilePath ) ; } catch ( error ) { if ( error . code === 'EINVAL' || error . code === 'EBUSY' || error . code === 'ENAMETOOLONG' ) { error = new Error ( `Unable to copy '${ basename ( fromFilePath ) }' into '${ basename ( dirname ( toFilePath ) ) }' (${ error . toString ( ) }).` ) ; } throw this . toFileSystemProviderError ( error ) ; } } private async validateTargetDeleted ( from , to , mode , overwrite ? ) { const isPathCaseSensitive = ! ! ( this . capabilities & FileSystemProviderCapabilities . PathCaseSensitive ) ; const fromFilePath = this . toFilePath ( from ) ; const toFilePath = this . toFilePath ( to ) ; let isSameResourceWithDifferentPathCase = false ; if ( ! isPathCaseSensitive ) { isSameResourceWithDifferentPathCase = fromFilePath . toLowerCase ( ) === toFilePath . toLowerCase ( ) ; } if ( isSameResourceWithDifferentPathCase && mode === 'copy' ) { throw createFileSystemProviderError ( "'File cannot be copied to same path with different path case" , FileSystemProviderErrorCode . FileExists ) ; } if ( ! isSameResourceWithDifferentPathCase && await promisify ( exists ) ( toFilePath ) ) { if ( ! overwrite ) { throw createFileSystemProviderError ( 'File at target already exists' , FileSystemProviderErrorCode . FileExists ) ; } await this . delete ( to , { recursive : true , useTrash : false } ) ; } } protected async doCopy ( source , target , copiedSourcesIn ? ) { const copiedSources = copiedSourcesIn ? copiedSourcesIn : Object . create ( null ) ; const fileStat = await promisify ( stat ) ( source ) ; if ( ! fileStat . isDirectory ( ) ) { return this . doCopyFile ( source , target , fileStat . mode & 511 ) ; } if ( copiedSources [ source ] ) { return Promise . resolve ( ) ; } copiedSources [ source ] = true ; this . mkdirp ( target , fileStat . mode & 511 ) ; const files = await promisify ( readdir ) ( source ) ; for ( let i = 0 ; i < files . length ; i ++ ) { const file = files [ i ] ; await this . doCopy ( join ( source , file ) , join ( target , file ) , copiedSources ) ; } } protected async mkdirp ( path , mode ? ) { const mkdir = async ( ) => { try { await promisify ( fs . mkdir ) ( path , mode ) ; } catch ( error ) { if ( error . code === 'ENOENT' ) { throw error ; } let targetIsFile = false ; try { const fileStat = await promisify ( fs . stat ) ( path ) ; targetIsFile = ! fileStat . isDirectory ( ) ; } catch ( statError ) { throw error ; } if ( targetIsFile ) { throw new Error ( `'${ path }' exists and is not a directory.` ) ; } } } ; if ( path === dirname ( path ) ) { return ; } try { await mkdir ( ) ; } catch ( error ) { if ( error . code === 'ENOENT' ) { await this . mkdirp ( dirname ( path ) , mode ) ; return mkdir ( ) ; } throw error ; } } protected doCopyFile ( source , target , mode ) { return new Promise ( ( resolve , reject ) => { const reader = fs . createReadStream ( source ) ; const writer = fs . createWriteStream ( target , { mode } ) ; let finished = false ; const finish = ( error ? ) => { if ( ! finished ) { finished = true ; if ( error ) { return reject ( error ) ; } fs . chmod ( target , mode , error => error ? reject ( error ) : resolve ( ) ) ; } } ; reader . once ( 'error' , error => finish ( error ) ) ; writer . once ( 'error' , error => finish ( error ) ) ; writer . once ( 'close' , ( ) => finish ( ) ) ; reader . pipe ( writer ) ; } ) ; } watch ( resource , opts ) { const watcherService = this . watcher ; /**
         * Disposable handle. Can be disposed early (before the watcher is allocated.)
         */ const handle = { disposed : false , watcherId : undefined as number | undefined , dispose ( ) { if ( this . disposed ) { return ; } if ( this . watcherId !== undefined ) { watcherService . unwatchFileChanges ( this . watcherId ) ; } this . disposed = true ; } , } ; watcherService . watchFileChanges ( resource . toString ( ) , { ignored : opts . excludes } ) . then ( watcherId => { if ( handle . disposed ) { watcherService . unwatchFileChanges ( watcherId ) ; } else { handle . watcherId = watcherId ; } } ) ; this . toDispose . push ( handle ) ; return handle ; } async updateFile ( resource , changes , opts ) { try { const content = await this . readFile ( resource ) ; const decoded = this . encodingService . decode ( BinaryBuffer . wrap ( content ) , opts . readEncoding ) ; const newContent = TextDocument . update ( TextDocument . create ( '' , '' , 1 , decoded ) , changes , 2 ) . getText ( ) ; const encoding = await this . encodingService . toResourceEncoding ( opts . writeEncoding , { overwriteEncoding : opts . overwriteEncoding , read : async ( length ) => { const fd = await this . open ( resource , { create : false } ) ; try { const data = new Uint8Array ( length ) ; await this . read ( fd , 0 , data , 0 , length ) ; return data ; } finally { await this . close ( fd ) ; } } } ) ; const encoded = this . encodingService . encode ( newContent , encoding ) ; await this . writeFile ( resource , encoded . buffer , { create : false , overwrite : true } ) ; const stat = await this . stat ( resource ) ; return Object . assign ( stat , { encoding : encoding . encoding } ) ; } catch ( error ) { throw this . toFileSystemProviderError ( error ) ; } } protected toFilePath ( resource ) { return normalize ( FileUri . fsPath ( resource ) ) ; } private toFileSystemProviderError ( error ) { if ( error instanceof FileSystemProviderError ) { return error ; } let code ; switch ( error . code ) { case 'ENOENT' : code = FileSystemProviderErrorCode . FileNotFound ; break ; case 'EISDIR' : code = FileSystemProviderErrorCode . FileIsADirectory ; break ; case 'ENOTDIR' : code = FileSystemProviderErrorCode . FileNotADirectory ; break ; case 'EEXIST' : code = FileSystemProviderErrorCode . FileExists ; break ; case 'EPERM' : case 'EACCES' : code = FileSystemProviderErrorCode . NoPermissions ; break ; default : code = FileSystemProviderErrorCode . Unknown ; } return createFileSystemProviderError ( error , code ) ; } dispose ( ) { this . toDispose . dispose ( ) ; } }