You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have implemented the following to handle paths easier in our app.
I have focused on providing convenience and make the using code cleaner, without sacrificing performance by doing too many under-the-hood operations.
I do not claim that this is how it should be, but what do people think of the idea of making a class based API for doing file system operations?
import{Capacitor,CopyOptions,FileAppendOptions,FileAppendResult,FileDeleteOptions,FileDeleteResult,FileReadOptions,FileReadResult,FilesystemDirectory,FilesystemEncoding,FileWriteOptions,FileWriteResult,MkdirOptions,MkdirResult,Plugins,ReaddirOptions,RenameOptions,RmdirOptions,StatResult,}from"@capacitor/core"const{ Filesystem }=PluginsexportenumInfoEntryType{Directory="directory",File="file",}exportinterfaceInfoResult{exists: booleantype?: InfoEntryTypeerror?: Error}/** * Utility class for easy handling of paths. * Can be constructed with non-existing paths. */exportclassPathHandle{readonlyrelativePath: stringreadonlydirectory: FilesystemDirectory/** * Constructor * * @param relativePath * @param directory */constructor(relativePath: string,directory: FilesystemDirectory){// Clean excessive slashes in pathletpathLength=relativePath.lengthletcleanedRelativePath=relativePath.replace("//","/")while(pathLength>cleanedRelativePath.length){pathLength=cleanedRelativePath.lengthcleanedRelativePath=cleanedRelativePath.replace("//","/")}this.relativePath=cleanedRelativePaththis.directory=directory}//# region Computed properties/** * Get path stats. */getstat(): Promise<StatResult>{returnFilesystem.stat({directory: this.directory,path: this.relativePath,})}/** * Get the full `file://` uri of the file. */geturi(): Promise<string>{// Note: If this is slow, we can probably make it synchronous by caching the roots of the directories.returnFilesystem.getUri({directory: this.directory,path: this.relativePath,}).then((value)=>value.uri)}/** * Get the local `http://` url for use in the UI. */geturl(): Promise<string>{returnthis.uri.then((uri)=>Capacitor.convertFileSrc(uri))}/** * Whether the relative path for this handle is root at its respective {@link FilesystemDirectory}. */getisRelativeRoot(): boolean{returnthis.relativePath===""||this.relativePath.replace("/","")===""}/** * Return a {@link PathHandle} to the directory containing this path. * * If already at the root of the current {@link FilesystemDirectory}, an error is thrown. */getparentDirectoryHandle(): PathHandle{if(this.isRelativeRoot){throwError("Already at relative root.")}constbase=!this.relativePath.endsWith("/")
? this.relativePath
: this.relativePath.slice(0,-1)constrelativePath=base.slice(0,base.lastIndexOf("/"))returnnewPathHandle(relativePath,this.directory)}/** * Gets derived information for path in one go. * * The reason this functionality is done in one, is that they are cheap to do * together, and the common workflow is to check for existence and then type. * In that regard this method saves calls to {@link stat} */getinfo(): Promise<InfoResult>{returnthis.stat.then((statResult)=>{constresult: InfoResult={exists: Object.entries(InfoEntryType).some((entry)=>entry[1]===statResult.type),}if(result.exists){result.type=statResult.typeasInfoEntryType}returnresult}).catch((reason)=>{// The reason this warning is here is to catch the actual error and see// if it is something that does not mean that the path is not there.console.warn("Non-existing file warning",reason)constresult: InfoResult={exists: false,error: reason,}returnresult})}//#endregion//#region Methods/** * Append another path to the current one (treating it as a directory), and return a {@link PathHandle} for it. * * For a checked version of this, see {@link checkedConcat}. * * @param pathAddition */concat(pathAddition: string): PathHandle{constcurrentPathCleaned=this.relativePath.endsWith("/")
? this.relativePath.slice(0,-1)
: this.relativePathconstpathAdditionCleaned=pathAddition.startsWith("/") ? pathAddition.slice(1) : pathAdditionreturnnewPathHandle(`${currentPathCleaned}/${pathAdditionCleaned}`,this.directory)}/** * Checked version of {@link concat} that verifies that the current path or nearest existing ancestor is not a file. * * @param pathAddition */asynccheckedConcat(pathAddition: string): Promise<PathHandle>{constinfo=awaitthis.infoif(info.exists&&info.type===InfoEntryType.File){thrownewError("Cannot concat path to file.")}elseif(!info.exists){// Look for an existing ancestorletancestor=this.parentDirectoryHandleletancestorInfo=awaitancestor.infowhile(!ancestorInfo.exists){ancestor=ancestor.parentDirectoryHandleancestorInfo=awaitancestor.info}// By here we have an ancestor that exists so we can check for compatibilityif(ancestorInfo.type===InfoEntryType.File){thrownewError("Cannot concat path to file.")}}returnthis.concat(pathAddition)}/** * Assumes that the path is a file and reads it. * * Returns data as a {@link string} according to the encoding (base64 if undefined) * like {@link Filesystem.readFile}. * * Alternatively the file can be fetched as a {@link Blob} by using the Fetch * API and the {@link url} of a path. * * @param encoding */asyncreadFile(encoding: FilesystemEncoding|undefined=undefined): Promise<FileReadResult>{constoptions: FileReadOptions={directory: this.directory,path: this.relativePath,}if(encoding!==undefined){options.encoding=encoding}returnFilesystem.readFile(options)}/** * Writes data to the path as a file. * * Will overwrite an existing file. * * @param data The data to write. * @param encoding The encoding to write the file in. * Works like the one of the same name in {@link FileWriteOptions}. */asyncwriteFile(data: string,encoding: FilesystemEncoding|undefined=undefined): Promise<FileWriteResult>{constoptions: FileWriteOptions={path: this.relativePath,data: data,directory: this.directory,recursive: true,}if(encoding!==undefined){options.encoding=encoding}returnFilesystem.writeFile(options)}/** * Assumes the path is a file, and appends data to it. * * @param data The data to write. * @param encoding The encoding to write the file in. * Works like the one of the same name in {@link FileWriteOptions}. */asyncappendFile(data: string,encoding: FilesystemEncoding|undefined=undefined): Promise<FileAppendResult>{constoptions: FileAppendOptions={path: this.relativePath,data: data,directory: this.directory,}if(encoding!==undefined){options.encoding=encoding}returnFilesystem.writeFile(options)}/** * Assumes that the path is a directory and reads it. */asyncreadDirectory(): Promise<string[]>{constoptions: ReaddirOptions={directory: this.directory,path: this.relativePath,}returnFilesystem.readdir(options).then((readdirResult)=>readdirResult.files)}/** * Creates the paths as a directory. */asynccreateDirectory(): Promise<MkdirResult>{constoptions: MkdirOptions={directory: this.directory,path: this.relativePath,recursive: true,}returnFilesystem.mkdir(options)}/** * Deletes whatever the path points at. * * If the path points to a directory it will be deleted recursively. * If the path points to nothing, then nothing will happen. * * @return */asyncdelete(): Promise<FileDeleteResult>{constinfo=awaitthis.infoif(info.exists){if(info.type===InfoEntryType.File){constoptions: FileDeleteOptions={directory: this.directory,path: this.relativePath,}returnFilesystem.deleteFile(options)}else{constoptions: RmdirOptions={directory: this.directory,path: this.relativePath,recursive: true,}returnFilesystem.rmdir(options)}}else{returnfalse}}/** * Helper to construct arguments for {@link rename} and {@link copy}. * * @param targetRelativePath * @param targetDirectory * @private */privaterenameOrCopyOptions(targetRelativePath: string,targetDirectory: FilesystemDirectory): RenameOptions|CopyOptions{return{from: this.relativePath,to: targetRelativePath,directory: this.directory,toDirectory: targetDirectory,}}/** * Rename the path target. * @param targetRelativePath * @param targetDirectory * @return A {@link Promise} of a {@link PathHandle} for the target path. */asyncrename(targetRelativePath: string,targetDirectory: FilesystemDirectory): Promise<PathHandle>{returnFilesystem.rename(this.renameOrCopyOptions(targetRelativePath,targetDirectory)).then(()=>newPathHandle(targetRelativePath,targetDirectory))}/** * Copy the path target. * @param targetRelativePath * @param targetDirectory * @return A {@link Promise} of a {@link PathHandle} for the target path. */asynccopy(targetRelativePath: string,targetDirectory: FilesystemDirectory): Promise<PathHandle>{returnFilesystem.copy(this.renameOrCopyOptions(targetRelativePath,targetDirectory)).then(()=>newPathHandle(targetRelativePath,targetDirectory))}}exportdefault{
InfoEntryType,
PathHandle,}
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
I have implemented the following to handle paths easier in our app.
I have focused on providing convenience and make the using code cleaner, without sacrificing performance by doing too many under-the-hood operations.
I do not claim that this is how it should be, but what do people think of the idea of making a class based API for doing file system operations?
Beta Was this translation helpful? Give feedback.
All reactions