UNPKG

@oletizi/sampler-backup

Version:

Akai sampler backup utilities for hardware samplers via PiSCSI

1,460 lines (1,403 loc) 47.1 kB
import { AudioToolsConfig } from '@oletizi/audiotools-config'; import { BackupSource as BackupSource_2 } from '@oletizi/audiotools-config'; import { DeviceDetectorInterface } from '@oletizi/lib-device-uuid'; import { DeviceInfo } from '@oletizi/lib-device-uuid'; /** * Auto-detect backup service implementation. * * Integrates device detection, smart defaults, and interactive prompts * to create a seamless auto-detect backup flow. * * @example * ```typescript * const detector = createDeviceDetector(); * const promptService = new InteractivePrompt(); * const resolver = new DeviceResolver(); * const autoDetect = new AutoDetectBackup(detector, promptService, resolver); * * // Auto-detect with full prompting * const result = await autoDetect.detectAndResolve('/Volumes/SDCARD', config); * console.log(`Action: ${result.action}, Source: ${result.source.name}`); * * // Auto-detect with overrides (skip prompts) * const result2 = await autoDetect.detectAndResolve( * '/Volumes/SDCARD', * config, * { deviceType: 'floppy', sampler: 's5000' } * ); * ``` */ export declare class AutoDetectBackup implements AutoDetectBackupInterface { private readonly deviceDetector; private readonly promptService; private readonly deviceResolver; constructor(deviceDetector: DeviceDetectorInterface, promptService: InteractivePromptInterface, deviceResolver: DeviceResolver); detectAndResolve(mountPath: string, config: AudioToolsConfig, options?: AutoDetectOptions): Promise<AutoDetectResult>; /** * Detect device info with graceful fallback. * Returns partial info if detection fails. */ private detectDeviceInfo; /** * Infer device type from filesystem type. * * Logic: * - FAT12/FAT16 → 'floppy' (typical for floppy disks) * - FAT32 → 'hard-drive' (typical for SD cards, USB drives) * - ISO9660/CDFS → 'cd-rom' * - exFAT, NTFS → 'hard-drive' * - Unknown → undefined (will prompt) */ private inferDeviceType; /** * Determine sampler from config or prompt. * * If no existing samplers, prompts for new sampler name. * If existing samplers, prompts to select or add new. */ private determineSampler; /** * Get list of unique sampler names from config. */ private getExistingSamplers; /** * Find existing source that matches the device info. * * Matches by UUID or serial number if available. */ private findExistingSource; /** * Generate source name from components. * * Format: ${sampler}-${deviceType}-${volumeLabel || 'device'} * Example: 's5000-floppy-SDCARD' * * Ensures uniqueness by appending number if needed. */ private generateSourceName; /** * Create a new BackupSource from components. */ private createBackupSource; /** * Check if device info has UUID or serial identifiers. */ private hasDeviceIdentifiers; /** * Extract volume name from mount path. * E.g., '/Volumes/SDCARD' → 'SDCARD' */ private extractVolumeNameFromPath; } /** * Interface for auto-detect backup operations. * * Defines the contract for auto-detecting and resolving backup sources * from mounted devices. Use this interface for dependency injection. */ export declare interface AutoDetectBackupInterface { /** * Detect device and resolve to complete backup source. * * Performs complete auto-detection workflow: * 1. Detects device info using lib-device-uuid * 2. Infers device type from filesystem if not provided * 3. Prompts for missing device type if needed * 4. Prompts for sampler if needed * 5. Checks if device is already registered * 6. Returns complete BackupSource ready to use * * @param mountPath - Path where device is mounted * @param config - Current audio-tools configuration * @param options - Optional overrides for device type and sampler * @returns Auto-detect result with complete source and metadata * @throws Error if device detection fails or user cancels */ detectAndResolve(mountPath: string, config: AudioToolsConfig, options?: AutoDetectOptions): Promise<AutoDetectResult>; } /** * Options for auto-detect backup operation. */ export declare interface AutoDetectOptions { /** Override device type inference (e.g., 'floppy', 'hard-drive') */ deviceType?: string; /** Override sampler selection (e.g., 's5000', 's3000xl') */ sampler?: string; } /** * Result of auto-detect backup operation. */ export declare interface AutoDetectResult { /** Complete backup source ready to use */ source: BackupSource_2; /** Action taken: registered (new), recognized (existing), created (new without UUID) */ action: 'registered' | 'recognized' | 'created'; /** Detected device information */ deviceInfo: DeviceInfo; /** Whether user was prompted for any information */ wasPrompted: boolean; } /** * Progress information during backup */ export declare interface BackupProgress { /** Current file being processed */ currentFile: string; /** Bytes processed so far */ bytesProcessed: number; /** Total bytes to process */ totalBytes: number; /** Files processed so far */ filesProcessed: number; /** Total files to process */ totalFiles: number; } /** * Progress information during backup */ declare interface BackupProgress_2 { /** Current file being processed */ currentFile: string; /** Bytes processed so far */ bytesProcessed: number; /** Total bytes to process */ totalBytes: number; /** Files processed so far */ filesProcessed: number; /** Total files to process */ totalFiles: number; } /** * Result of a backup operation */ export declare interface BackupResult { success: boolean; interval: RsnapshotInterval; configPath: string; snapshotPath?: string; errors: string[]; } /** * Unified interface for backup sources * * Implementations: * - RemoteSource: Wraps rsnapshot for SSH-based backups * - LocalSource: Uses LocalBackupAdapter for file-based backups */ export declare interface BackupSource { /** Source type identifier */ readonly type: 'remote' | 'local'; /** * Execute backup for the specified interval * * @param interval - Backup interval (daily, weekly, monthly) * @returns Promise resolving to backup result with success status and metadata */ backup(interval: RsnapshotInterval): Promise<BackupResult>; /** * Test if the source is accessible and properly configured * * @returns Promise resolving to true if source is accessible, false otherwise */ test(): Promise<boolean>; /** * Get the configuration for this source * * @returns Source configuration object */ getConfig(): BackupSourceConfig; } /** * Union type for all backup source configurations */ export declare type BackupSourceConfig = RemoteSourceConfig | LocalSourceConfig; /** * BackupSourceFactory - Factory for creating BackupSource instances * * Provides unified interface for creating backup sources from either explicit * configuration objects or path strings with automatic type detection. * * @remarks * The factory automatically detects whether a path is a remote SSH source or * local filesystem path based on the presence of a colon (`:`) in the path. * * Path detection logic: * - Contains `:` and not a Windows path → Remote SSH source (e.g., `host:/path`) * - Otherwise → Local filesystem source (e.g., `/Volumes/SDCARD`) * * @example * Create from explicit configuration * ```typescript * const config: LocalSourceConfig = { * type: 'local', * sourcePath: '/Volumes/SDCARD', * backupSubdir: 'sdcard' * }; * const source = BackupSourceFactory.create(config); * ``` * * @example * Create from local path with auto-detection * ```typescript * const source = BackupSourceFactory.fromPath('/Volumes/SDCARD', { * backupSubdir: 'gotek', * snapshotRoot: '~/.audiotools/backup' * }); * await source.backup('daily'); * ``` * * @example * Create from remote SSH path with auto-detection * ```typescript * const source = BackupSourceFactory.fromPath('pi@host:/home/pi/images/', { * backupSubdir: 'pi-scsi2' * }); * await source.backup('daily'); * ``` */ export declare class BackupSourceFactory { /** * Create a BackupSource from explicit configuration * * @param config - Backup source configuration (RemoteSourceConfig or LocalSourceConfig) * @returns BackupSource instance (RemoteSource or LocalSource) * * @example * Create a remote source * ```typescript * const source = BackupSourceFactory.create({ * type: 'remote', * host: 'pi@pi-scsi2.local', * sourcePath: '/home/pi/images/', * backupSubdir: 'pi-scsi2' * }); * ``` * * @example * Create a local source * ```typescript * const source = BackupSourceFactory.create({ * type: 'local', * sourcePath: '/Volumes/SDCARD', * backupSubdir: 'sdcard', * snapshotRoot: '~/.audiotools/backup' * }); * ``` */ static create(config: BackupSourceConfig): BackupSource; /** * Create a BackupSource from a path string with automatic type detection * * This is the recommended method for CLI integration and simple use cases. * The factory automatically determines whether the path is a remote SSH * source or local filesystem path. * * @param path - Source path (local or remote SSH syntax) * @param options - Optional configuration overrides * @returns BackupSource instance (RemoteSource or LocalSource) * * @throws Error if path is empty or invalid format * * @remarks * Detection logic: * - Contains `:` and not a Windows path → Remote SSH source (e.g., `host:/path`) * - Otherwise → Local filesystem source (e.g., `/Volumes/SDCARD`) * * Subdirectory naming: * - Remote: Generated from hostname (e.g., `pi-scsi2.local` → `pi-scsi2`) * - Local: Generated from last path component (e.g., `/Volumes/SDCARD` → `sdcard`) * - Override with `options.backupSubdir` for custom naming * * @example * Auto-detect local filesystem path * ```typescript * const source = BackupSourceFactory.fromPath('/Volumes/SDCARD'); * await source.backup('daily'); * // Backs up to: ~/.audiotools/backup/daily.0/sdcard/ * ``` * * @example * Auto-detect remote SSH path * ```typescript * const source = BackupSourceFactory.fromPath('pi@host:/images/'); * await source.backup('daily'); * // Backs up to: ~/.audiotools/backup/daily.0/host/ * ``` * * @example * Override subdirectory name * ```typescript * const source = BackupSourceFactory.fromPath('/Volumes/GOTEK', { * backupSubdir: 'gotek-s3000xl' * }); * await source.backup('daily'); * // Backs up to: ~/.audiotools/backup/daily.0/gotek-s3000xl/ * ``` */ static fromPath(path: string, options?: BackupSourceFromPathOptions): BackupSource; /** * Check if path is a remote SSH path * Remote paths follow the format: [user@]host:path */ private static isRemotePath; /** * Create RemoteSource from SSH path string */ private static createRemoteFromPath; /** * Create LocalSource from filesystem path string */ private static createLocalFromPath; /** * Generate backup subdirectory name from hostname * Examples: "pi-scsi2.local" → "pi-scsi2" */ private static generateSubdirFromHost; /** * Generate backup subdirectory name from filesystem path * Examples: * "/Volumes/SDCARD" → "sdcard" * "/media/user/USB" → "usb" * "local-media" → "local-media" */ private static generateSubdirFromPath; } /** * Options for creating a backup source from a path string * * @example * ```typescript * const options: BackupSourceFromPathOptions = { * backupSubdir: 'my-sampler', * snapshotRoot: '/custom/backup/location' * }; * ``` */ export declare interface BackupSourceFromPathOptions { /** * Sampler name (REQUIRED for local sources, optional for remote) * @example 's5k-studio', 's3k-zulu', 'my-sampler' */ sampler?: string; /** * Device name (REQUIRED for all sources) * @example 'scsi0', 'scsi1', 'floppy' */ device?: string; /** * Backup subdirectory name (DEPRECATED: use device instead) * @deprecated Use device parameter instead */ backupSubdir?: string; /** * Snapshot root directory (DEPRECATED) * @deprecated No longer used with rsync */ snapshotRoot?: string; /** * Config file path (DEPRECATED) * @deprecated No longer used with rsync */ configPath?: string; } /** * Borg archive (snapshot) metadata */ export declare interface BorgArchive { /** Archive name (e.g., "daily-2025-10-05T12:34:56-pi-scsi2") */ name: string; /** Archive creation timestamp */ timestamp: Date; /** Archive statistics */ stats: { /** Original uncompressed size */ originalSize: number; /** Compressed size (before deduplication) */ compressedSize: number; /** Deduplicated size (actual storage used) */ dedupedSize: number; /** Number of files in archive */ nfiles: number; }; /** Archive comment/description (optional) */ comment?: string; } /** * BorgBackup adapter for efficient incremental backups */ export declare class BorgBackupAdapter implements IBorgBackupAdapter { private config; constructor(config: BorgRepositoryConfig); /** * Initialize a new Borg repository */ initRepository(config: BorgRepositoryConfig): Promise<void>; /** * Create a new backup archive */ createArchive(sources: string[], archiveName: string, onProgress?: (progress: BorgProgress) => void): Promise<BorgArchive>; /** * List all archives in repository */ listArchives(): Promise<BorgArchive[]>; /** * Restore specific archive to destination */ restoreArchive(archiveName: string, destination: string, onProgress?: (progress: BorgProgress) => void): Promise<void>; /** * Prune old archives based on retention policy */ pruneArchives(policy: BorgRetentionPolicy): Promise<void>; /** * Get repository information and statistics */ getRepositoryInfo(): Promise<BorgRepositoryInfo>; /** * Check repository consistency */ checkRepository(): Promise<boolean>; /** * Check if archive already exists for today */ hasArchiveForToday(interval: string, source: string): Promise<boolean>; } /** * Result of a Borg command execution */ export declare interface BorgCommandResult { /** Standard output */ stdout: string; /** Standard error */ stderr: string; /** Exit code */ exitCode: number; } /** * Progress information during backup/restore */ export declare interface BorgProgress { /** Current operation (e.g., "Creating archive") */ operation: string; /** Current file being processed */ currentFile?: string; /** Bytes processed so far */ bytesProcessed: number; /** Total bytes to process (estimate) */ totalBytes: number; /** Files processed so far */ filesProcessed: number; /** Total files to process */ totalFiles: number; /** Compression ratio so far */ compressionRatio?: number; /** Deduplication ratio so far */ dedupRatio?: number; } /** * Type definitions for BorgBackup integration */ /** * Borg repository configuration */ export declare interface BorgRepositoryConfig { /** Path to Borg repository (e.g., ~/.audiotools/borg-repo) */ repoPath: string; /** Encryption mode (default: 'none' for simplicity) */ encryption?: 'none' | 'repokey' | 'keyfile'; /** Compression algorithm (default: 'zstd' for balance) */ compression?: 'none' | 'lz4' | 'zstd' | 'zlib,6'; /** SSH connection string for remote repositories (optional) */ sshCommand?: string; } /** * Repository information and statistics */ export declare interface BorgRepositoryInfo { /** Repository path */ path: string; /** Repository ID (unique identifier) */ id: string; /** Last modified timestamp */ lastModified: Date; /** Number of archives in repository */ archiveCount: number; /** Total original size of all archives */ originalSize: number; /** Total compressed size */ compressedSize: number; /** Total deduplicated size (actual disk usage) */ dedupedSize: number; /** Repository encryption mode */ encryption: string; } /** * Retention policy matching rsnapshot intervals */ export declare interface BorgRetentionPolicy { /** Keep last N daily backups */ daily: number; /** Keep last N weekly backups */ weekly: number; /** Keep last N monthly backups */ monthly: number; } /** * Factory function to create an auto-detect backup service. * * @param deviceDetector - Device detector instance * @param promptService - Interactive prompt service instance * @param deviceResolver - Device resolver instance * @returns New AutoDetectBackup instance */ export declare function createAutoDetectBackup(deviceDetector: DeviceDetectorInterface, promptService: InteractivePromptInterface, deviceResolver: DeviceResolver): AutoDetectBackupInterface; /** * Factory function to create an interactive prompt service. * Provides backward compatibility and convenience. * * @param options - Optional configuration * @returns New InteractivePrompt instance */ export declare function createInteractivePrompt(options?: InteractivePromptOptions): InteractivePromptInterface; /** * Detect sampler type by reading disk image header * * Reads only the first 4KB to identify: * - DOS/FAT boot sector (0xaa55 signature at offset 0x1fe) * - S3K native format ("AKAI" signature at offset 0) * - S5K native format ("VOL1" or "PART" signature in first 4KB) * * @param diskImage - Absolute path to disk image file * @returns Detected sampler type or 'unknown' */ export declare function detectSamplerType(diskImage: string): SamplerType; /** * Device type choices for backup sources. * Ordered by commonality (most common first). */ export declare const DEVICE_TYPES: readonly [{ readonly name: "Floppy Disk"; readonly value: "floppy"; readonly description: "Standard 3.5\" floppy disk"; }, { readonly name: "Hard Drive"; readonly value: "hard-drive"; readonly description: "Internal or external hard drive"; }, { readonly name: "CD-ROM"; readonly value: "cd-rom"; readonly description: "CD-ROM or optical disc"; }, { readonly name: "Other"; readonly value: "other"; readonly description: "Other storage device"; }]; /** * Service for matching detected devices to configured backup sources using UUIDs * * This service bridges lib-device-uuid (platform-specific detection) with * audiotools-config (backup source configuration) to provide device identity * tracking across backup sessions. * * Matching strategy: * 1. UUID match (most reliable, works for ext4, HFS+, APFS, NTFS) * 2. Serial match (fallback for FAT32 which lacks persistent UUIDs) * 3. Conflict detection (same UUID/serial on multiple sources) */ export declare class DeviceMatcher { private readonly detector; constructor(detector?: DeviceDetectorInterface); /** * Attempts to match a mount path to an existing backup source by UUID * * @param mountPath - Path where device is mounted * @param sources - Array of configured backup sources * @returns Match result with device info and matched source (if any) */ matchDevice(mountPath: string, sources: BackupSource_2[]): Promise<DeviceMatchResult>; /** * Registers a new device by populating UUID fields in a backup source * * @param source - Backup source to register * @param deviceInfo - Device information from detection * @returns Updated backup source with UUID fields populated */ registerDevice(source: BackupSource_2, deviceInfo: DeviceInfo): BackupSource_2; /** * Updates lastSeen timestamp for a recognized device * * @param source - Backup source to update * @returns Updated backup source with current lastSeen */ updateLastSeen(source: BackupSource_2): BackupSource_2; /** * Checks if a backup source has device UUID information * * @param source - Backup source to check * @returns true if source has UUID or serial */ hasDeviceInfo(source: BackupSource_2): boolean; } export declare interface DeviceMatchResult { matched: boolean; source?: BackupSource_2; deviceInfo: DeviceInfo; reason?: 'uuid' | 'serial' | 'not-found' | 'conflict'; conflictingSources?: BackupSource_2[]; } /** * Orchestrates device UUID registration and recognition workflows * * This service provides the main entry point for device identity tracking. * It handles the complete workflow from device detection through config updates. * * Workflows: * 1. First-time registration: Detects and stores UUID for new devices * 2. Recognition: Matches devices by UUID and updates lastSeen * 3. Conflict resolution: Detects and reports UUID conflicts * 4. Mismatch detection: Warns when expected vs actual device differs */ export declare class DeviceResolver { private readonly matcher; constructor(matcher?: DeviceMatcher); /** * Resolves a mount path to a backup source, registering or recognizing as needed * * This is the main entry point for device UUID tracking. It: * 1. Tries to match by UUID/serial to existing sources * 2. If matched, updates lastSeen timestamp * 3. If not matched and source doesn't have UUID, registers device * 4. Handles conflicts by throwing descriptive errors * * @param mountPath - Path where device is mounted * @param sourceName - Name of the backup source in config * @param config - Full audio-tools configuration * @returns Resolution result with updated source and action taken */ resolveDevice(mountPath: string, sourceName: string, config: AudioToolsConfig): Promise<ResolutionResult>; } /** * Discover all disk images in a directory with metadata * * Combines recursive search with sampler type detection. * This is the main entry point for discovering disk images. * * @param sourceDir - Root directory to search * @returns Array of disk information objects, sorted by path */ export declare function discoverDiskImages(sourceDir: string): DiskInfo[]; /** * Type definitions for sampler backup functionality using rsnapshot */ /** * Information about a discovered disk image file */ export declare interface DiskImageInfo { /** Absolute path to disk image file */ path: string; /** Filename without extension */ name: string; /** File size in bytes */ size: number; /** Last modification time */ mtime: Date; } /** * Media detection and disk image discovery for local storage media * Supports macOS and Linux platform-specific mount point detection */ /** * Information about a discovered disk image file */ declare interface DiskImageInfo_2 { /** Absolute path to disk image file */ path: string; /** Filename without extension */ name: string; /** File size in bytes */ size: number; /** Last modification time */ mtime: Date; } /** * Information about a discovered disk image */ export declare interface DiskInfo { /** Absolute path to disk image file */ path: string; /** Filename without extension */ name: string; /** Detected sampler type from disk header */ samplerType: SamplerType; /** Last modification time */ mtime: Date; } /** * Filesystem operations interface for dependency injection */ declare interface FileSystemOperations { readdir(path: string): Promise<string[]>; stat(path: string): Promise<{ isDirectory(): boolean; isFile(): boolean; size: number; mtime: Date; }>; platform(): string; } /** * Filesystem operations interface for dependency injection */ declare interface FileSystemOperations_2 { stat(path: string): Promise<{ size: number; mtime: Date; isFile(): boolean; isDirectory(): boolean; }>; mkdir(path: string, options?: { recursive: boolean; }): Promise<void>; unlink(path: string): Promise<void>; utimes(path: string, atime: Date, mtime: Date): Promise<void>; createReadStream(path: string): NodeJS.ReadableStream; createWriteStream(path: string): NodeJS.WritableStream; readdir(path: string): Promise<string[]>; } /** * Find all disk images recursively in a directory * * Searches for files with extensions: .hds, .img, .iso * Skips hidden directories (starting with .) * * @param dir - Directory to search * @param maxDepth - Maximum recursion depth (default: 3) * @returns Array of absolute paths to disk image files */ export declare function findDiskImagesRecursive(dir: string, maxDepth?: number): string[]; /** * Core Borg backup adapter interface * * This adapter provides a clean interface to BorgBackup for the * sampler-backup system, handling repository management, archive * creation, restoration, and pruning. * * @example * ```typescript * const adapter = new BorgBackupAdapter({ * repoPath: '~/.audiotools/borg-repo', * compression: 'zstd', * encryption: 'none' * }); * * // Create backup * const archive = await adapter.createArchive( * ['/Volumes/SDCARD'], * 'daily-2025-10-05-local-media' * ); * * // Prune old archives * await adapter.pruneArchives({ * daily: 7, * weekly: 4, * monthly: 12 * }); * ``` */ export declare interface IBorgBackupAdapter { /** * Initialize a new Borg repository * * Creates a new repository with the specified configuration. * This is a one-time operation per repository. * * @param config Repository configuration * @throws Error if repository already exists or cannot be created */ initRepository(config: BorgRepositoryConfig): Promise<void>; /** * Create a new backup archive * * Creates a new archive in the repository containing the specified * source paths. Supports both local paths and SSH remote paths. * * @param sources Array of paths to backup (can be local or SSH remote) * @param archiveName Name for the archive (must be unique in repository) * @param onProgress Optional callback for progress updates * @returns Archive metadata with statistics * @throws Error if backup fails or archive name already exists * * @example * ```typescript * // Local backup * const archive = await adapter.createArchive( * ['/Volumes/SDCARD/HD0.hds', '/Volumes/SDCARD/HD1.hds'], * 'daily-2025-10-05-local-media', * (progress) => console.log(`${progress.bytesProcessed} bytes`) * ); * * // Remote SSH backup * const archive = await adapter.createArchive( * ['pi@pi-scsi2.local:/home/orion/images/'], * 'daily-2025-10-05-pi-scsi2' * ); * ``` */ createArchive(sources: string[], archiveName: string, onProgress?: (progress: BorgProgress) => void): Promise<BorgArchive>; /** * List all archives in repository * * Returns metadata for all archives, sorted by creation timestamp. * * @returns Array of archive metadata * @throws Error if repository cannot be accessed */ listArchives(): Promise<BorgArchive[]>; /** * Restore specific archive to destination * * Extracts all files from the specified archive to the destination path. * Creates destination directory if it doesn't exist. * * @param archiveName Name of archive to restore * @param destination Path where files should be extracted * @param onProgress Optional callback for progress updates * @throws Error if archive doesn't exist or restore fails * * @example * ```typescript * await adapter.restoreArchive( * 'daily-2025-10-05-pi-scsi2', * '/tmp/restored-backup' * ); * ``` */ restoreArchive(archiveName: string, destination: string, onProgress?: (progress: BorgProgress) => void): Promise<void>; /** * Prune old archives based on retention policy * * Removes archives that don't match the retention rules. * Borg automatically handles the logic of which archives to keep. * * @param policy Retention policy (daily, weekly, monthly counts) * @throws Error if prune operation fails * * @example * ```typescript * await adapter.pruneArchives({ * daily: 7, * weekly: 4, * monthly: 12 * }); * ``` */ pruneArchives(policy: BorgRetentionPolicy): Promise<void>; /** * Get repository information and statistics * * Returns detailed information about the repository including * size statistics and archive count. * * @returns Repository metadata and statistics * @throws Error if repository cannot be accessed */ getRepositoryInfo(): Promise<BorgRepositoryInfo>; /** * Check repository consistency * * Runs Borg's integrity check to verify repository is not corrupted. * This can be a slow operation for large repositories. * * @returns True if repository is consistent, false otherwise */ checkRepository(): Promise<boolean>; /** * Check if archive already exists for today * * Used for same-day resume logic to avoid duplicate backups. * * @param interval Backup interval (daily, weekly, monthly) * @param source Source identifier (e.g., "pi-scsi2", "local-media") * @returns True if archive exists for today with this interval/source */ hasArchiveForToday(interval: string, source: string): Promise<boolean>; } /** * Interactive prompt service implementation using inquirer. * * Provides user-friendly prompts for collecting backup configuration. * Implements validation and handles user cancellation gracefully. * * @example * ```typescript * const promptService = new InteractivePrompt(); * * // Prompt for device type * const deviceType = await promptService.promptDeviceType(); * console.log(`Selected: ${deviceType}`); * * // Prompt for sampler * const existingSamplers = ['s5000', 's3000xl']; * const { sampler, isNew } = await promptService.promptSampler(existingSamplers); * if (isNew) { * console.log(`Created new sampler: ${sampler}`); * } else { * console.log(`Selected existing sampler: ${sampler}`); * } * ``` */ export declare class InteractivePrompt implements InteractivePromptInterface { private readonly options; constructor(options?: InteractivePromptOptions); /** * Prompt user to select a device type. */ promptDeviceType(): Promise<string>; /** * Prompt user to select existing sampler or add new. */ promptSampler(existingSamplers: string[]): Promise<SamplerPromptResult>; /** * Prompt user to enter a new sampler name. */ promptNewSamplerName(): Promise<string>; /** * Get input/output stream options for inquirer prompts. * Used for testing with custom streams. */ private getStreamOptions; /** * Handle user cancellation (Ctrl+C) gracefully. * Converts inquirer errors to UserCancelledError. */ private handleCancellation; } /** * Interface for interactive prompt operations. * * Defines the contract for prompting users for backup configuration. * Use this interface for dependency injection and testing. */ export declare interface InteractivePromptInterface { /** * Prompt user to select a device type. * * @returns Promise resolving to selected device type * @throws {UserCancelledError} If user cancels with Ctrl+C */ promptDeviceType(): Promise<string>; /** * Prompt user to select an existing sampler or add a new one. * * Shows list of existing samplers with option to add new. * If user chooses "Add new", prompts for sampler name. * * @param existingSamplers - List of sampler names from config * @returns Promise resolving to sampler selection result * @throws {UserCancelledError} If user cancels with Ctrl+C */ promptSampler(existingSamplers: string[]): Promise<SamplerPromptResult>; /** * Prompt user to enter a name for a new sampler. * * @returns Promise resolving to new sampler name * @throws {UserCancelledError} If user cancels with Ctrl+C */ promptNewSamplerName(): Promise<string>; } /** * Options for configuring the interactive prompt service. */ export declare interface InteractivePromptOptions { /** * Custom input/output streams for testing. * If not provided, uses process.stdin/stdout. */ input?: NodeJS.ReadableStream; output?: NodeJS.WritableStream; } /** * LocalBackupAdapter - Handles file-based incremental backups * * Features: * - Incremental copying (skip unchanged files) * - Timestamp preservation * - Progress tracking for large files * - Error handling with partial file cleanup */ export declare class LocalBackupAdapter { private readonly fs; constructor(fsOps?: FileSystemOperations_2); /** * Perform backup from source to destination */ backup(options: LocalBackupOptions_2): Promise<LocalBackupResult_2>; /** * Recursively discover files to backup */ private discoverFiles; /** * Determine if a file should be copied based on incremental logic * Returns true if: * - Destination doesn't exist * - Source is newer than destination (mtime) * - Source size differs from destination */ private shouldCopyFile; /** * Copy file with progress tracking and timestamp preservation */ private copyFileWithProgress; /** * Ensure directory exists, creating it recursively if needed */ private ensureDirectory; } /** * Options for local backup operations */ export declare interface LocalBackupOptions { /** Source directory (e.g., /Volumes/SDCARD) */ sourcePath: string; /** Destination directory (e.g., ~/.audiotools/backup/daily.0/local-media) */ destPath: string; /** Skip unchanged files based on mtime and size (default: true) */ incremental?: boolean; /** Progress callback invoked during backup */ onProgress?: (progress: BackupProgress) => void; } /** * Local backup adapter for copying disk images from local media * Implements incremental backup with progress tracking */ /** * Options for local backup operations */ declare interface LocalBackupOptions_2 { /** Source directory (e.g., /Volumes/SDCARD) */ sourcePath: string; /** Destination directory (e.g., ~/.audiotools/backup/daily.0/local-media) */ destPath: string; /** Skip unchanged files based on mtime and size (default: true) */ incremental?: boolean; /** Progress callback invoked during backup */ onProgress?: (progress: BackupProgress_2) => void; } /** * Result of a local backup operation */ export declare interface LocalBackupResult { /** Whether the backup completed successfully */ success: boolean; /** Number of files processed */ filesProcessed: number; /** Number of files copied */ filesCopied: number; /** Number of files skipped (unchanged) */ filesSkipped: number; /** Total bytes processed */ bytesProcessed: number; /** Errors encountered during backup */ errors: string[]; } /** * Result of a backup operation */ declare interface LocalBackupResult_2 { /** Whether the backup completed successfully */ success: boolean; /** Number of files processed */ filesProcessed: number; /** Number of files copied */ filesCopied: number; /** Number of files skipped (unchanged) */ filesSkipped: number; /** Total bytes processed */ bytesProcessed: number; /** Errors encountered during backup */ errors: string[]; } /** * LocalSource - File-based backup source for local media * * Uses RsyncAdapter for simple file-level synchronization without snapshots, * rotation, or deduplication. Fast and straightforward. */ export declare class LocalSource implements BackupSource { private readonly config; readonly type: "local"; private readonly rsyncAdapter; private readonly backupPath; constructor(config: LocalSourceConfig, rsyncAdapter?: RsyncAdapter); /** * Execute local backup using rsync */ backup(interval: RsnapshotInterval): Promise<BackupResult>; /** * Test if local source is accessible */ test(): Promise<boolean>; /** * Get source configuration */ getConfig(): LocalSourceConfig; /** * Get backup path for this source */ getBackupPath(): string; } /** * Configuration for local file-based backup sources */ export declare interface LocalSourceConfig { /** Source type identifier */ type: 'local'; /** Local source path to backup from (e.g., "/Volumes/SDCARD") */ sourcePath: string; /** Sampler name (REQUIRED: s5k-studio, s3k-zulu, etc.) */ sampler: string; /** Device name (REQUIRED: scsi0, scsi1, floppy, etc.) */ device: string; /** Local backup subdirectory name (DEPRECATED: use device instead) */ backupSubdir: string; /** Optional override for snapshot root directory (DEPRECATED: not used with Borg) */ snapshotRoot?: string; } /** * MediaDetector - Detects storage media and discovers disk images * * Platform support: * - macOS: Scans /Volumes/ (excludes system volumes) * - Linux: Scans /media/$USER/ and /mnt/ * * Supported disk image formats: .hds, .img, .iso */ export declare class MediaDetector { private readonly fs; private static readonly DISK_IMAGE_EXTENSIONS; private static readonly HIDDEN_FILE_PREFIX; private static readonly MACOS_SYSTEM_VOLUMES; constructor(fsOps?: FileSystemOperations); /** * Detect all available storage media on the system * @returns Array of MediaInfo for each detected media */ detectMedia(): Promise<MediaInfo_2[]>; /** * Recursively find disk images in a directory * @param path - Directory path to search * @returns Array of DiskImageInfo for discovered disk images */ findDiskImages(path: string): Promise<DiskImageInfo_2[]>; /** * Get platform-specific mount points to scan */ private getMountPoints; /** * Get macOS mount points from /Volumes/ * Excludes system volumes like "Macintosh HD" */ private getMacOSMountPoints; /** * Get Linux mount points from /media/$USER/ and /mnt/ */ private getLinuxMountPoints; /** * Recursively scan a directory for disk images */ private scanDirectory; /** * Check if a filename has a disk image extension */ private isDiskImage; } /** * Information about detected storage media */ export declare interface MediaInfo { /** Mount point path (e.g., "/Volumes/SDCARD") */ mountPoint: string; /** Volume name (e.g., "SDCARD") */ volumeName: string; /** Disk images found on this media */ diskImages: DiskImageInfo[]; } /** * Information about detected storage media */ declare interface MediaInfo_2 { /** Mount point path (e.g., "/Volumes/SDCARD") */ mountPoint: string; /** Volume name (e.g., "SDCARD") */ volumeName: string; /** Disk images found on this media */ diskImages: DiskImageInfo_2[]; } /** * RemoteSource - SSH-based backup source using rsync * * Implements BackupSource interface using rsync to sync remote directories * to local backup storage with hierarchical organization. */ export declare class RemoteSource implements BackupSource { private readonly config; readonly type: "remote"; private readonly rsyncAdapter; private readonly backupPath; constructor(config: RemoteSourceConfig, rsyncAdapter?: RsyncAdapter); /** * Execute remote backup using rsync * * Simple synchronization: user@host:/path → ~/.audiotools/backup/{sampler}/{device}/ */ backup(interval: RsnapshotInterval): Promise<BackupResult>; /** * Test if remote source is accessible * * Tests SSH connectivity to remote host. */ test(): Promise<boolean>; /** * Get source configuration */ getConfig(): RemoteSourceConfig; /** * Get the backup path for this source */ getBackupPath(): string; } /** * Configuration for remote SSH-based backup sources */ export declare interface RemoteSourceConfig { /** Source type identifier */ type: 'remote'; /** Remote host (e.g., "pi-scsi2.local") */ host: string; /** Remote source path to backup (e.g., "/home/pi/images/") */ sourcePath: string; /** Device name (REQUIRED: scsi0, scsi1, floppy, etc.) */ device: string; /** Optional sampler name override (defaults to hostname) */ sampler?: string; /** @deprecated Use device instead - will be removed in 2.0 */ backupSubdir?: string; } export declare interface ResolutionResult { source: BackupSource_2; action: 'registered' | 'recognized' | 'updated' | 'no-change'; message: string; } /** * Backup interval type (reused from rsnapshot for compatibility) */ export declare type RsnapshotInterval = "daily" | "weekly" | "monthly"; /** * Simple wrapper around rsync for file synchronization. * * Provides a clean interface for syncing files from local or remote sources * to local destinations. Supports both SSH-based remote sync and local sync. * * @example * ```typescript * const adapter = new RsyncAdapter(); * * // Remote sync * await adapter.sync({ * sourcePath: 'user@host:/remote/path/', * destPath: '/local/backup/path/', * }); * * // Local sync * await adapter.sync({ * sourcePath: '/Volumes/DSK0/', * destPath: '/local/backup/path/', * verbose: true, * }); * ``` */ declare class RsyncAdapter { /** * Synchronize files from source to destination using rsync. * * Uses rsync's archive mode (-a) to preserve permissions, timestamps, etc. * Automatically deletes files in destination that don't exist in source. * * @param config - Rsync configuration * @throws {Error} If rsync command fails or is not available * * @example * ```typescript * await adapter.sync({ * sourcePath: 'pi-scsi2:/home/orion/images/', * destPath: '~/.audiotools/backup/pi-scsi2/scsi0/', * dryRun: true, // Preview changes first * }); * ``` */ sync(config: RsyncConfig): Promise<void>; /** * Check if rsync is available on the system. * * Attempts to execute `rsync --version` to verify availability. * * @returns Promise that resolves to true if rsync is available, false otherwise * * @example * ```typescript * const available = await adapter.checkRsyncAvailable(); * if (!available) { * throw new Error('rsync is not installed. Please install rsync to continue.'); * } * ``` */ checkRsyncAvailable(): Promise<boolean>; } /** * Configuration for rsync synchronization operation */ declare interface RsyncConfig { /** Source path - can be local path or user@host:/path for remote */ sourcePath: string; /** Destination path - local directory */ destPath: string; /** Enable verbose output (default: false) */ verbose?: boolean; /** Perform a dry run without making changes (default: false) */ dryRun?: boolean; } /** * Result of sampler selection prompt. */ export declare interface SamplerPromptResult { /** The selected or newly entered sampler name */ sampler: string; /** Whether this is a new sampler (true) or existing (false) */ isNew: boolean; } /** * Disk Scanner - Find disk images in backup directories * * This is the single source of truth for finding disk images. * Both backup and export tools must use this logic to ensure consistency. */ /** * Sampler type determined from disk image header */ export declare type SamplerType = 's3k' | 's5k' | 'unknown'; /** * Interactive prompt service for sampler backup CLI. * * Provides user-friendly prompts for collecting missing backup configuration * information. Implements the minimal-command UX principle by asking only * for information that cannot be auto-detected. * * @module prompt/interactive-prompt */ /** * Custom error thrown when user cancels the interactive prompt. * Catch this to handle Ctrl+C gracefully in CLI workflows. */ export declare class UserCancelledError extends Error { constructor(message?: string); } export { }