fix(fs): Improve waitForFileToBeReady function to handle directories and file stabilization

This commit is contained in:
Philipp Kunz 2025-01-07 04:58:31 +01:00
parent be011a4637
commit e6b8240031
3 changed files with 138 additions and 55 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 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) ## 2025-01-07 - 11.1.4 - fix(fs)
Fix file existence check in waitForFileToBeReady method. Fix file existence check in waitForFileToBeReady method.

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartfile', name: '@push.rocks/smartfile',
version: '11.1.4', version: '11.1.5',
description: 'Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.' description: 'Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.'
} }

155
ts/fs.ts
View File

@ -397,49 +397,67 @@ export const listFileTree = async (
/** /**
* Watches for file stability before resolving the promise. * Watches for file stability before resolving the promise.
* Ensures that the directory and file exist before setting up the watcher. * Ensures that the directory/file exists before setting up the watcher.
* @param filePathArg The path of the file to monitor. *
* **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. * @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 = ( export const waitForFileToBeReady = async (
filePathArg: string, fileOrDirPathArg: string,
timeoutMs: number = 60000 timeoutMs: number = 60000
): Promise<void> => { ): Promise<void> => {
return new Promise(async (resolve, reject) => {
let lastFileSize = -1;
let fileIsStable = false;
const startTime = Date.now(); const startTime = Date.now();
const fileDir = plugins.path.dirname(filePathArg);
const ensureFileExists = async () => { /**
* 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) { while (true) {
try { try {
// Check if the file exists
await plugins.smartpromise.fromCallback((cb) => await plugins.smartpromise.fromCallback((cb) =>
plugins.fs.access(filePathArg, plugins.fs.constants.F_OK, cb) plugins.fs.access(pathToCheck, plugins.fs.constants.F_OK, cb)
); );
break; // Exit the loop if the file exists return;
} catch (err: any) { } catch (err: any) {
if (err.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
throw err; // Propagate unexpected errors throw err; // Propagate unexpected errors
} }
if (Date.now() - startTime > timeoutMs) { if (Date.now() - startTime > timeoutMs) {
reject(new Error(`Timeout waiting for file to exist: ${filePathArg}`)); throw new Error(`Timeout waiting for path to exist: ${pathToCheck}`);
return;
} }
// Wait and retry
await plugins.smartdelay.delayFor(500); 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 () => { const checkFileStability = async () => {
try { try {
const stats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) => const stats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
plugins.fs.stat(filePathArg, 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) { if (stats.size === lastFileSize) {
fileIsStable = true; fileIsStable = true;
} else { } else {
@ -447,49 +465,106 @@ export const waitForFileToBeReady = (
fileIsStable = false; fileIsStable = false;
} }
} catch (err: any) { } catch (err: any) {
// Ignore only if file not found
if (err.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
throw err; // Only ignore ENOENT (file not found) errors throw err;
} }
} }
}; };
// Ensure the file exists before setting up the watcher // Ensure file exists first
await ensureFileExists(); await ensurePathExists(filePathArg);
const watcher = plugins.fs.watch(filePathArg, { persistent: true }, async () => { // Set up a watcher on the file itself
const fileWatcher = plugins.fs.watch(filePathArg, { persistent: true }, async () => {
if (!fileIsStable) { if (!fileIsStable) {
await checkFileStability(); await checkFileStability();
} }
}); });
watcher.on('error', (error) => {
watcher.close();
reject(error);
});
try { try {
// Poll until stable or timeout
while (!fileIsStable) { while (!fileIsStable) {
// Check for timeout
if (Date.now() - startTime > timeoutMs) { if (Date.now() - startTime > timeoutMs) {
watcher.close(); throw new Error(`Timeout waiting for file to stabilize: ${filePathArg}`);
reject(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; return;
} }
// Check file stability // Otherwise, it's a directory. Wait for the first file inside to appear and be stable
await checkFileStability(); const dirPath = fileOrDirPathArg;
if (!fileIsStable) {
await plugins.smartdelay.delayFor(1000); // Polling interval // 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();
} }
} }
watcher.close(); // Now that we have a file path, wait for that file to stabilize
resolve(); await waitForSingleFileToBeStable(firstFilePath);
} catch (err) {
watcher.close();
reject(err);
}
});
}; };
/** /**