import fs from "fs/promises"
import path from "path"
const HIDDEN_FILE_PREFIX = "."
// File list cache
const fileCache = new Map<string, { files: string[]; timestamp: number }>()
const CACHE_TTL = 5000 // Cache validity period: 5 seconds
// Cache cleanup timer reference
let cleanupTimer: NodeJS.Timeout | null = null
/**
* Recursively read all files in a directory (with caching)
* @param dir - Directory path to scan
* @param useCache - Whether to use cache (default: true)
* @returns Array of file paths
* @throws {Error} When directory does not exist or cannot be read
*/
export async function getFilesRecursively(
dir: string,
useCache: boolean = true
): Promise<string[]> {
const now = Date.now()
const cached = fileCache.get(dir)
// Check if cache is valid
if (useCache && cached && now - cached.timestamp < CACHE_TTL) {
return cached.files
}
// Scan file system
let results: string[] = []
const list = await fs.readdir(dir)
for (const file of list) {
if (file.startsWith(HIDDEN_FILE_PREFIX)) continue // Ignore .git and hidden files
const filePath = path.resolve(dir, file)
const stat = await fs.stat(filePath)
if (stat && stat.isDirectory()) {
results = results.concat(
await getFilesRecursively(filePath, useCache)
)
} else {
results.push(filePath)
}
}
// Update cache
if (useCache) {
fileCache.set(dir, { files: results, timestamp: now })
}
return results
}
/**
* Clear cache for specified directory
* @param dir - Directory path (optional, if not provided clears all cache)
*/
export function clearFileCache(dir?: string): void {
if (dir) {
fileCache.delete(dir)
} else {
fileCache.clear()
}
}
/**
* Clean up expired cache entries
* Removes all cache entries that have exceeded their TTL
* @returns Number of cache entries cleaned up
*/
export function cleanupExpiredCache(): number {
const now = Date.now()
let cleaned = 0
const expiredDirs: string[] = []
// Collect expired entries
for (const [dir, cache] of fileCache.entries()) {
if (now - cache.timestamp >= CACHE_TTL) {
expiredDirs.push(dir)
}
}
// Remove expired entries
for (const dir of expiredDirs) {
fileCache.delete(dir)
cleaned++
}
return cleaned
}
/**
* Start periodic cache cleanup
* Automatically removes expired cache entries at specified intervals
* @param interval - Cleanup interval in milliseconds (default: CACHE_TTL * 2)
* @param onCleanup - Optional callback function called after each cleanup with number of cleaned entries
*/
export function startCacheCleanup(
interval: number = CACHE_TTL * 2,
onCleanup?: (cleaned: number) => void
): void {
// Stop existing cleanup if running
stopCacheCleanup()
// Start periodic cleanup
cleanupTimer = setInterval(() => {
const cleaned = cleanupExpiredCache()
if (onCleanup) {
onCleanup(cleaned)
}
}, interval)
}
/**
* Stop periodic cache cleanup
* Clears the cleanup timer if it's running
*/
export function stopCacheCleanup(): void {
if (cleanupTimer !== null) {
clearInterval(cleanupTimer)
cleanupTimer = null
}
}
/**
* Get cache statistics
* @returns Object containing cache size and other statistics
*/
export function getCacheStats(): {
size: number
entries: Array<{ dir: string; age: number; expired: boolean }>
} {
const now = Date.now()
const entries: Array<{ dir: string; age: number; expired: boolean }> = []
for (const [dir, cache] of fileCache.entries()) {
const age = now - cache.timestamp
entries.push({
dir,
age,
expired: age >= CACHE_TTL,
})
}
return {
size: fileCache.size,
entries,
}
}
/**
* Ensure directory exists and has read/write permissions
* @param dir - Directory path
* @throws {Error} When directory cannot be accessed
*/
export async function ensureDirectoryAccess(dir: string): Promise<void> {
try {
await fs.access(dir, fs.constants.R_OK | fs.constants.W_OK)
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error)
throw new Error(`No access to directory ${dir}: ${errorMessage}`)
}
}