diff --git a/changelog.md b/changelog.md index 996f65a..a0b9a95 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # 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) Fix file existence check in waitForFileToBeReady method. diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index af860ce..254653f 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { 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.' } diff --git a/ts/fs.ts b/ts/fs.ts index a1e18b7..e86405a 100644 --- a/ts/fs.ts +++ b/ts/fs.ts @@ -397,49 +397,67 @@ export const listFileTree = async ( /** * Watches for file stability before resolving the promise. - * Ensures that the directory and file exist before setting up the watcher. - * @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 => { - 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 => { + 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 => { let lastFileSize = -1; let fileIsStable = false; - const startTime = Date.now(); - const fileDir = plugins.path.dirname(filePathArg); - - const ensureFileExists = async () => { - while (true) { - try { - // Check if the file exists - await plugins.smartpromise.fromCallback((cb) => - plugins.fs.access(filePathArg, plugins.fs.constants.F_OK, cb) - ); - break; // Exit the loop if the file exists - } catch (err: any) { - if (err.code !== 'ENOENT') { - throw err; // Propagate unexpected errors - } - if (Date.now() - startTime > timeoutMs) { - reject(new Error(`Timeout waiting for file to exist: ${filePathArg}`)); - return; - } - // Wait and retry - await plugins.smartdelay.delayFor(500); - } - } - }; - + // We'll create a helper for repeated stats-checking logic const checkFileStability = async () => { try { const stats = await plugins.smartpromise.fromCallback((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 { @@ -447,49 +465,106 @@ export const waitForFileToBeReady = ( fileIsStable = false; } } catch (err: any) { + // Ignore only if file not found if (err.code !== 'ENOENT') { - throw err; // Only ignore ENOENT (file not found) errors + throw err; } } }; - // Ensure the file exists before setting up the watcher - await ensureFileExists(); + // Ensure file exists first + 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) { 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 stabilize: ${filePathArg}`)); - return; + throw new Error(`Timeout waiting for file to stabilize: ${filePathArg}`); } - - // 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((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 => { + const entries = await plugins.smartpromise.fromCallback((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((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); }; /**