@oletizi/sampler-backup
Version:
Akai sampler backup utilities for hardware samplers via PiSCSI
1,460 lines (1,403 loc) • 47.1 kB
TypeScript
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 { }