importpathLibraryfrom"path";import{execSync}from"./cmd";importfsfrom"fs";importloggerfrom"./log";importmimefrom"mime";import{Repo}from"./git";/** * If a path string has a postfixed slash, remove it. */constremovePostfixedSlash=(pathString: string)=>{if(pathString[pathString.length-1]==="/"){returnpathString.slice(0,pathString.length-1);}returnpathString;};/** * A Path is the path to a file or directory on a system. */classPath{// These params are immutable, so it's safe to store both// and access them directly.publicpathArray: string[]=[];publicrelativePathArray: string[]=[];publicrelativePathString: String="";privatepathString: string="";/** * Construct a Path. * Do not call this directly. * * Works for both relative and absolute paths, fixing them into absolute paths as needed. * this should only be called by the static methods. */constructor(pathString: string){letnormalizedPath=pathLibrary.normalize(pathString);letabsolutePath=normalizedPath;if(!pathLibrary.isAbsolute(normalizedPath)){absolutePath=pathLibrary.resolve(process.cwd(),normalizedPath);}normalizedPath=removePostfixedSlash(normalizedPath);absolutePath=removePostfixedSlash(absolutePath);this.relativePathString=normalizedPath;this.relativePathArray=normalizedPath.split("/").slice(1).filter((p)=>p.length);this.pathString=absolutePath;this.pathArray=absolutePath.split("/").slice(1).filter((p)=>p.length);}/** * Create a path. Call this to make a path. * @param maybePathString either an existing Path or a string. */staticcreate(maybePathString: Path|string){if(typeofmaybePathString==="string"){returnnewPath(maybePathString);}elseif(maybePathStringinstanceofPath){returnmaybePathString;}thrownewError(`Path provided must be a string or Path, given ${maybePathString}`);}/** * Returns a new Path with the proper full path. */staticfromUrl(url: string,websiteName: string,sourcePath: string){returnnewPath(url.replace(websiteName,sourcePath));}toString(){returnthis.pathString;}/** * This path is equal to another path if they have the same pathString. * If the other path is a string, this is still true.. * @param otherPath The other path to compare this path to. */equals(otherPath: Path|string){returnthis.pathString===Path.create(otherPath).pathString;}/** * Get the name of this file, including the extension. */getname(){returnthis.pathArray[this.pathArray.length-1];}/** * Get the mimeType of this file based on its path string. */getmimeType(){returnmime.getType(this.pathString)||"text/plain";}/** * Get this file's extension. * If we don't have an extension provided, we determine it from disk. */getextension(): string|null{// we always fetch [1], because if the file has multiple extensions// we ignore the second and only care about the first.constext=pathLibrary.extname(this.pathString).split(".")[1]??null;if(ext){returnext;}elseif(this.exists()&&this.isDirectory()){return"dir";}returnext;}/** * Get the parent of this path. */getparent(){returnPath.create("/"+this.pathArray.slice(0,this.pathArray.length-1).join("/"));}/** * Is this path the root path of the directory? */isRootPath(){returnthis.pathArray.length===0;}/** * Fetch and construct the repo if we don't have one yet. * Returns null if we can't find a repo for the page. */private__repo(): Repo|null{if(this.isRootPath()){returnnull;}if(!this.isDirectory()){returnthis.parent.__repo();}constgitDir=this.join("/.git");if(gitDir.exists()){returnRepo.create(this);}returnthis.parent.__repo();}/** * Get the git repo that this path is a member of. */getrepo(){if(!this.exists()){returnnull;}constrootRepo=this.__repo();returnrootRepo??undefined;}/** * Copy the file or directory at this path to another path. * If the path is not a subdir of this path, throw an error. */copy(fromPath: Path|string,toPath: Path|string){constfrom=Path.create(fromPath);constto=Path.create(toPath);// TODO: this would be a good safeguard to add, but it doesn't work?// if (!this.contains(from)) {// throw new Error(// `Cannot copy ${from.toString()} to ${to.toString()} because it is not a subdirectory of ${this.toString()}`// );// }try{execSync(`cp -r ${from.pathString}${to.pathString}`,{cwd: this.toString(),});}catch(e){console.log("error copying directory",e);}}/** * Move the file or directory at this path to another path. * If the path is not a subdir of this path, throw an error. */move(fromPath: Path|string,toPath: Path|string,{ force =false}: {force: boolean}={force: false,}){console.log("moving from ",{from: fromPath,to: toPath});try{// Avoid normalizing the paths by using the originals providedexecSync(`rsync -av --delete ${fromPath}${toPath}`,{cwd: this.toString(),});}catch(e){console.log("error moving directory",e);}}/** * get this path's position relative to another path or string * ASSUME that the other paths, if defined, are the prefix of this one. * REMOVE 'maybeOtherPath' from this path's string. * If 'maypeReplaceWithPath' is defined, append it. */relativeTo(maybeOtherPath: Path|string,maybeReplaceWithPath=""){constotherPath=Path.create(maybeOtherPath);constreplaceWith=maybeReplaceWithPath.toString();if(!this.pathString.startsWith(otherPath.toString())){thrownewError(`Path we are removing is no present on the current path. Was looking for path: ${this.pathString} relative to ${maybeOtherPath}`);}letresultingPathString=this.pathString;if(otherPath){if(otherPath.toString()===resultingPathString){resultingPathString="";}resultingPathString=resultingPathString.replace(`${otherPath.toString()}`,"");}if(maybeReplaceWithPath){resultingPathString=replaceWith.toString()+resultingPathString;}returnPath.create(resultingPathString);}/** * Does the file at this path exist? */exists(){returnfs.existsSync(this.pathString);}/** * Accepts the extension WITHOUT a prefixed period */addExtension(extension: string){returnnewPath(`${this.toString()}.${extension}`);}/** * Replace the path's extension with a new one. * @argument extension the extension WITHOUT a prefixed period * if undefined, the extension is dropped * if the path has multiple extensions, the last one is dropped */replaceExtension(extension?: string){letnewPathWithExtension=this.pathString;if(!newPathWithExtension.includes(".")&&extension){newPathWithExtension+=`.${extension}`;}else{newPathWithExtension=newPathWithExtension.replace(/\.[a-zA-Z0-9]+$/,extension ? `.${extension}` : "");}returnnewPath(newPathWithExtension);}/** * Is this path a directory? */isDirectory({ noFSOperation }={noFSOperation: false}){if(noFSOperation){constextension=this.pathString.split(".")[1];return!extension;}if(!this.exists()){thrownewError(`Cannot check if a path is a directory if it doesn't exist. Was looking for path: ${this.pathString}`);}returnfs.lstatSync(this.pathString).isDirectory();}// read this path as a utf8 stringreadString(){returnfs.readFileSync(this.pathString,"utf8");}/** * Write a string to the file at this path, * creating the file if it doesn't exist. */writeString(str: string){// if (this.isDirectory({ noFSOperation: true })) {// throw new Error("Cannot write a string to a non-directory file");// }this.make();fs.writeFileSync(this.pathString,str);}readBinary(){// const chunks = [];letbuffer;// const readStream = fs.createReadStream(this.pathString);// readStream.on('data', (chunk) => {// chunks.push(chunk);// });// readStream.on('end', () => {// buffer = Buffer.concat(chunks);// });// while (!buffer) {// }returnbuffer;}writeBinary(){// const outStream = fs.createWriteStream(outPath);// inStream.pipe(outStream);// TODO// await new Promise((resolve) => {// outStream.on('close', resolve);// });}/** * Read an entire directory, returning all of the paths in the directory. */readDirectory(){if(!this.isDirectory()){thrownewError(`Cannot read directory '${this.pathString}' because it is not a directory`);}letnormalizedPathString=this.pathString;// if the path doesn't end in a slash, add oneif(normalizedPathString[normalizedPathString.length-1]!=="/"){normalizedPathString+="/";}returnfs.readdirSync(normalizedPathString).map((fileName)=>newPath(`${normalizedPathString}${fileName}`));}/** * Join the provided next part of the path to this path, * producing the conjunction of the two. */join(nextPart: Path|string){logger.file("Joining path",this.pathString,"with",nextPart.toString());returnnewPath(this.pathString+nextPart.toString());}/** * Determines whether this path contains the other. * This implementation is a bit strange and title might not be accurate. */contains(maybeOtherPathString: Path|string){constotherPath=Path.create(maybeOtherPathString);if(this.pathArray.length>otherPath.pathArray.length||this.pathString===otherPath.pathString){returnfalse;}for(leti=0;i<this.pathArray.length;i++){if(otherPath.pathArray[i]!==this.pathArray[i]){returnfalse;}}returntrue;}/** * Make this path exist, creating any parent directories along the way. * Assume the path is a file unless provided that it's a directory. */make(settings?: {isDirectory?: boolean}){constisDirectory=settings?.isDirectory;if(this.exists()){console.log(".make: File already exists at path ",this.pathString);returnthis;}// If this file is supposed to have a parent, then,// by definition, its parent must be a directory.// Make sure the parent directory exists.if(!this.parent.exists()){console.log("The parent of this path",this.toString(),"does not exist. Making it: ",this.parent.toString());this.parent.make({isDirectory: true});}if(isDirectory){console.log("Making directory at",this.pathString);fs.mkdirSync(this.pathString);}else{console.log("Making file at",this.pathString);fs.writeFileSync(this.pathString,"");}returnthis;}/** * Watch this file for any action. * Invoke a callback listener if the file changes. * * NOTE: We currently don't listen for file change events. * Those cause this to fail because we pass `this` through. */watch(callback: Function){if(!this.exists()){thrownewError(`Cannot watch path '${this.pathString}' because it does not exist`);}constwatcher=fs.watch(this.pathString,(eventType,filename)=>{// TODO: filename could be different if the file moves?callback(eventType,this);});return()=>watcher.close();}}export{Path};