//! Blob Storage Backend Port — abstracts raw byte I/O for content-addressable storage. //! //! This trait decouples `DedupService` from any specific storage medium. //! Implementations include: //! - `LocalBlobBackend` — local filesystem (default) //! - `S3BlobBackend` — any S3-compatible service (AWS, Backblaze B2, MinIO, R2…) //! //! `DedupService` owns an `Arc` and delegates all //! byte-level I/O through this trait, keeping BLAKE3 hashing, ref-counting //! and PostgreSQL index logic in `DedupService` itself. use bytes::Bytes; use futures::Stream; use serde::Serialize; use std::future::Future; use std::path::{Path, PathBuf}; use std::pin::Pin; use crate::domain::errors::DomainError; /// Boxed future alias used by [`BlobStorageBackend`] to keep the trait dyn-compatible. type BoxFut<'a, T> = Pin + Send + 'a>>; /// Pinned boxed byte stream — the return type for blob reads. pub type BlobStream = Pin> + Send>>; /// Health-check result returned by [`BlobStorageBackend::health_check`]. #[derive(Debug, Clone, Serialize)] pub struct StorageHealthStatus { /// Whether the backend is reachable and functional. pub connected: bool, /// Human-readable backend identifier (e.g. `"local"`, `"s3"`). pub backend_type: String, /// Descriptive status message. pub message: String, /// Available space in bytes, if the backend can report it. pub available_bytes: Option, } /// Minimal trait for blob byte I/O — decoupled from dedup logic. /// /// Every method operates on a *hash key* that uniquely identifies a blob. /// The backend is responsible for mapping the hash to its own addressing /// scheme (filesystem path, S3 key, etc.). /// /// Returns boxed futures so the trait is dyn-compatible (`Arc`). pub trait BlobStorageBackend: Send + Sync + 'static { /// Perform any one-time setup (create directories, verify bucket, etc.). fn initialize(&self) -> BoxFut<'_, Result<(), DomainError>>; /// Store a blob from a local temporary file. /// /// Must be **idempotent**: if the blob already exists the call succeeds /// without overwriting. Returns the number of bytes stored. fn put_blob(&self, hash: &str, source_path: &Path) -> BoxFut<'_, Result>; /// Store a blob from in-memory bytes (used by CDC chunk storage). /// /// Must be **idempotent**: if the blob already exists the call succeeds /// without overwriting. Returns the number of bytes stored. fn put_blob_from_bytes(&self, hash: &str, data: Bytes) -> BoxFut<'_, Result>; /// Stream the full blob content in chunks. fn get_blob_stream(&self, hash: &str) -> BoxFut<'_, Result>; /// Stream a byte range of the blob (for HTTP Range requests / video seek). fn get_blob_range_stream( &self, hash: &str, start: u64, end: Option, ) -> BoxFut<'_, Result>; /// Delete a blob by hash. Must be **idempotent** (no error if already gone). fn delete_blob(&self, hash: &str) -> BoxFut<'_, Result<(), DomainError>>; /// Check if a blob exists in the backend. fn blob_exists(&self, hash: &str) -> BoxFut<'_, Result>; /// Get blob size in bytes without downloading content. fn blob_size(&self, hash: &str) -> BoxFut<'_, Result>; /// Verify connectivity and permissions (used by the admin "Test Connection" button). fn health_check(&self) -> BoxFut<'_, Result>; /// Return the backend type name for display (e.g. `"local"`, `"s3"`). fn backend_type(&self) -> &'static str; /// Return the local filesystem path for a blob, if available. /// /// Only meaningful for local-filesystem backends. Remote backends /// return `None`; callers that need a local file must stream + spool. fn local_blob_path(&self, hash: &str) -> Option; }