618 lines
18 KiB
TypeScript
618 lines
18 KiB
TypeScript
import * as plugins from './plugins.js';
|
||
import * as interpreter from './interpreter.js';
|
||
|
||
import { SmartFile } from './classes.smartfile.js';
|
||
|
||
import * as memory from './memory.js';
|
||
import type { StreamFile } from './classes.streamfile.js';
|
||
/*===============================================================
|
||
============================ Checks =============================
|
||
===============================================================*/
|
||
|
||
/**
|
||
*
|
||
* @param filePath
|
||
* @returns {boolean}
|
||
*/
|
||
export const fileExistsSync = (filePath): boolean => {
|
||
let fileExistsBool: boolean = false;
|
||
try {
|
||
plugins.fsExtra.readFileSync(filePath);
|
||
fileExistsBool = true;
|
||
} catch (err) {
|
||
fileExistsBool = false;
|
||
}
|
||
return fileExistsBool;
|
||
};
|
||
|
||
/**
|
||
*
|
||
* @param filePath
|
||
* @returns {any}
|
||
*/
|
||
export const fileExists = async (filePath): Promise<boolean> => {
|
||
const done = plugins.smartpromise.defer<boolean>();
|
||
plugins.fs.access(filePath, 4, (err) => {
|
||
err ? done.resolve(false) : done.resolve(true);
|
||
});
|
||
return done.promise;
|
||
};
|
||
|
||
/**
|
||
* Checks if given path points to an existing directory
|
||
*/
|
||
export const isDirectory = (pathArg: string): boolean => {
|
||
try {
|
||
return plugins.fsExtra.statSync(pathArg).isDirectory();
|
||
} catch (err) {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Checks if given path points to an existing directory
|
||
*/
|
||
export const isDirectorySync = (pathArg: string): boolean => {
|
||
try {
|
||
return plugins.fsExtra.statSync(pathArg).isDirectory();
|
||
} catch (err) {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Checks if a given path points to an existing file
|
||
*/
|
||
export const isFile = (pathArg): boolean => {
|
||
return plugins.fsExtra.statSync(pathArg).isFile();
|
||
};
|
||
|
||
/*===============================================================
|
||
============================ FS ACTIONS =========================
|
||
===============================================================*/
|
||
|
||
/**
|
||
* copies a file from A to B on the local disk
|
||
*/
|
||
export const copy = async (fromArg: string, toArg: string): Promise<boolean> => {
|
||
const done = plugins.smartpromise.defer<boolean>();
|
||
plugins.fsExtra.copy(fromArg, toArg, {}, (err) => {
|
||
if (err) {
|
||
throw new Error(`Could not copy from ${fromArg} to ${toArg}: ${err}`);
|
||
}
|
||
done.resolve(true);
|
||
});
|
||
return done.promise;
|
||
};
|
||
|
||
/**
|
||
* copies a file SYNCHRONOUSLY from A to B on the local disk
|
||
*/
|
||
export const copySync = (fromArg: string, toArg: string): boolean => {
|
||
plugins.fsExtra.copySync(fromArg, toArg);
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* ensures that a directory is in place
|
||
*/
|
||
export const ensureDir = async (dirPathArg: string) => {
|
||
await plugins.fsExtra.ensureDir(dirPathArg);
|
||
};
|
||
|
||
/**
|
||
* ensures that a directory is in place
|
||
*/
|
||
export const ensureDirSync = (dirPathArg: string) => {
|
||
plugins.fsExtra.ensureDirSync(dirPathArg);
|
||
};
|
||
|
||
/**
|
||
* ensure an empty directory
|
||
* @executes ASYNC
|
||
*/
|
||
export const ensureEmptyDir = async (dirPathArg: string) => {
|
||
await plugins.fsExtra.ensureDir(dirPathArg);
|
||
await plugins.fsExtra.emptyDir(dirPathArg);
|
||
};
|
||
|
||
/**
|
||
* ensure an empty directory
|
||
* @executes SYNC
|
||
*/
|
||
export const ensureEmptyDirSync = (dirPathArg: string) => {
|
||
plugins.fsExtra.ensureDirSync(dirPathArg);
|
||
plugins.fsExtra.emptyDirSync(dirPathArg);
|
||
};
|
||
|
||
/**
|
||
* ensures that a file is on disk
|
||
* @param filePath the filePath to ensureDir
|
||
* @param the fileContent to place into a new file in case it doesn't exist yet
|
||
* @returns Promise<void>
|
||
* @exec ASYNC
|
||
*/
|
||
export const ensureFile = async (filePathArg, initFileStringArg): Promise<void> => {
|
||
ensureFileSync(filePathArg, initFileStringArg);
|
||
};
|
||
|
||
/**
|
||
* ensures that a file is on disk
|
||
* @param filePath the filePath to ensureDir
|
||
* @param the fileContent to place into a new file in case it doesn't exist yet
|
||
* @returns Promise<void>
|
||
* @exec SYNC
|
||
*/
|
||
export const ensureFileSync = (filePathArg: string, initFileStringArg: string): void => {
|
||
if (fileExistsSync(filePathArg)) {
|
||
return null;
|
||
} else {
|
||
memory.toFsSync(initFileStringArg, filePathArg);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* removes a file or folder from local disk
|
||
*/
|
||
export const remove = async (pathArg: string): Promise<void> => {
|
||
await plugins.fsExtra.remove(pathArg);
|
||
};
|
||
|
||
/**
|
||
* removes a file SYNCHRONOUSLY from local disk
|
||
*/
|
||
export const removeSync = (pathArg: string): void => {
|
||
plugins.fsExtra.removeSync(pathArg);
|
||
};
|
||
|
||
/**
|
||
* removes an array of filePaths from disk
|
||
*/
|
||
export const removeMany = async (filePathArrayArg: string[]) => {
|
||
const promiseArray: Array<Promise<void>> = [];
|
||
for (const filePath of filePathArrayArg) {
|
||
promiseArray.push(remove(filePath));
|
||
}
|
||
await Promise.all(promiseArray);
|
||
};
|
||
|
||
/**
|
||
* like removeFilePathArray but SYNCHRONOUSLY
|
||
*/
|
||
export const removeManySync = (filePathArrayArg: string[]): void => {
|
||
for (const filePath of filePathArrayArg) {
|
||
removeSync(filePath);
|
||
}
|
||
};
|
||
|
||
/*===============================================================
|
||
============================ Write/Read =========================
|
||
===============================================================*/
|
||
|
||
/**
|
||
* reads a file content to an object
|
||
* good for JSON, YAML, TOML, etc.
|
||
* @param filePathArg
|
||
* @param fileTypeArg
|
||
* @returns {any}
|
||
*/
|
||
export const toObjectSync = (filePathArg, fileTypeArg?) => {
|
||
const fileString = plugins.fsExtra.readFileSync(filePathArg, 'utf8');
|
||
let fileType;
|
||
fileTypeArg ? (fileType = fileTypeArg) : (fileType = interpreter.filetype(filePathArg));
|
||
try {
|
||
return interpreter.objectFile(fileString, fileType);
|
||
} catch (err) {
|
||
err.message = `Failed to read file at ${filePathArg}` + err.message;
|
||
throw err;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* reads a file content to a String
|
||
*/
|
||
export const toStringSync = (filePath: string): string => {
|
||
const encoding = plugins.smartmime.getEncodingForPathSync(filePath);
|
||
let fileString: string | Buffer = plugins.fsExtra.readFileSync(filePath, encoding);
|
||
if (Buffer.isBuffer(fileString)) {
|
||
fileString = fileString.toString('binary');
|
||
}
|
||
return fileString;
|
||
};
|
||
|
||
export const toBuffer = async (filePath: string): Promise<Buffer> => {
|
||
return plugins.fsExtra.readFile(filePath);
|
||
};
|
||
|
||
export const toBufferSync = (filePath: string): Buffer => {
|
||
return plugins.fsExtra.readFileSync(filePath);
|
||
};
|
||
|
||
/**
|
||
* Creates a Readable Stream from a file path.
|
||
* @param filePath The path to the file.
|
||
* @returns {fs.ReadStream}
|
||
*/
|
||
export const toReadStream = (filePath: string): plugins.fs.ReadStream => {
|
||
if (!fileExistsSync(filePath)) {
|
||
throw new Error(`File does not exist at path: ${filePath}`);
|
||
}
|
||
return plugins.fsExtra.createReadStream(filePath);
|
||
};
|
||
|
||
export const fileTreeToHash = async (dirPathArg: string, miniMatchFilter: string) => {
|
||
const fileTreeObject = await fileTreeToObject(dirPathArg, miniMatchFilter);
|
||
let combinedString = '';
|
||
for (const smartfile of fileTreeObject) {
|
||
combinedString += await smartfile.getHash();
|
||
}
|
||
const hash = await plugins.smarthash.sha256FromString(combinedString);
|
||
return hash;
|
||
};
|
||
|
||
/**
|
||
* creates a smartfile array from a directory
|
||
* @param dirPathArg the directory to start from
|
||
* @param miniMatchFilter a minimatch filter of what files to include
|
||
*/
|
||
export const fileTreeToObject = async (dirPathArg: string, miniMatchFilter: string) => {
|
||
// handle absolute miniMatchFilter
|
||
let dirPath: string;
|
||
if (plugins.path.isAbsolute(miniMatchFilter)) {
|
||
dirPath = '/';
|
||
} else {
|
||
dirPath = plugins.smartpath.transform.toAbsolute(dirPathArg) as string;
|
||
}
|
||
|
||
const fileTree = await listFileTree(dirPath, miniMatchFilter);
|
||
const smartfileArray: SmartFile[] = [];
|
||
for (const filePath of fileTree) {
|
||
const readPath = ((): string => {
|
||
if (!plugins.path.isAbsolute(filePath)) {
|
||
return plugins.path.join(dirPath, filePath);
|
||
} else {
|
||
return filePath;
|
||
}
|
||
})();
|
||
const fileBuffer = plugins.fs.readFileSync(readPath);
|
||
|
||
// push a read file as Smartfile
|
||
smartfileArray.push(
|
||
new SmartFile({
|
||
contentBuffer: fileBuffer,
|
||
base: dirPath,
|
||
path: filePath,
|
||
})
|
||
);
|
||
}
|
||
return smartfileArray;
|
||
};
|
||
|
||
/**
|
||
* lists Folders in a directory on local disk
|
||
* @returns Promise with an array that contains the folder names
|
||
*/
|
||
export const listFolders = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
|
||
return listFoldersSync(pathArg, regexFilter);
|
||
};
|
||
|
||
/**
|
||
* lists Folders SYNCHRONOUSLY in a directory on local disk
|
||
* @returns an array with the folder names as strings
|
||
*/
|
||
export const listFoldersSync = (pathArg: string, regexFilter?: RegExp): string[] => {
|
||
let folderArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
||
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isDirectory();
|
||
});
|
||
if (regexFilter) {
|
||
folderArray = folderArray.filter((fileItem) => {
|
||
return regexFilter.test(fileItem);
|
||
});
|
||
}
|
||
return folderArray;
|
||
};
|
||
|
||
/**
|
||
* lists Files in a directory on local disk
|
||
* @returns Promise
|
||
*/
|
||
export const listFiles = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
|
||
return listFilesSync(pathArg, regexFilter);
|
||
};
|
||
|
||
/**
|
||
* lists Files SYNCHRONOUSLY in a directory on local disk
|
||
* @returns an array with the folder names as strings
|
||
*/
|
||
export const listFilesSync = (pathArg: string, regexFilter?: RegExp): string[] => {
|
||
let fileArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
||
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
|
||
});
|
||
if (regexFilter) {
|
||
fileArray = fileArray.filter((fileItem) => {
|
||
return regexFilter.test(fileItem);
|
||
});
|
||
}
|
||
return fileArray;
|
||
};
|
||
|
||
/**
|
||
* lists all items (folders AND files) in a directory on local disk
|
||
* @returns Promise<string[]>
|
||
*/
|
||
export const listAllItems = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
|
||
return listAllItemsSync(pathArg, regexFilter);
|
||
};
|
||
|
||
/**
|
||
* lists all items (folders AND files) in a directory on local disk
|
||
* @returns an array with the folder names as strings
|
||
* @executes SYNC
|
||
*/
|
||
export const listAllItemsSync = (pathArg: string, regexFilter?: RegExp): string[] => {
|
||
let allItmesArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
|
||
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
|
||
});
|
||
if (regexFilter) {
|
||
allItmesArray = allItmesArray.filter((fileItem) => {
|
||
return regexFilter.test(fileItem);
|
||
});
|
||
}
|
||
return allItmesArray;
|
||
};
|
||
|
||
/**
|
||
* lists a file tree using a miniMatch filter
|
||
* note: if the miniMatch Filter is an absolute path, the cwdArg will be omitted
|
||
* @returns Promise<string[]> string array with the absolute paths of all matching files
|
||
*/
|
||
export const listFileTree = async (
|
||
dirPathArg: string,
|
||
miniMatchFilter: string,
|
||
absolutePathsBool: boolean = false
|
||
): Promise<string[]> => {
|
||
// handle absolute miniMatchFilter
|
||
let dirPath: string;
|
||
if (plugins.path.isAbsolute(miniMatchFilter)) {
|
||
dirPath = '/';
|
||
} else {
|
||
dirPath = dirPathArg;
|
||
}
|
||
|
||
const options = {
|
||
cwd: dirPath,
|
||
nodir: true,
|
||
dot: true,
|
||
};
|
||
|
||
let fileList = await plugins.glob.glob(miniMatchFilter, options);
|
||
if (absolutePathsBool) {
|
||
fileList = fileList.map((filePath) => {
|
||
return plugins.path.resolve(plugins.path.join(dirPath, filePath));
|
||
});
|
||
}
|
||
|
||
return fileList;
|
||
};
|
||
|
||
/**
|
||
* Watches for file stability before resolving the promise.
|
||
* Ensures that the directory/file exists before setting up the watcher.
|
||
*
|
||
* **New behavior**: If the given path is a directory, this function will:
|
||
* 1. Wait for that directory to exist (creating a timeout if needed).
|
||
* 2. Watch the directory until at least one file appears.
|
||
* 3. Then wait for the first file in the directory to stabilize before resolving.
|
||
*
|
||
* @param fileOrDirPathArg The path of the file or directory to monitor.
|
||
* @param timeoutMs The maximum time to wait for the file to stabilize (in milliseconds). Default is 60 seconds.
|
||
* @returns A promise that resolves when the target is stable or rejects on timeout/error.
|
||
*/
|
||
export const waitForFileToBeReady = async (
|
||
fileOrDirPathArg: string,
|
||
timeoutMs: number = 60000
|
||
): Promise<void> => {
|
||
const startTime = Date.now();
|
||
|
||
/**
|
||
* Ensure that a path (file or directory) exists. If it doesn't yet exist,
|
||
* wait until it does (or time out).
|
||
* @param pathToCheck The file or directory path to check.
|
||
*/
|
||
const ensurePathExists = async (pathToCheck: string): Promise<void> => {
|
||
while (true) {
|
||
try {
|
||
await plugins.smartpromise.fromCallback((cb) =>
|
||
plugins.fs.access(pathToCheck, plugins.fs.constants.F_OK, cb)
|
||
);
|
||
return;
|
||
} catch (err: any) {
|
||
if (err.code !== 'ENOENT') {
|
||
throw err; // Propagate unexpected errors
|
||
}
|
||
if (Date.now() - startTime > timeoutMs) {
|
||
throw new Error(`Timeout waiting for path to exist: ${pathToCheck}`);
|
||
}
|
||
await plugins.smartdelay.delayFor(500);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Checks if a file (not directory) is stable by comparing sizes
|
||
* across successive checks.
|
||
* @param filePathArg The path of the file to check.
|
||
* @returns A promise that resolves once the file stops changing.
|
||
*/
|
||
const waitForSingleFileToBeStable = async (filePathArg: string): Promise<void> => {
|
||
let lastFileSize = -1;
|
||
let fileIsStable = false;
|
||
|
||
// We'll create a helper for repeated stats-checking logic
|
||
const checkFileStability = async () => {
|
||
try {
|
||
const stats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
|
||
plugins.fs.stat(filePathArg, cb)
|
||
);
|
||
if (stats.isDirectory()) {
|
||
// If it unexpectedly turns out to be a directory here, throw
|
||
throw new Error(`Expected a file but found a directory: ${filePathArg}`);
|
||
}
|
||
if (stats.size === lastFileSize) {
|
||
fileIsStable = true;
|
||
} else {
|
||
lastFileSize = stats.size;
|
||
fileIsStable = false;
|
||
}
|
||
} catch (err: any) {
|
||
// Ignore only if file not found
|
||
if (err.code !== 'ENOENT') {
|
||
throw err;
|
||
}
|
||
}
|
||
};
|
||
|
||
// Ensure file exists first
|
||
await ensurePathExists(filePathArg);
|
||
|
||
// Set up a watcher on the file itself
|
||
const fileWatcher = plugins.fs.watch(filePathArg, { persistent: true }, async () => {
|
||
if (!fileIsStable) {
|
||
await checkFileStability();
|
||
}
|
||
});
|
||
|
||
try {
|
||
// Poll until stable or timeout
|
||
while (!fileIsStable) {
|
||
if (Date.now() - startTime > timeoutMs) {
|
||
throw new Error(`Timeout waiting for file to stabilize: ${filePathArg}`);
|
||
}
|
||
await checkFileStability();
|
||
if (!fileIsStable) {
|
||
await plugins.smartdelay.delayFor(1000);
|
||
}
|
||
}
|
||
} finally {
|
||
fileWatcher.close();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Main logic: check if we have a directory or file at fileOrDirPathArg.
|
||
* If directory, wait for first file in the directory to appear and stabilize.
|
||
* If file, do the old single-file wait logic.
|
||
*/
|
||
const statsForGivenPath = await (async () => {
|
||
try {
|
||
await ensurePathExists(fileOrDirPathArg);
|
||
return await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
|
||
plugins.fs.stat(fileOrDirPathArg, cb)
|
||
);
|
||
} catch (err) {
|
||
// If there's an error (including timeout), just rethrow
|
||
throw err;
|
||
}
|
||
})();
|
||
|
||
if (!statsForGivenPath.isDirectory()) {
|
||
// It's a file – just do the single-file stability wait
|
||
await waitForSingleFileToBeStable(fileOrDirPathArg);
|
||
return;
|
||
}
|
||
|
||
// Otherwise, it's a directory. Wait for the first file inside to appear and be stable
|
||
const dirPath = fileOrDirPathArg;
|
||
|
||
// Helper to find the first file in the directory if it exists
|
||
const getFirstFileInDirectory = async (): Promise<string | null> => {
|
||
const entries = await plugins.smartpromise.fromCallback<string[]>((cb) =>
|
||
plugins.fs.readdir(dirPath, cb)
|
||
);
|
||
// We only want actual files, not subdirectories
|
||
for (const entry of entries) {
|
||
const entryPath = plugins.path.join(dirPath, entry);
|
||
const entryStats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
|
||
plugins.fs.stat(entryPath, cb)
|
||
);
|
||
if (entryStats.isFile()) {
|
||
return entryPath;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// Wait for a file to appear in this directory
|
||
let firstFilePath = await getFirstFileInDirectory();
|
||
if (!firstFilePath) {
|
||
// Set up a watcher on the directory to see if a file appears
|
||
const directoryWatcher = plugins.fs.watch(dirPath, { persistent: true });
|
||
try {
|
||
// We'll poll for the existence of a file in that directory
|
||
while (!firstFilePath) {
|
||
if (Date.now() - startTime > timeoutMs) {
|
||
throw new Error(`Timeout waiting for a file to appear in directory: ${dirPath}`);
|
||
}
|
||
firstFilePath = await getFirstFileInDirectory();
|
||
if (!firstFilePath) {
|
||
await plugins.smartdelay.delayFor(1000);
|
||
}
|
||
}
|
||
} finally {
|
||
directoryWatcher.close();
|
||
}
|
||
}
|
||
|
||
// Now that we have a file path, wait for that file to stabilize
|
||
await waitForSingleFileToBeStable(firstFilePath);
|
||
};
|
||
|
||
/**
|
||
* writes string or Smartfile to disk.
|
||
* @param fileArg
|
||
* @param fileNameArg
|
||
* @param fileBaseArg
|
||
*/
|
||
export let toFs = async (
|
||
fileContentArg: string | Buffer | SmartFile | StreamFile,
|
||
filePathArg: string,
|
||
optionsArg: {
|
||
respectRelative?: boolean;
|
||
} = {}
|
||
) => {
|
||
const done = plugins.smartpromise.defer();
|
||
|
||
// check args
|
||
if (!fileContentArg || !filePathArg) {
|
||
throw new Error('expected valid arguments');
|
||
}
|
||
|
||
// prepare actual write action
|
||
let fileContent: string | Buffer;
|
||
let fileEncoding: 'utf8' | 'binary' = 'utf8';
|
||
let filePath: string = filePathArg;
|
||
|
||
// handle Smartfile
|
||
if (fileContentArg instanceof SmartFile) {
|
||
fileContent = fileContentArg.contentBuffer;
|
||
// handle options
|
||
if (optionsArg.respectRelative) {
|
||
filePath = plugins.path.join(filePath, fileContentArg.path);
|
||
}
|
||
} else if (Buffer.isBuffer(fileContentArg)) {
|
||
fileContent = fileContentArg;
|
||
fileEncoding = 'binary';
|
||
} else if (typeof fileContentArg === 'string') {
|
||
fileContent = fileContentArg;
|
||
} else {
|
||
throw new Error('fileContent is neither string nor Smartfile');
|
||
}
|
||
await ensureDir(plugins.path.parse(filePath).dir);
|
||
plugins.fsExtra.writeFile(filePath, fileContent, { encoding: fileEncoding }, done.resolve);
|
||
return await done.promise;
|
||
};
|
||
|
||
export const stat = async (filePathArg: string) => {
|
||
return plugins.fsPromises.stat(filePathArg);
|
||
};
|