class BackupRepository # docker:cli-backed adapter for an external host mount (#105). Listing # is served from SQLite; mutating ops dispatch a short-lived sidecar # that mounts the configured `external_path` as `/data`. module External IMAGE = BackupRunner::IMAGE class << self include BackupRepository::Tracking include BackupRepository::Sidecar def directory nil end def host_directory Configuration.current.backup.external_path.presence end def destination_configured? host_directory.present? end def destination_key 'external' end def destination_coords { external_path: host_directory } end def record_backup!(filename) raise BackupRepository::NotFound unless BackupRepository.valid_filename?(filename) return nil unless destination_configured? metadata = fetch_metadata(filename) return nil unless metadata && metadata[:bytes] entries = metadata[:entries].map { |e| BackupRepository::Entry.new(name: e[:name], bytes: e[:bytes]) } archive = BackupRepository::ArchiveContents.new(entries: entries, config: metadata[:config]) upsert_backup_record(filename, metadata[:bytes], archive) end def download(filename, &) raise BackupRepository::NotFound unless BackupRepository.valid_filename?(filename) raise BackupRepository::NotFound unless host_directory raise BackupRepository::NotFound unless recorded?(filename) stream_sidecar_download('cat', sidecar_path(filename), &) end # An external bind mount is not reachable from the browser; downloads # must go through the sidecar stream. def direct_download_url(_filename) nil end def read_archive_for(filename) return BackupRepository::EMPTY_ARCHIVE unless host_directory stream_sidecar_archive('cat', "/data/#{filename}") end def remove_files!(filenames) return if filenames.empty? return unless host_directory args = filenames.map { |filename| "/data/#{filename}" } sidecar_run!('rm', '-f', *args) end private # In-container path rebuilt from the matched date/time digits as # integers so the value provably carries no path traversal or shell # metacharacters regardless of the caller. def sidecar_path(filename) match = BackupRepository::FILENAME_PATTERN.match(filename.to_s) raise BackupRepository::NotFound unless match format('/data/solectrus-backup-%08d-%