Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
7a32835a74 | |||
e78682d9b4 | |||
8dceea67be | |||
40018532a7 | |||
f6fb28d32f | |||
2d1ac0bd50 | |||
04a25221a5 | |||
13081b7344 | |||
0abbe8bbd7 | |||
de2a250a45 | |||
1657d0e1c6 | |||
e6b8240031 | |||
be011a4637 | |||
dbddf2a8ba | |||
207320ff26 | |||
be99bdae66 | |||
768d970918 | |||
a9799e05ee |
57
changelog.md
57
changelog.md
@ -1,5 +1,62 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-01-29 - 11.2.0 - feat(fs)
|
||||
Enhanced copy method with optional replaceTargetDir option for directory replacement
|
||||
|
||||
- Added optional 'replaceTargetDir' option to 'copy' and 'copySync' methods in 'fs.ts'.
|
||||
- The 'replaceTargetDir' option allows replacing the target directory if both source and target are directories.
|
||||
|
||||
## 2025-01-29 - 11.1.9 - fix(fs)
|
||||
Fix directory handling in copy and copySync functions
|
||||
|
||||
- Ensured existing directories at destination are removed before copying over them in async copy.
|
||||
- Added a similar check and handling for synchronous copySync when destination is a directory.
|
||||
|
||||
## 2025-01-29 - 11.1.8 - fix(fs)
|
||||
Fixed copy and copySync functions to ensure they always overwrite files.
|
||||
|
||||
- Fixed bug in copy function where files were not being overwritten when they already existed at the destination.
|
||||
- Fixed bug in copySync function to ensure files are overwritten to match the async function's behavior.
|
||||
|
||||
## 2025-01-29 - 11.1.7 - fix(fs)
|
||||
Refactor copy and copySync functions to simplify return type
|
||||
|
||||
- Changed the return type of fs.copy and fs.copySync from boolean to void.
|
||||
- Removed unnecessary promise handling in fs.copy.
|
||||
|
||||
## 2025-01-29 - 11.1.6 - fix(fs)
|
||||
Fix issues with fs file copy functions.
|
||||
|
||||
- Updated dependencies in package.json.
|
||||
- Corrected comments for asynchronous and synchronous file copy functions in fs.ts.
|
||||
|
||||
## 2025-01-07 - 11.1.5 - fix(fs)
|
||||
Improve waitForFileToBeReady function to handle directories and file stabilization
|
||||
|
||||
- Enhanced the waitForFileToBeReady to handle directory paths by checking for file existence within directories and waiting for stabilization.
|
||||
- Modified the watcher logic to cater to changes when monitoring directories for file appearance.
|
||||
- Introduced a helper function to ensure paths exist and another to resolve the first file in directories.
|
||||
- Corrected logic for polling and stabilizing files within directories.
|
||||
|
||||
## 2025-01-07 - 11.1.4 - fix(fs)
|
||||
Fix file existence check in waitForFileToBeReady method.
|
||||
|
||||
- Ensured that the directory and file exist before setting up the watcher in waitForFileToBeReady.
|
||||
- Changed ensureDirectoryExists to ensureFileExists for correct file path verification.
|
||||
- Handled ENOENT errors correctly to retry file existence checks until timeout is reached.
|
||||
|
||||
## 2025-01-07 - 11.1.3 - fix(fs)
|
||||
Fix TypeScript type issue in fs module
|
||||
|
||||
- Corrected a TypeScript type in the fs module's checkFileStability function.
|
||||
|
||||
## 2025-01-07 - 11.1.2 - fix(fs)
|
||||
Fix issues in file stability check and directory existence verification in fs module
|
||||
|
||||
- Removed unused variable 'isFileAvailable' in 'waitForFileToBeReady'.
|
||||
- Fixed logic for ensuring the target directory exists before setting up file stability watcher.
|
||||
- Refactored directory existence logic into 'ensureDirectoryExists' function.
|
||||
|
||||
## 2025-01-07 - 11.1.1 - fix(fs)
|
||||
Improve waitForFileToBeReady function for file stability detection
|
||||
|
||||
|
16
package.json
16
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@push.rocks/smartfile",
|
||||
"private": false,
|
||||
"version": "11.1.1",
|
||||
"version": "11.2.0",
|
||||
"description": "Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
@ -49,22 +49,22 @@
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartmime": "^2.0.4",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartpromise": "^4.1.0",
|
||||
"@push.rocks/smartpromise": "^4.2.2",
|
||||
"@push.rocks/smartrequest": "^2.0.23",
|
||||
"@push.rocks/smartstream": "^3.2.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^11.0.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"glob": "^11.0.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.2.0",
|
||||
"@git.zone/tsbuild": "^2.2.1",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@push.rocks/tapbundle": "^5.5.4",
|
||||
"@types/node": "^22.10.5"
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/tapbundle": "^5.5.6",
|
||||
"@types/node": "^22.12.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
4748
pnpm-lock.yaml
generated
4748
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartfile',
|
||||
version: '11.1.1',
|
||||
version: '11.2.0',
|
||||
description: 'Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.'
|
||||
}
|
||||
|
216
ts/fs.ts
216
ts/fs.ts
@ -72,25 +72,23 @@ export const isFile = (pathArg): boolean => {
|
||||
===============================================================*/
|
||||
|
||||
/**
|
||||
* copies a file from A to B on the local disk
|
||||
* copies a file or directory 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;
|
||||
export const copy = async (fromArg: string, toArg: string, optionsArg?: plugins.fsExtra.CopyOptions & { replaceTargetDir?: boolean }): Promise<void> => {
|
||||
if (optionsArg?.replaceTargetDir && isDirectory(fromArg) && isDirectory(toArg)) {
|
||||
await remove(toArg);
|
||||
}
|
||||
return await plugins.fsExtra.copy(fromArg, toArg, optionsArg as plugins.fsExtra.CopyOptions);
|
||||
};
|
||||
|
||||
/**
|
||||
* copies a file SYNCHRONOUSLY from A to B on the local disk
|
||||
* copies a file or directory SYNCHRONOUSLY from A to B on the local disk
|
||||
*/
|
||||
export const copySync = (fromArg: string, toArg: string): boolean => {
|
||||
plugins.fsExtra.copySync(fromArg, toArg);
|
||||
return true;
|
||||
export const copySync = (fromArg: string, toArg: string, optionsArg?: plugins.fsExtra.CopyOptionsSync & { replaceTargetDir?: boolean }): void => {
|
||||
if (optionsArg?.replaceTargetDir && isDirectory(fromArg) && isDirectory(toArg)) {
|
||||
removeSync(toArg);
|
||||
}
|
||||
return plugins.fsExtra.copySync(fromArg, toArg, optionsArg as plugins.fsExtra.CopyOptionsSync);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -205,7 +203,7 @@ export const toObjectSync = (filePathArg, fileTypeArg?) => {
|
||||
} catch (err) {
|
||||
err.message = `Failed to read file at ${filePathArg}` + err.message;
|
||||
throw err;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -397,94 +395,174 @@ export const listFileTree = async (
|
||||
|
||||
/**
|
||||
* Watches for file stability before resolving the promise.
|
||||
* @param filePathArg The path of the file to monitor.
|
||||
* 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 file is stable or rejects on timeout or error.
|
||||
* @returns A promise that resolves when the target is stable or rejects on timeout/error.
|
||||
*/
|
||||
export const waitForFileToBeReady = (
|
||||
filePathArg: string,
|
||||
export const waitForFileToBeReady = async (
|
||||
fileOrDirPathArg: string,
|
||||
timeoutMs: number = 60000
|
||||
): Promise<void> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
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;
|
||||
let isFileAvailable = false;
|
||||
|
||||
const startTime = Date.now();
|
||||
const fileDir = plugins.path.dirname(filePathArg);
|
||||
|
||||
// We'll create a helper for repeated stats-checking logic
|
||||
const checkFileStability = async () => {
|
||||
try {
|
||||
const stats: any = await plugins.smartpromise.fromCallback((cb) =>
|
||||
const stats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
|
||||
plugins.fs.stat(filePathArg, cb)
|
||||
);
|
||||
isFileAvailable = true;
|
||||
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) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isFileAvailable = false; // File not available yet
|
||||
} else {
|
||||
throw err; // Propagate other errors
|
||||
} catch (err: any) {
|
||||
// Ignore only if file not found
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkDirectory = async () => {
|
||||
try {
|
||||
await plugins.smartpromise.fromCallback((cb) =>
|
||||
plugins.fs.access(fileDir, plugins.fs.constants.R_OK, cb)
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// Ensure file exists first
|
||||
await ensurePathExists(filePathArg);
|
||||
|
||||
const watcher = plugins.fs.watch(filePathArg, { persistent: true }, async () => {
|
||||
if (isFileAvailable && !fileIsStable) {
|
||||
// Set up a watcher on the file itself
|
||||
const fileWatcher = plugins.fs.watch(filePathArg, { persistent: true }, async () => {
|
||||
if (!fileIsStable) {
|
||||
await checkFileStability();
|
||||
}
|
||||
});
|
||||
|
||||
watcher.on('error', (error) => {
|
||||
watcher.close();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
try {
|
||||
// Poll until stable or timeout
|
||||
while (!fileIsStable) {
|
||||
// Check for timeout
|
||||
if (Date.now() - startTime > timeoutMs) {
|
||||
watcher.close();
|
||||
reject(new Error(`Timeout waiting for file to be ready: ${filePathArg}`));
|
||||
return;
|
||||
throw new Error(`Timeout waiting for file to stabilize: ${filePathArg}`);
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if (!await checkDirectory()) {
|
||||
await plugins.smartdelay.delayFor(500); // Wait and retry
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check file stability
|
||||
await checkFileStability();
|
||||
if (!fileIsStable) {
|
||||
await plugins.smartdelay.delayFor(1000); // Polling interval
|
||||
await plugins.smartdelay.delayFor(1000);
|
||||
}
|
||||
}
|
||||
|
||||
watcher.close();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
watcher.close();
|
||||
reject(err);
|
||||
} 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);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -534,4 +612,4 @@ export let toFs = async (
|
||||
|
||||
export const stat = async (filePathArg: string) => {
|
||||
return plugins.fsPromises.stat(filePathArg);
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user