import { mkdir, readdir, readFile, stat, unlink } from 'fs/promises' import { join } from 'path' import { FileEntry } from '../../types/files.js' import { createReadStream } from 'fs' import { LSBlockDevice, NomadDiskInfoRaw } from '../../types/system.js' export async function listDirectoryContents(path: string): Promise { const entries = await readdir(path, { withFileTypes: true }) const results: FileEntry[] = [] for (const entry of entries) { if (entry.isFile()) { results.push({ type: 'file', key: join(path, entry.name), name: entry.name, }) } else if (entry.isDirectory()) { results.push({ type: 'directory', prefix: join(path, entry.name), name: entry.name, }) } } return results } export async function listDirectoryContentsRecursive(path: string): Promise { let results: FileEntry[] = [] const entries = await readdir(path, { withFileTypes: true }) for (const entry of entries) { const fullPath = join(path, entry.name) if (entry.isDirectory()) { const subdirectoryContents = await listDirectoryContentsRecursive(fullPath) results = results.concat(subdirectoryContents) } else { results.push({ type: 'file', key: fullPath, name: entry.name, }) } } return results } export async function ensureDirectoryExists(path: string): Promise { try { await stat(path) } catch (error) { if (error.code === 'ENOENT') { await mkdir(path, { recursive: true }) } } } export async function getFile(path: string, returnType: 'buffer'): Promise export async function getFile( path: string, returnType: 'stream' ): Promise export async function getFile(path: string, returnType: 'string'): Promise export async function getFile( path: string, returnType: 'buffer' | 'string' | 'stream' = 'buffer' ): Promise { try { if (returnType === 'string') { return await readFile(path, 'utf-8') } else if (returnType === 'stream') { return createReadStream(path) } return await readFile(path) } catch (error) { if (error.code === 'ENOENT') { return null } throw error } } export async function getFileStatsIfExists( path: string ): Promise<{ size: number; modifiedTime: Date } | null> { try { const stats = await stat(path) return { size: stats.size, modifiedTime: stats.mtime, } } catch (error) { if (error.code === 'ENOENT') { return null } throw error } } export async function deleteFileIfExists(path: string): Promise { try { await unlink(path) } catch (error) { if (error.code !== 'ENOENT') { throw error } } } export function getAllFilesystems( device: LSBlockDevice, fsSize: NomadDiskInfoRaw['fsSize'] ): NomadDiskInfoRaw['fsSize'] { const filesystems: NomadDiskInfoRaw['fsSize'] = [] const seen = new Set() function traverse(dev: LSBlockDevice) { // Try to find matching filesystem const fs = fsSize.find((f) => matchesDevice(f.fs, dev.name)) if (fs && !seen.has(fs.fs)) { filesystems.push(fs) seen.add(fs.fs) } // Traverse children recursively if (dev.children) { dev.children.forEach((child) => traverse(child)) } } traverse(device) return filesystems } export function matchesDevice(fsPath: string, deviceName: string): boolean { // Remove /dev/ and /dev/mapper/ prefixes const normalized = fsPath.replace('/dev/mapper/', '').replace('/dev/', '') // Direct match if (normalized === deviceName) { return true } // LVM volumes use dashes instead of slashes // e.g., ubuntu--vg-ubuntu--lv matches the device name if (fsPath.includes(deviceName)) { return true } return false }