Files
WorkNote/.obsidian/plugins/obsidian-webpage-export-master/scripts/utils/path.ts
2025-04-10 14:07:13 +08:00

873 lines
20 KiB
TypeScript

const pathTools = require('upath');
import { Stats, existsSync } from 'fs';
import { FileSystemAdapter, Notice } from 'obsidian';
import { Utils } from './utils';
import { promises as fs } from 'fs';
import { statSync } from 'fs';
import internal from 'stream';
import { ExportLog } from 'scripts/html-generation/render-log';
import { join } from 'path';
import { homedir } from 'os';
export class Path
{
private static logQueue: { title: string, message: any, type: "info" | "warn" | "error" | "fatal" }[] = [];
private static log(title: string, message: any, type: "info" | "warn" | "error" | "fatal")
{
this.logQueue.push({ title: title, message: message, type: type });
}
public static dequeueLog(): { title: string, message: any, type: "info" | "warn" | "error" | "fatal" }[]
{
let queue = this.logQueue;
this.logQueue = [];
return queue;
}
private _root: string = "";
private _dir: string = "";
private _parent: string = "";
private _base: string = "";
private _ext: string = "";
private _name: string = "";
private _fullPath: string = "";
private _isDirectory: boolean = false;
private _isFile: boolean = false;
private _exists: boolean | undefined = undefined;
private _workingDirectory: string;
private _rawString: string = "";
private _isWindows: boolean = process.platform === "win32";
constructor(path: string, workingDirectory: string = Path.vaultPath.asString)
{
this._workingDirectory = Path.parsePath(workingDirectory).fullPath;
this.reparse(path);
if (this.isAbsolute) this._workingDirectory = "";
}
reparse(path: string): Path
{
let parsed = Path.parsePath(path);
this._root = parsed.root;
this._dir = parsed.dir;
this._parent = parsed.parent;
this._base = parsed.base;
this._ext = parsed.ext;
this._name = parsed.name;
this._fullPath = parsed.fullPath;
this._isDirectory = this._ext == "";
this._isFile = this._ext != "";
this._exists = undefined;
this._rawString = path;
if (this._isWindows)
{
if (this._root.startsWith("http:") || this._root.startsWith("https:"))
{
this._isWindows = false;
this.reparse(this._fullPath.replaceAll("\\", "/"));
}
else
{
this._root = this._root.replaceAll("/", "\\");
this._dir = this._dir.replaceAll("/", "\\");
this._parent = this._parent.replaceAll("/", "\\");
this._fullPath = this._fullPath.replaceAll("/", "\\");
this._workingDirectory = this._workingDirectory.replaceAll("/", "\\");
}
}
this._exists; // force a re-evaluation of the exists property which will also throw an error if the path does not exist
return this;
}
joinString(...paths: string[]): Path
{
return this.copy.reparse(Path.joinStringPaths(this.asString, ...paths));
}
join(...paths: Path[]): Path
{
return new Path(Path.joinStringPaths(this.asString, ...paths.map(p => p.asString)), this._workingDirectory);
}
makeAbsolute(workingDirectory: string | Path = this._workingDirectory): Path
{
if(workingDirectory instanceof Path && !workingDirectory.isAbsolute) throw new Error("workingDirectory must be an absolute path: " + workingDirectory.asString);
if (!this.isAbsolute)
{
this._fullPath = Path.joinStringPaths(workingDirectory.toString(), this.asString);
this._workingDirectory = "";
this.reparse(this.asString);
}
return this;
}
makeForceFolder(): Path
{
if (!this.isDirectory)
{
this.reparse(this.asString + "/");
}
return this;
}
makeNormalized(): Path
{
let fullPath = pathTools.normalizeSafe(this.absolute().asString);
let newWorkingDir = "";
let newFullPath = "";
let reachedEndOfWorkingDir = false;
for (let i = 0; i < fullPath.length; i++)
{
let fullChar = fullPath.charAt(i);
let workingChar = this.workingDirectory.charAt(i);
if (fullChar == workingChar && !reachedEndOfWorkingDir)
{
newWorkingDir += fullChar;
continue;
}
reachedEndOfWorkingDir = true;
newFullPath += fullChar;
}
this.reparse(newFullPath);
this._workingDirectory = newWorkingDir;
return this;
}
normalized(): Path
{
return this.copy.makeNormalized();
}
makeRootAbsolute(): Path
{
if (!this.isAbsolute)
{
if (this._isWindows)
{
if(this._fullPath.contains(":"))
{
this._fullPath = this.asString.substring(this._fullPath.indexOf(":") - 1);
}
else
{
this._fullPath = "\\" + this.asString;
}
}
else
{
this._fullPath = "/" + this.asString;
}
this.reparse(this.asString);
}
return this;
}
setWorkingDirectory(workingDirectory: string): Path
{
this._workingDirectory = workingDirectory;
return this;
}
makeRootRelative(): Path
{
if (this.isAbsolute)
{
if (this._isWindows)
{
// replace the drive letter and colon with nothing
this._fullPath = this.asString.replace(/^.:\/\//i, "").replace(/^.:\//i, "");
this._fullPath = Utils.trimStart(this._fullPath, "\\");
}
else
{
this._fullPath = Utils.trimStart(this._fullPath, "/");
}
this.reparse(this.asString);
}
return this;
}
makeWebStyle(makeWebStyle: boolean = true): Path
{
if (!makeWebStyle) return this;
this._fullPath = Path.toWebStyle(this.asString);
this.reparse(this.asString);
return this;
}
makeWindowsStyle(): Path
{
this._isWindows = true;
this._fullPath = this.asString.replaceAll("/", "\\");
this.reparse(this.asString);
return this;
}
makeUnixStyle(): Path
{
this._isWindows = false;
this._fullPath = this.asString.replaceAll("\\", "/").replace(/^.:\/\//i, "/");
this.reparse(this.asString);
return this;
}
setExtension(extension: string): Path
{
if (!extension.contains(".")) extension = "." + extension;
this._ext = extension;
this._base = this._name + this._ext;
this._fullPath = Path.joinStringPaths(this._dir, this._base);
this.reparse(this._fullPath);
return this;
}
replaceExtension(searchExt: string, replaceExt: string): Path
{
if (!searchExt.contains(".")) searchExt = "." + searchExt;
if (!replaceExt.contains(".")) replaceExt = "." + replaceExt;
this._ext = this._ext.replace(searchExt, replaceExt);
this._base = this._name + this._ext;
this._fullPath = Path.joinStringPaths(this._dir, this._base);
this.reparse(this._fullPath);
return this;
}
// overide the default toString() method
toString(): string
{
return this.asString;
}
/**
* The root of the path
* @example
* "C:/" or "/".
*/
get root(): string
{
return this._root;
}
/**
* The parent directory of the file, or if the path is a directory this will be the full path.
* @example
* "C:/Users/JohnDoe/Documents" or "/home/johndoe/Documents".
*/
get directory(): Path
{
return new Path(this._dir, this._workingDirectory);
}
/**
* Same as dir, but if the path is a directory this will be the parent directory not the full path.
*/
get parent(): Path
{
return new Path(this._parent, this._workingDirectory);
}
/**
* The name of the file or folder including the extension.
* @example
* "file.txt" or "Documents".
*/
get fullName(): string
{
return this._base;
}
/**
* The extension of the file or folder.
* @example
* ".txt" or "".
*/
get extension(): string
{
return this._ext;
}
get extensionName(): string
{
return this._ext.replace(".", "");
}
/**
* The name of the file or folder without the extension.
* @example
* "file" or "Documents".
*/
get basename(): string
{
return this._name;
}
/**
* The depth of the path.
* @example
* "C:/Users/JohnDoe/Documents/file.txt" = 4
* "/home/johndoe/Documents/file.txt" = 4
* "JohnDoe/Documents/Documents" = 2
*/
get depth(): number
{
return this.asString.replaceAll("\\", "/").replaceAll("//", "/").split("/").length - 1;
}
/**
* The original unparsed uncleaned string that was used to create this path.
* @example
* Can be any string: "C:/Users//John Doe/../Documents\file.txt " or ""
*/
get rawString(): string
{
return this._rawString;
}
/**
* The full path of the file or folder.
* @example
* "C:/Users/John Doe/Documents/file.txt"
* "/home/john doe/Documents/file.txt"
* "C:/Users/John Doe/Documents/Documents"
* "/home/john doe/Documents/Documents"
* "relative/path/to/example.txt"
* "relative/path/to/folder"
*/
get asString(): string
{
return this._fullPath;
}
/**
* True if this is a directory.
*/
get isDirectory(): boolean
{
return this._isDirectory;
}
/**
* True if this is an empty path: ".".
* AKA is the path just referencing its working directory.
*/
get isEmpty(): boolean
{
return this.asString == ".";
}
/**
* True if this is a file, not a folder.
*/
get isFile(): boolean
{
return this._isFile;
}
get workingDirectory(): string
{
return this._workingDirectory;
}
/**
* True if the file or folder exists on the filesystem.
*/
get exists(): boolean
{
if(this._exists == undefined)
{
try
{
this._exists = Path.pathExists(this.absolute().asString);
}
catch (error)
{
this._exists = false;
Path.log("Error checking if path exists: " + this.asString, error, "error");
}
}
return this._exists;
}
get stat(): Stats|undefined
{
if(!this.exists) return;
try
{
let stat = statSync(this.absolute().asString);
return stat;
}
catch (error)
{
Path.log("Error getting stat: " + this.asString, error, "error");
return;
}
}
assertExists(): boolean
{
if(!this.exists)
{
new Notice("Error: Path does not exist: \n\n" + this.asString, 5000);
ExportLog.error("Path does not exist: " + this.asString);
}
return this.exists;
}
get isAbsolute(): boolean
{
let asString = this.asString;
if (asString.startsWith("http:") || asString.startsWith("https:")) return true;
if(this._isWindows)
{
if (asString.match(/^[A-Za-z]:[\\|\/|\\\\|\/\/]/)) return true;
if (asString.startsWith("\\") && !asString.contains(":")) return true;
else return false;
}
else
{
if (asString.startsWith("/")) return true;
else return false;
}
}
get isRelative(): boolean
{
return !this.isAbsolute;
}
get copy(): Path
{
return new Path(this.asString, this._workingDirectory);
}
getDepth(): number
{
return this.asString.split("/").length - 1;
}
absolute(workingDirectory: string | Path = this._workingDirectory): Path
{
return this.copy.makeAbsolute(workingDirectory);
}
validate(options: {allowEmpty?: boolean, requireExists?: boolean, allowAbsolute?: boolean, allowRelative?: boolean, allowTildeHomeDirectory?: boolean, allowFiles?: boolean, allowDirectories?: boolean, requireExtentions?: string[]}): {valid: boolean, isEmpty: boolean, error: string}
{
let error = "";
let valid = true;
let isEmpty = this.rawString.trim() == "";
// remove dots from requireExtention
options.requireExtentions = options.requireExtentions?.map(e => e.replace(".", "")) ?? [];
let dottedExtention = options.requireExtentions.map(e => "." + e);
if (!options.allowEmpty && isEmpty)
{
error += "Path cannot be empty\n";
valid = false;
}
else if (options.allowEmpty && isEmpty)
{
return { valid: true, isEmpty: isEmpty, error: "" };
}
if (options.requireExists && !this.exists)
{
error += "Path does not exist";
valid = false;
}
else if (!options.allowTildeHomeDirectory && this.asString.startsWith("~"))
{
error += "Home directory with tilde (~) is not allowed";
valid = false;
}
else if (!options.allowAbsolute && this.isAbsolute)
{
error += "Path cannot be absolute";
valid = false;
}
else if (!options.allowRelative && this.isRelative)
{
error += "Path cannot be relative";
valid = false;
}
else if (!options.allowFiles && this.isFile)
{
error += "Path cannot be a file";
valid = false;
}
else if (!options.allowDirectories && this.isDirectory)
{
error += "Path cannot be a directory";
valid = false;
}
else if (options.requireExtentions.length > 0 && !options.requireExtentions.includes(this.extensionName) && !isEmpty)
{
error += "Path must be: " + dottedExtention.join(", ");
valid = false;
}
return { valid: valid, isEmpty: isEmpty, error: error };
}
async createDirectory(): Promise<boolean>
{
if (!this.exists)
{
let path = this.absolute().directory.asString;
try
{
await fs.mkdir(path, { recursive: true });
}
catch (error)
{
Path.log("Error creating directory: " + path, error, "error");
return false;
}
}
return true;
}
async readFileString(encoding: "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "base64url" | "latin1" | "binary" | "hex" = "utf-8"): Promise<string|undefined>
{
if(!this.exists || this.isDirectory) return;
try
{
let data = await fs.readFile(this.absolute().asString, { encoding: encoding });
return data;
}
catch (error)
{
Path.log("Error reading file: " + this.asString, error, "error");
return;
}
}
async readFileBuffer(): Promise<Buffer|undefined>
{
if(!this.exists || this.isDirectory) return;
try
{
let data = await fs.readFile(this.absolute().asString);
return data;
}
catch (error)
{
Path.log("Error reading file buffer: " + this.asString, error, "error");
return;
}
}
async writeFile(data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView> | internal.Stream, encoding: "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "base64url" | "latin1" | "binary" | "hex" = "utf-8"): Promise<boolean>
{
if (this.isDirectory) return false;
try
{
await fs.writeFile(this.absolute().asString, data, { encoding: encoding });
return true;
}
catch (error)
{
let dirExists = await this.createDirectory();
if (!dirExists) return false;
try
{
await fs.writeFile(this.absolute().asString, data, { encoding: encoding });
return true;
}
catch (error)
{
Path.log("Error writing file: " + this.asString, error, "error");
return false;
}
}
}
async delete(recursive: boolean = false): Promise<boolean>
{
if (!this.exists) return false;
try
{
await fs.rm(this.absolute().asString, { recursive: recursive });
return true;
}
catch (error)
{
Path.log("Error deleting file: " + this.asString, error, "error");
return false;
}
}
public static fromString(path: string): Path
{
return new Path(path);
}
private static parsePath(path: string): { root: string, dir: string, parent: string, base: string, ext: string, name: string, fullPath: string }
{
let args = path.split("?")[1] ?? "";
path = path.split("?")[0];
if (process.platform === "win32")
{
if (path.startsWith("~"))
{
path = path.replace("~", homedir());
}
}
try
{
path = decodeURI(path);
}
catch (trash)
{
try
{
path = decodeURI(path.replaceAll("%", ""));
}
catch (e)
{
this.log("Could not decode path:" + path, e, "info");
}
}
let parsed = pathTools.parse(path) as { root: string, dir: string, base: string, ext: string, name: string };
if (parsed.ext.contains(" "))
{
parsed.ext = "";
}
if(parsed.name.endsWith(" "))
{
parsed.name += parsed.ext;
parsed.ext = "";
}
let parent = parsed.dir;
let fullPath = "";
if(path.endsWith("/") || path.endsWith("\\") || parsed.ext == "")
{
if (path.endsWith("/") || path.endsWith("\\")) path = path.substring(0, path.length - 1);
parsed.dir = pathTools.normalizeSafe(path);
let items = parsed.dir.split("/");
parsed.name = items[items.length - 1];
parsed.base = parsed.name;
parsed.ext = "";
fullPath = parsed.dir;
}
else
{
fullPath = pathTools.join(parent, parsed.base);
}
if (args && args.trim() != "") fullPath += "?" + args;
if(fullPath.startsWith("http:")) parsed.root = "http://";
else if(fullPath.startsWith("https:")) parsed.root = "https://";
// make sure that protocols and windows drives use two slashes
parsed.dir = parsed.dir.replace(/[:][\\/](?![\\/])/g, "://");
parent = parsed.dir;
fullPath = fullPath.replace(/[:][\\/](?![\\/])/g, "://");
return { root: parsed.root, dir: parsed.dir, parent: parent, base: parsed.base, ext: parsed.ext, name: parsed.name, fullPath: fullPath };
}
private static pathExists(path: string): boolean
{
return existsSync(path);
}
private static joinStringPaths(...paths: string[]): string
{
let joined = pathTools.join(...paths);
if (joined.startsWith("http"))
{
joined = joined.replaceAll(":/", "://");
}
try
{
return decodeURI(joined);
}
catch (e)
{
this.log("Could not decode joined paths: " + joined, e, "info");
return joined;
}
}
public static joinPath(...paths: Path[]): Path
{
return new Path(Path.joinStringPaths(...paths.map(p => p.asString)), paths[0]._workingDirectory);
}
public static joinStrings(...paths: string[]): Path
{
return new Path(Path.joinStringPaths(...paths));
}
/**
* @param from The source path / working directory
* @param to The destination path
* @returns The relative path to the destination from the source
*/
public static getRelativePath(from: Path, to: Path, useAbsolute: boolean = false): Path
{
let fromUse = useAbsolute ? from.absolute() : from;
let toUse = useAbsolute ? to.absolute() : to;
let relative = pathTools.relative(fromUse.directory.asString, toUse.asString);
let workingDir = from.absolute().directory.asString;
return new Path(relative, workingDir);
}
public static getRelativePathFromVault(path: Path, useAbsolute: boolean = false): Path
{
return Path.getRelativePath(Path.vaultPath, path, useAbsolute);
}
private static vaultPathCache: Path | undefined = undefined;
static get vaultPath(): Path
{
if (this.vaultPathCache != undefined) return this.vaultPathCache;
let adapter = app.vault.adapter;
if (adapter instanceof FileSystemAdapter)
{
let basePath = adapter.getBasePath() ?? "";
this.vaultPathCache = new Path(basePath, "");
return this.vaultPathCache;
}
throw new Error("Vault path could not be determined");
}
private static vaultConfigDirCache: Path | undefined = undefined;
static get vaultConfigDir(): Path
{
if (this.vaultConfigDirCache == undefined)
{
this.vaultConfigDirCache = new Path(app.vault.configDir, "");
}
return this.vaultConfigDirCache;
}
static get emptyPath(): Path
{
return new Path("", "");
}
static get rootPath(): Path
{
return new Path("/", "");
}
static toWebStyle(path: string): string
{
return path.replaceAll(" ", "-").replaceAll(/-{2,}/g, "-").toLowerCase();
}
static equal(path1: string, path2: string): boolean
{
let path1Parsed = new Path(path1).makeUnixStyle().makeWebStyle().asString;
let path2Parsed = new Path(path2).makeUnixStyle().makeWebStyle().asString;
return path1Parsed == path2Parsed;
}
public static async getAllEmptyFoldersRecursive(folder: Path): Promise<Path[]>
{
if (!folder.isDirectory) throw new Error("folder must be a directory: " + folder.asString);
let folders: Path[] = [];
let folderFiles = await fs.readdir(folder.asString);
for (let i = 0; i < folderFiles.length; i++)
{
let file = folderFiles[i];
let path = folder.joinString(file);
if ((await fs.stat(path.asString)).isDirectory())
{
let subFolders = await this.getAllEmptyFoldersRecursive(path);
if (subFolders.length == 0)
{
let subFiles = await fs.readdir(path.asString);
if (subFiles.length == 0) folders.push(path);
}
else
{
folders.push(...subFolders);
}
}
}
return folders;
}
public static async getAllFilesInFolderRecursive(folder: Path): Promise<Path[]>
{
if (!folder.isDirectory) throw new Error("folder must be a directory: " + folder.asString);
let files: Path[] = [];
let folderFiles = await fs.readdir(folder.asString);
for (let i = 0; i < folderFiles.length; i++)
{
let file = folderFiles[i];
let path = folder.joinString(file);
ExportLog.progress(i, folderFiles.length, "Finding Old Files", "Searching: " + folder.asString, "var(--color-yellow)");
if ((await fs.stat(path.asString)).isDirectory())
{
files.push(...await this.getAllFilesInFolderRecursive(path));
}
else
{
files.push(path);
}
}
return files;
}
}