/** * Local filesystem storage provider. * Stores backup archives in a local directory (or NAS-mounted path). */ import { copyFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync, } from 'node:fs'; import { dirname, join, relative } from 'node:path'; import type { StorageProvider, StorageVerifyResult } from './types'; /** Subdirectory of the configured path that actually holds archives. */ export const BACKUP_PREFIX = 'celilo-backups'; export interface LocalStorageConfig { path: string; } export function createLocalStorageProvider(config: LocalStorageConfig): StorageProvider { const basePath = join(config.path, BACKUP_PREFIX); return { async upload(localPath: string, remotePath: string): Promise { const destPath = join(basePath, remotePath); const destDir = dirname(destPath); mkdirSync(destDir, { recursive: true }); copyFileSync(localPath, destPath); }, async download(remotePath: string, localPath: string): Promise { const srcPath = join(basePath, remotePath); if (!existsSync(srcPath)) { throw new Error(`Backup file not found: ${remotePath}`); } const destDir = dirname(localPath); mkdirSync(destDir, { recursive: true }); copyFileSync(srcPath, localPath); }, async delete(remotePath: string): Promise { const fullPath = join(basePath, remotePath); if (existsSync(fullPath)) { unlinkSync(fullPath); } }, async list(prefix: string): Promise { const searchDir = join(basePath, prefix); if (!existsSync(searchDir)) { return []; } return collectFiles(searchDir).map((f) => relative(basePath, f)); }, async verify(): Promise { try { // Check base directory is writable mkdirSync(basePath, { recursive: true }); // Write test file const testFile = join(basePath, '.celilo-verify-test'); writeFileSync(testFile, 'verify'); // Read it back const content = Bun.file(testFile); const text = await content.text(); if (text !== 'verify') { return { success: false, message: 'Read-back verification failed' }; } // Delete test file unlinkSync(testFile); return { success: true, message: `Write test passed at ${basePath}` }; } catch (error) { return { success: false, message: `Storage verification failed: ${error instanceof Error ? error.message : String(error)}`, }; } }, async initialize(): Promise { mkdirSync(basePath, { recursive: true }); }, }; } function collectFiles(dir: string): string[] { const results: string[] = []; const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { results.push(...collectFiles(fullPath)); } else { results.push(fullPath); } } return results; }