file-age
Version:
Get file age/timestamp information with human-readable formatting
338 lines (294 loc) • 9.55 kB
JavaScript
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;