Files
Oxicloud/src/application/ports/dedup_ports.rs
T
Diocrafts 761d159a92 feat(dedup): CDC sub-file deduplication with FastCDC + parallel chunk storage + dedup skip
- Replace whole-file SHA-256 dedup with FastCDC 2020 content-defined chunking
  (min 64KB, avg 256KB, max 1MB) + BLAKE3 hashing
- Add chunk_manifests table (file_hash → chunk_hashes[] + chunk_sizes[])
- Add put_blob_from_bytes to BlobStorageBackend trait (all 7 backends)
- 3-phase store_chunks pipeline:
  Phase 0: batch-check existing chunks (single PG query)
  Phase 1: selective disk read (skip existing chunks entirely)
  Phase 2: parallel upload with buffer_unordered(8)
- CDC-aware read_blob_stream and read_blob_range_stream with legacy fallback
- Transactional manifest + chunk ref-count cascade on remove_reference
- 12 CDC tests (determinism, reassembly, contiguity, sub-file dedup, etc.)
- Update deduplication.md to reflect new architecture
2026-04-14 23:17:39 +02:00

150 lines
5.1 KiB
Rust

//! Deduplication Port - Application layer abstraction for content-addressable storage.
//!
//! This module defines the port (trait) and DTOs for deduplication operations,
//! keeping the application and interface layers independent of the specific
//! content-addressable storage implementation.
use crate::common::errors::DomainError;
use bytes::Bytes;
use futures::Stream;
use serde::Serialize;
use std::path::{Path, PathBuf};
use std::pin::Pin;
/// Metadata of a stored blob in the dedup system.
#[derive(Debug, Clone, Serialize)]
pub struct BlobMetadataDto {
/// BLAKE3 hash of the content.
pub hash: String,
/// Size in bytes.
pub size: u64,
/// Number of references to this blob.
pub ref_count: u32,
/// Original content type (for serving).
pub content_type: Option<String>,
}
/// Result of a deduplication store operation.
#[derive(Debug, Clone)]
pub enum DedupResultDto {
/// New content was stored (first occurrence).
NewBlob { hash: String, size: u64 },
/// Content already existed; a reference was added instead.
ExistingBlob {
hash: String,
size: u64,
saved_bytes: u64,
},
}
impl DedupResultDto {
pub fn hash(&self) -> &str {
match self {
DedupResultDto::NewBlob { hash, .. } => hash,
DedupResultDto::ExistingBlob { hash, .. } => hash,
}
}
pub fn size(&self) -> u64 {
match self {
DedupResultDto::NewBlob { size, .. } => *size,
DedupResultDto::ExistingBlob { size, .. } => *size,
}
}
pub fn was_deduplicated(&self) -> bool {
matches!(self, DedupResultDto::ExistingBlob { .. })
}
}
/// Statistics for the deduplication service.
#[derive(Debug, Clone, Default, Serialize)]
pub struct DedupStatsDto {
/// Total number of unique blobs.
pub total_blobs: u64,
/// Total bytes stored (actual disk usage).
pub total_bytes_stored: u64,
/// Total bytes referenced (logical size).
pub total_bytes_referenced: u64,
/// Bytes saved through deduplication.
pub bytes_saved: u64,
/// Number of deduplication hits.
pub dedup_hits: u64,
/// Deduplication ratio (referenced / stored).
pub dedup_ratio: f64,
}
/// Port for content-addressable deduplication operations.
///
/// Implementations store files by their content hash, eliminating
/// duplicate storage automatically. Multiple file references can
/// point to the same physical blob.
pub trait DedupPort: Send + Sync + 'static {
/// Store content with deduplication (streaming from file).
///
/// If `pre_computed_hash` is provided (e.g. hash-on-write from the handler),
/// the file will NOT be re-read to calculate the hash — saving one full
/// sequential read of the file.
async fn store_from_file(
&self,
source_path: &Path,
content_type: Option<String>,
pre_computed_hash: Option<String>,
) -> Result<DedupResultDto, DomainError>;
/// Check if a blob with the given hash exists.
async fn blob_exists(&self, hash: &str) -> bool;
/// Get metadata for a blob.
async fn get_blob_metadata(&self, hash: &str) -> Option<BlobMetadataDto>;
/// Stream blob content in chunks (64 KB default) — constant memory usage.
async fn read_blob_stream(
&self,
hash: &str,
) -> Result<Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>, DomainError>;
/// Stream a byte range of a blob — only reads the requested portion.
///
/// Uses seek + take so a 1 MB range on a 1 GB file only reads 1 MB from disk.
async fn read_blob_range_stream(
&self,
hash: &str,
start: u64,
end: Option<u64>,
) -> Result<Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>, DomainError>;
/// Get the size of a blob without reading its content.
///
/// Used by HEAD requests to return Content-Length without loading the file.
async fn blob_size(&self, hash: &str) -> Result<u64, DomainError>;
/// Add a reference to a blob (increment ref_count).
async fn add_reference(&self, hash: &str) -> Result<(), DomainError>;
/// Remove a reference from a blob.
///
/// Returns `true` if the blob was deleted (ref_count reached 0).
async fn remove_reference(&self, hash: &str) -> Result<bool, DomainError>;
/// Calculate BLAKE3 hash of a file (streaming).
async fn hash_file(&self, path: &Path) -> Result<String, DomainError>;
/// Get the physical filesystem path for a blob by its hash.
///
/// Returns the path where the blob is stored on disk.
/// Used by services that need direct filesystem access (e.g., thumbnail generation).
fn blob_path(&self, hash: &str) -> PathBuf;
/// Get deduplication statistics.
async fn get_stats(&self) -> DedupStatsDto;
/// Flush the index to persistent storage.
async fn flush(&self) -> Result<(), DomainError>;
/// Verify integrity of all stored blobs.
///
/// Returns a list of issues found (empty if everything is OK).
async fn verify_integrity(&self) -> Result<Vec<String>, DomainError>;
}