UNPKG

file-age

Version:

Get file age/timestamp information with human-readable formatting

338 lines (294 loc) 9.55 kB
const fs = require('fs').promises; const path = require('path'); /** * Get file age/timestamp information * @param {string} filePath - Path to the file * @param {Object} options - Configuration options * @param {boolean} options.human - Return human-readable format (default: false) * @param {string} options.type - 'modified' (default), 'created', 'accessed' * @param {string} options.format - For human format: 'relative' (default), 'absolute' * @param {boolean} options.precise - Include milliseconds (default: false) * @returns {Promise<number|string>} Timestamp or human-readable string */ async function fileAge(filePath, options = {}) { const { human = false, type = 'modified', format = 'relative', precise = false } = options || {}; try { const stats = await fs.stat(filePath); let timestamp; switch (type) { case 'created': case 'birth': timestamp = stats.birthtime.getTime(); break; case 'accessed': case 'access': timestamp = stats.atime.getTime(); break; case 'modified': case 'mod': default: timestamp = stats.mtime.getTime(); break; } if (!precise) { timestamp = Math.floor(timestamp / 1000) * 1000; // Remove milliseconds } if (human) { return formatHuman(timestamp, format); } return timestamp; } catch (error) { if (error.code === 'ENOENT') { throw new Error(`File not found: ${filePath}`); } if (error.code === 'EACCES') { throw new Error(`Permission denied: ${filePath}`); } throw new Error(`Unable to read file stats: ${error.message}`); } } /** * Synchronous version of fileAge * @param {string} filePath - Path to the file * @param {Object} options - Configuration options * @returns {number|string} Timestamp or human-readable string */ function fileAgeSync(filePath, options = {}) { const { human = false, type = 'modified', format = 'relative', precise = false } = options || {}; try { const stats = require('fs').statSync(filePath); let timestamp; switch (type) { case 'created': case 'birth': timestamp = stats.birthtime.getTime(); break; case 'accessed': case 'access': timestamp = stats.atime.getTime(); break; case 'modified': case 'mod': default: timestamp = stats.mtime.getTime(); break; } if (!precise) { timestamp = Math.floor(timestamp / 1000) * 1000; } if (human) { return formatHuman(timestamp, format); } return timestamp; } catch (error) { if (error.code === 'ENOENT') { throw new Error(`File not found: ${filePath}`); } if (error.code === 'EACCES') { throw new Error(`Permission denied: ${filePath}`); } throw new Error(`Unable to read file stats: ${error.message}`); } } /** * Get age information for multiple files * @param {string[]} filePaths - Array of file paths * @param {Object} options - Configuration options * @returns {Promise<Object>} Object with file paths as keys and ages as values */ async function fileAgeMultiple(filePaths, options = {}) { options = options || {}; const results = {}; await Promise.all( filePaths.map(async (filePath) => { try { results[filePath] = await fileAge(filePath, options); } catch (error) { results[filePath] = { error: error.message }; } }) ); return results; } /** * Compare ages of two files * @param {string} file1 - First file path * @param {string} file2 - Second file path * @param {Object} options - Configuration options * @returns {Promise<Object>} Comparison result */ async function compareFileAge(file1, file2, options = {}) { options = options || {}; const age1 = await fileAge(file1, { ...options, human: false }); const age2 = await fileAge(file2, { ...options, human: false }); const difference = Math.abs(age1 - age2); return { file1: { path: file1, timestamp: age1 }, file2: { path: file2, timestamp: age2 }, newer: age1 > age2 ? file1 : file2, older: age1 < age2 ? file1 : file2, difference: difference, differenceHuman: formatDuration(difference) }; } /** * Find files in directory older/newer than specified time * @param {string} dirPath - Directory path * @param {Object} options - Configuration options * @param {number|string} options.olderThan - Timestamp or duration string * @param {number|string} options.newerThan - Timestamp or duration string * @param {boolean} options.recursive - Search recursively (default: false) * @returns {Promise<string[]>} Array of matching file paths */ async function findFilesByAge(dirPath, options = {}) { options = options || {}; const { olderThan, newerThan, recursive = false } = options; const olderTimestamp = parseTimeInput(olderThan); const newerTimestamp = parseTimeInput(newerThan); const files = []; async function scanDirectory(currentPath) { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); if (entry.isDirectory() && recursive) { await scanDirectory(fullPath); } else if (entry.isFile()) { try { const age = await fileAge(fullPath); let matches = true; if (olderTimestamp !== null && age >= olderTimestamp) { matches = false; } if (newerTimestamp !== null && age <= newerTimestamp) { matches = false; } if (matches) { files.push(fullPath); } } catch (error) { // Skip files we can't read } } } } await scanDirectory(dirPath); return files; } /** * Format timestamp as human-readable string * @param {number} timestamp - Timestamp in milliseconds * @param {string} format - 'relative' or 'absolute' * @returns {string} Human-readable string */ function formatHuman(timestamp, format = 'relative') { const date = new Date(timestamp); if (format === 'absolute') { return date.toLocaleString(); } // Relative format const now = Date.now(); const difference = now - timestamp; return formatDuration(Math.abs(difference), difference < 0); } /** * Format duration in milliseconds as human-readable string * @param {number} ms - Duration in milliseconds * @param {boolean} future - Whether the time is in the future * @returns {string} Human-readable duration */ function formatDuration(ms, future = false) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const weeks = Math.floor(days / 7); const months = Math.floor(days / 30); const years = Math.floor(days / 365); let result; if (years > 0) { result = `${years} year${years !== 1 ? 's' : ''}`; } else if (months > 0) { result = `${months} month${months !== 1 ? 's' : ''}`; } else if (weeks > 0) { result = `${weeks} week${weeks !== 1 ? 's' : ''}`; } else if (days > 0) { result = `${days} day${days !== 1 ? 's' : ''}`; } else if (hours > 0) { result = `${hours} hour${hours !== 1 ? 's' : ''}`; } else if (minutes > 0) { result = `${minutes} minute${minutes !== 1 ? 's' : ''}`; } else { result = `${seconds} second${seconds !== 1 ? 's' : ''}`; } if (future) { return `in ${result}`; } else { return `${result} ago`; } } /** * Parse time input (timestamp or duration string) * @param {number|string} input - Time input * @returns {number|null} Timestamp in milliseconds */ function parseTimeInput(input) { if (input === undefined || input === null) { return null; } if (typeof input === 'number') { return input; } if (typeof input === 'string') { // Parse duration strings like "2 hours", "30 minutes", "1 day" const durationRegex = /^(\d+)\s*(second|seconds|minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years)$/i; const match = input.match(durationRegex); if (match) { const amount = parseInt(match[1]); const unit = match[2].toLowerCase(); const multipliers = { second: 1000, seconds: 1000, minute: 60 * 1000, minutes: 60 * 1000, hour: 60 * 60 * 1000, hours: 60 * 60 * 1000, day: 24 * 60 * 60 * 1000, days: 24 * 60 * 60 * 1000, week: 7 * 24 * 60 * 60 * 1000, weeks: 7 * 24 * 60 * 60 * 1000, month: 30 * 24 * 60 * 60 * 1000, months: 30 * 24 * 60 * 60 * 1000, year: 365 * 24 * 60 * 60 * 1000, years: 365 * 24 * 60 * 60 * 1000 }; const multiplier = multipliers[unit]; if (multiplier) { return Date.now() - (amount * multiplier); } } // Try parsing as date string const parsed = Date.parse(input); if (!isNaN(parsed)) { return parsed; } } throw new Error(`Invalid time input: ${input}`); } // Export main function and utilities module.exports = fileAge; module.exports.fileAge = fileAge; module.exports.fileAgeSync = fileAgeSync; module.exports.fileAgeMultiple = fileAgeMultiple; module.exports.compareFileAge = compareFileAge; module.exports.findFilesByAge = findFilesByAge; module.exports.formatDuration = formatDuration;