OxiCloud uses **content-defined chunking (CDC)** via FastCDC for sub-file deduplication. Files are split into variable-size chunks (64 KB – 1 MB, average 256 KB) using the FastCDC 2020 algorithm. Each chunk is individually BLAKE3-hashed and stored in a pluggable blob backend (local FS, S3, Azure). A PostgreSQL *manifest* maps the whole-file BLAKE3 hash to the ordered list of chunk hashes that compose it. Identical chunks across any files are stored once and reference-counted.
Deduplication is always enabled and non-fatal — if dedup fails, file operations proceed normally with a warning log.
**Backward compatibility**: files uploaded before CDC (legacy whole-file blobs in `storage.blobs`) are served transparently. When no manifest row exists for a hash, the service falls back to direct blob reads.
The **biggest I/O saving** for versioned files. Before reading any chunk from disk or uploading it to the blob backend, `store_chunks` batch-queries PG to discover which chunk hashes already exist:
```sql
SELECThashFROMstorage.blobsWHEREhash=ANY($1)
```
This single round-trip returns all known chunks. For each existing chunk, the service skips:
-`seek()` + `read_exact()` from the source file (no disk I/O)
-`put_blob_from_bytes()` to the backend (no network I/O for S3/Azure)
Only a lightweight `UPDATE ref_count + 1` is executed in PG (~0.1 ms per chunk).
**Impact**: for a 100 MB versioned file where 95% of chunks are unchanged, only ~5 MB is read from disk and uploaded. The remaining 95% costs only PG ref-count bumps.
### Parallel Chunk Storage
Phase 2 of `store_chunks` uses `futures::stream::buffer_unordered(8)` to execute up to 8 concurrent chunk operations. This is a major win for S3/Azure backends where each PUT has 50-200 ms of network latency.
Chunk order in the returned `(chunk_hashes, chunk_sizes)` is preserved by deriving both from the original `ChunkMeta` slice (CDC order), not from the unordered parallel results.
### Full-File Dedup Hit (Fast Path)
When a file with the exact same BLAKE3 hash already has a manifest, `try_dedup_hit` returns immediately:
- Bumps `chunk_manifests.ref_count`
- Deletes the source file
- Returns `ExistingBlob` — **zero chunk I/O**
Also checks legacy whole-file blobs in `storage.blobs` for backward compatibility.
## Read Path
### Streaming Read (`read_blob_stream`)
CDC-aware with legacy fallback:
1. Query `chunk_manifests` for `chunk_hashes[]`
2. If found: stream chunks in order via `backend.get_blob_stream(chunk_hash)`, concatenated into a single byte stream with `buffered(1) + try_flatten`
3. If not found: fall back to `backend.get_blob_stream(hash)` for legacy blobs
### Range Read (`read_blob_range_stream`)
For HTTP Range requests (and WOPI/WebDAV partial reads):
1. Query manifest for `chunk_hashes[]`, `chunk_sizes[]`, `total_size`
2. Calculate which chunks overlap `[start, end)` using cumulative offsets
3. For each overlapping chunk, compute the sub-range within that chunk
4. Stream only the relevant chunk portions via `backend.get_blob_range_stream()`
### Blob Size (`blob_size`)
Returns `total_size` from the manifest (O(1) PG lookup). Falls back to `backend.blob_size()` for legacy blobs. Used by HEAD requests for Content-Length.
## Reference Counting
### Adding References (`add_reference`)
Manifest-aware with legacy fallback:
1. Try `UPDATE chunk_manifests SET ref_count = ref_count + 1 WHERE file_hash = $1`
2. If no rows affected, try `UPDATE storage.blobs SET ref_count + 1 WHERE hash = $1`
3. If neither exists, return NotFound error
### Removing References (`remove_reference`)
**CDC manifest path** (transactional):
1. Check `chunk_manifests` for the file hash
2. If `ref_count > 1`: decrement manifest ref_count → commit
3. If `ref_count == 1` (last reference):
-`SELECT ... FOR UPDATE` to lock the manifest row
-`DELETE FROM chunk_manifests`
-`UPDATE storage.blobs SET ref_count = ref_count - 1 WHERE hash = ANY(chunk_hashes)`
-`DELETE FROM storage.blobs WHERE hash = ANY(chunk_hashes) AND ref_count <= 0 RETURNING hash`
- Commit TX
- Delete orphaned chunk blob files from backend (after commit)
**Legacy blob path** (transactional):
1.`SELECT ref_count, size FROM storage.blobs WHERE hash = $1 FOR UPDATE`
2. If `ref_count == 1`: `DELETE FROM storage.blobs` + delete blob file
3. If `ref_count > 1`: `UPDATE SET ref_count = ref_count - 1`
- **CDC analysis in `spawn_blocking`**: mmap + FastCDC runs off the async runtime to avoid blocking the event loop
- **Dual PG pools**: request-path operations use the primary pool; `verify_integrity` and `garbage_collect` use the maintenance pool to prevent starvation
- **Pluggable blob backend**: all chunk I/O goes through `Arc<dyn BlobStorageBackend>` — works with local FS, S3, Azure, or any composed backend (retry, encryption, caching)
- **Atomic chunk storage**: `put_blob_from_bytes` is idempotent; `INSERT ON CONFLICT` handles concurrent uploads of the same chunk
- **Delete-after-commit**: blob files are deleted from the backend only after the PG transaction commits, preventing orphaned PG rows
- **Flush is no-op**: PG handles durability via WAL/commit — no explicit index persistence needed
1.**Phase 1 — Orphaned manifests**: `DELETE FROM chunk_manifests WHERE ref_count <= 0` (batches of 500). For each deleted manifest, `UPDATE storage.blobs SET ref_count = ref_count - 1` for its chunks.
2.**Phase 2 — Orphaned blobs**: `DELETE FROM storage.blobs WHERE ref_count <= 0` (batches of 500). Deletes blob files from backend + thumbnail cleanup (best-effort).