import TrieSearch from 'trie-search' ; import micromatch from 'micromatch' ; import { Arguments , CodedError , Util } from '@kui-shell/core' ; import { FStat , VFS } from '..' ; import { basename , dirname , join } from './posix' ; const uid = - 1 ; const gid = - 1 ; const username = '' ; export interface BaseEntry { mountPath : string ; isDirectory ? : boolean ; isExecutable ? : boolean ; } export type Directory = BaseEntry ; export interface Leaf < D extends any > { mountPath : string ; isExecutable ? : boolean ; data : D ; } export abstract class TrieVFS < D extends any , L extends Leaf < D > = Leaf < D > > implements VFS { public readonly isLocal = false ; public readonly isVirtual = true ; protected readonly prefix = new RegExp ( `^${ this . mountPath }\\/?` ) ; public constructor ( public readonly mountPath = '/kui' , protected readonly trie = new TrieSearch < Directory | L > ( ) ) { } protected abstract loadAsString ( leaf ) ; protected nameForDisplay ( name , entry ) { return name ; } protected viewer ( leaf ) { return 'open' ; } protected isLeaf ( entry ) { return ( entry as L ) . data !== undefined ; } /** Turn an ls-style glob into a nodejs-style regexp */ private glob2RegExp ( filepath ) { return filepath . replace ( /\//g , '\\/' ) . replace ( /\*/g , '.*' ) ; } private dirPattern ( filepath ) { if ( filepath . charAt ( filepath . length - 1 ) === '/' ) { return new RegExp ( `^${ this . glob2RegExp ( filepath ) }[^/]+\\/?$` ) ; } else { return this . dirPattern ( filepath + '/' ) ; } } protected trieGet ( filepath ) { return this . trie . get ( filepath ) ; } /** Looks in the trie for any matches for the given filepath, handling the "contents of directory" case */ private find ( filepath , dashD = false , exact = false ) { const dirPattern = this . dirPattern ( filepath ) ; const matches = this . trieGet ( filepath . replace ( /[*{].*$/ , '' ) ) ; const flexMatches = matches . filter ( _ => exact ? _ . mountPath === filepath : micromatch . isMatch ( _ . mountPath , filepath ) || dirPattern . test ( _ . mountPath ) ) ; if ( dashD ) { return matches . filter ( _ => _ . mountPath === filepath || _ . mountPath === filepath + '/' ) ; } else if ( exact ) { return flexMatches ; } else { return flexMatches . filter ( _ => ! _ . isDirectory || _ . mountPath !== filepath ) ; } } /** Looks in the trie for a single precise match */ private async findExact ( filepath , withData ) { const possibleMatches = this . find ( filepath , false , true ) ; if ( possibleMatches . length > 1 ) { const msg = 'Multiple matches' ; console . error ( msg , possibleMatches ) ; throw new Error ( msg ) ; } else if ( possibleMatches . length === 0 ) { const flexMatches = this . find ( filepath , false , false ) ; if ( filepath === this . mountPath || flexMatches . find ( _ => _ . mountPath . startsWith ( filepath ) ) ) { return { viewer : 'ls' , filepath , fullpath : filepath , size : 0 , isDirectory : true } ; } } else { const entry = possibleMatches [ 0 ] ; const data = this . isLeaf ( entry ) ? await this . loadAsString ( entry ) : undefined ; const isDirectory = ! this . isLeaf ( entry ) ; return { viewer : ! this . isLeaf ( entry ) ? 'open' : this . viewer ( entry ) , filepath : entry . mountPath , fullpath : entry . mountPath , size : data ? data . length : 0 , isDirectory , isExecutable : this . isLeaf ( entry ) && entry . isExecutable , data : ( withData && data ) || undefined } ; } } private async enumerate ( { entries } ) { return Promise . all ( entries . map ( async ( mount ) => { const name = basename ( mount . mountPath ) ; const nameForDisplay = await this . nameForDisplay ( name , mount ) ; const isDir = ! this . isLeaf ( mount ) ; return { name , nameForDisplay , path : mount . mountPath , viewer : this . isLeaf ( mount ) ? this . viewer ( mount ) : undefined , stats : { size : 0 , mtimeMs : 0 , mode : 0 , uid , gid } , dirent : { mount : { isLocal : this . isLocal , mountPath : this . mountPath } , isFile : ! isDir , isDirectory : isDir , isSymbolicLink : false , isSpecial : false , isExecutable : this . isLeaf ( mount ) && mount . isExecutable , permissions : '' , username } } ; } ) ) ; } public async ls ( { parsedOptions } , filepaths ) { return Util . flatten ( await Promise . all ( filepaths . map ( filepath => ( { filepath , entries : this . find ( filepath , parsedOptions . d ) } ) ) . filter ( _ => _ . entries . length > 0 ) . map ( _ => this . enumerate ( _ ) ) ) ) ; } /** Insert filepath into directory */ public cp ( _ , srcFilepaths , dstFilepath ) { return Promise . all ( srcFilepaths . map ( srcFilepath => { const match1 = srcFilepath . match ( /^plugin:\/\/plugin-(.*)\/notebooks\/(.*)\.(md|json)$/ ) ; const match2 = srcFilepath . match ( /^plugin:\/\/client\/notebooks\/(.*)\.(md|json)$/ ) ; const match3 = srcFilepath . match ( /^plugin:\/\/client\/(.*)\.(md|json)$/ ) ; const match = match1 || match2 || match3 ; if ( match ) { const dir = dirname ( dstFilepath ) ; if ( ! this . trieGet ( dir ) ) { throw new Error ( `Directory does not exist: ${ dir }` ) ; } else { const file = match1 ? match1 [ 2 ] : match2 ? match2 [ 1 ] : match3 [ 1 ] ; const extension = '.' + ( match1 ? match1 [ 3 ] : match2 ? match2 [ 2 ] : match3 [ 2 ] ) ; const mountPath = join ( dstFilepath , file + extension ) ; this . trie . map ( mountPath , { mountPath , data : { srcFilepath } } ) ; } } else { throw new Error ( `Unable to copy given source into the VFS: ${ srcFilepath }` ) ; } } ) ) . then ( ( ) => 'ok' ) ; } /** Remove filepath */ public rm ( ) { throw new Error ( 'Unsupported operation' ) ; } /** Fetch contents */ public async fstat ( opts , filepath , withData , enoentOk ) { const entry = ( await this . findExact ( filepath . replace ( /\/$/ , '' ) , withData ) ) || ( await this . findExact ( filepath + '/' , withData ) ) ; if ( ! entry ) { if ( enoentOk ) { } const error = new Error ( `File not found: ${ filepath }` ) ; error . code = 404 ; throw error ; } else { return entry ; } } public async fwrite ( opts , filepath , data ) { throw new Error ( 'Unsupported operation' ) ; } /** Fetch content slice */ public async fslice ( filename , offset , length ) { const entry = await this . findExact ( filename , true ) ; if ( entry . data ) { const buffer = Buffer . from ( entry . data ) ; if ( offset > buffer . length ) { throw new Error ( `notebook fslice: reach file end` ) ; } else { const end = ! length ? buffer . length : length + offset ; return buffer . slice ( offset , end ) . toString ( ) ; } } else { throw new Error ( `fslice: data not found ${ filename }` ) ; } } /** Create a directory/bucket */ public async mkdir ( opts ) { const mountPath = opts . argvNoOptions [ opts . argvNoOptions . indexOf ( 'mkdir' ) + 1 ] ; this . trie . map ( mountPath , { mountPath , isDirectory : true } ) ; } /** Remove a directory/bucket */ public rmdir ( ) { throw new Error ( 'Unsupported operation' ) ; } }