server: Factor out CompressionStream

This commit is contained in:
Zhaofeng Li
2025-07-08 12:48:02 -06:00
parent 5bfbc953eb
commit 96fe8b8620
3 changed files with 85 additions and 99 deletions
+3 -99
View File
@@ -14,7 +14,6 @@ use axum::{
};
use bytes::{Bytes, BytesMut};
use chrono::Utc;
use digest::Output as DigestOutput;
use futures::future::join_all;
use futures::StreamExt;
use sea_orm::entity::prelude::*;
@@ -22,13 +21,14 @@ use sea_orm::sea_query::Expr;
use sea_orm::ActiveValue::Set;
use sea_orm::{QuerySelect, TransactionTrait};
use sha2::{Digest, Sha256};
use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt, BufReader};
use tokio::sync::{OnceCell, Semaphore};
use tokio::io::{AsyncBufRead, AsyncReadExt};
use tokio::sync::Semaphore;
use tokio::task::spawn;
use tokio_util::io::StreamReader;
use tracing::instrument;
use uuid::Uuid;
use crate::compression::{CompressionStream, CompressorFn};
use crate::config::CompressionType;
use crate::error::{ErrorKind, ServerError, ServerResult};
use crate::narinfo::Compression;
@@ -55,8 +55,6 @@ use crate::database::{AtticDatabase, ChunkGuard, NarGuard};
/// TODO: Make this configurable
const CONCURRENT_CHUNK_UPLOADS: usize = 10;
type CompressorFn<C> = Box<dyn FnOnce(C) -> Box<dyn AsyncRead + Unpin + Send> + Send>;
/// Data of a chunk.
enum ChunkData {
/// Some bytes in memory.
@@ -72,33 +70,6 @@ struct UploadChunkResult {
deduplicated: bool,
}
/// Applies compression to a stream, computing hashes along the way.
///
/// Our strategy is to stream directly onto a UUID-keyed file on the
/// storage backend, performing compression and computing the hashes
/// along the way. We delete the file if the hashes do not match.
///
/// ```text
/// ┌───────────────────────────────────►NAR Hash
/// │
/// │
/// ├───────────────────────────────────►NAR Size
/// │
/// ┌─────┴────┐ ┌──────────┐ ┌───────────┐
/// NAR Stream──►│NAR Hasher├─►│Compressor├─►│File Hasher├─►File Stream
/// └──────────┘ └──────────┘ └─────┬─────┘
/// │
/// ├───────►File Hash
/// │
/// │
/// └───────►File Size
/// ```
struct CompressionStream {
stream: Box<dyn AsyncRead + Unpin + Send>,
nar_compute: Arc<OnceCell<(DigestOutput<Sha256>, usize)>>,
file_compute: Arc<OnceCell<(DigestOutput<Sha256>, usize)>>,
}
trait UploadPathNarInfoExt {
fn to_active_model(&self) -> object::ActiveModel;
}
@@ -811,73 +782,6 @@ impl ChunkData {
}
}
impl CompressionStream {
/// Creates a new compression stream.
fn new<R>(stream: R, compressor: CompressorFn<BufReader<HashReader<R, Sha256>>>) -> Self
where
R: AsyncRead + Unpin + Send + 'static,
{
// compute NAR hash and size
let (stream, nar_compute) = HashReader::new(stream, Sha256::new());
// compress NAR
let stream = compressor(BufReader::new(stream));
// compute file hash and size
let (stream, file_compute) = HashReader::new(stream, Sha256::new());
Self {
stream: Box::new(stream),
nar_compute,
file_compute,
}
}
/*
/// Creates a compression stream without compute the uncompressed hash/size.
///
/// This is useful if you already know the hash. `nar_hash_and_size` will
/// always return `None`.
fn new_without_nar_hash<R>(stream: R, compressor: CompressorFn<BufReader<R>>) -> Self
where
R: AsyncRead + Unpin + Send + 'static,
{
// compress NAR
let stream = compressor(BufReader::new(stream));
// compute file hash and size
let (stream, file_compute) = HashReader::new(stream, Sha256::new());
Self {
stream: Box::new(stream),
nar_compute: Arc::new(OnceCell::new()),
file_compute,
}
}
*/
/// Returns the stream of the compressed object.
fn stream(&mut self) -> &mut (impl AsyncRead + Unpin) {
&mut self.stream
}
/// Returns the NAR hash and size.
///
/// The hash is only finalized when the stream is fully read.
/// Otherwise, returns `None`.
fn nar_hash_and_size(&self) -> Option<&(DigestOutput<Sha256>, usize)> {
self.nar_compute.get()
}
/// Returns the file hash and size.
///
/// The hash is only finalized when the stream is fully read.
/// Otherwise, returns `None`.
fn file_hash_and_size(&self) -> Option<&(DigestOutput<Sha256>, usize)> {
self.file_compute.get()
}
}
impl UploadPathNarInfoExt for UploadPathNarInfo {
fn to_active_model(&self) -> object::ActiveModel {
object::ActiveModel {
+81
View File
@@ -0,0 +1,81 @@
use std::sync::Arc;
use digest::Output as DigestOutput;
use sha2::{Digest, Sha256};
use tokio::io::{AsyncRead, BufReader};
use tokio::sync::OnceCell;
use attic::io::HashReader;
pub type CompressorFn<C> = Box<dyn FnOnce(C) -> Box<dyn AsyncRead + Unpin + Send> + Send>;
/// Applies compression to a stream, computing hashes along the way.
///
/// Our strategy is to stream directly onto a UUID-keyed file on the
/// storage backend, performing compression and computing the hashes
/// along the way. We delete the file if the hashes do not match.
///
/// ```text
/// ┌───────────────────────────────────►NAR Hash
/// │
/// │
/// ├───────────────────────────────────►NAR Size
/// │
/// ┌─────┴────┐ ┌──────────┐ ┌───────────┐
/// NAR Stream──►│NAR Hasher├─►│Compressor├─►│File Hasher├─►File Stream
/// └──────────┘ └──────────┘ └─────┬─────┘
/// │
/// ├───────►File Hash
/// │
/// │
/// └───────►File Size
/// ```
pub struct CompressionStream {
stream: Box<dyn AsyncRead + Unpin + Send>,
nar_compute: Arc<OnceCell<(DigestOutput<Sha256>, usize)>>,
file_compute: Arc<OnceCell<(DigestOutput<Sha256>, usize)>>,
}
impl CompressionStream {
/// Creates a new compression stream.
pub fn new<R>(stream: R, compressor: CompressorFn<BufReader<HashReader<R, Sha256>>>) -> Self
where
R: AsyncRead + Unpin + Send + 'static,
{
// compute NAR hash and size
let (stream, nar_compute) = HashReader::new(stream, Sha256::new());
// compress NAR
let stream = compressor(BufReader::new(stream));
// compute file hash and size
let (stream, file_compute) = HashReader::new(stream, Sha256::new());
Self {
stream: Box::new(stream),
nar_compute,
file_compute,
}
}
/// Returns the stream of the compressed object.
pub fn stream(&mut self) -> &mut (impl AsyncRead + Unpin) {
&mut self.stream
}
/// Returns the NAR hash and size.
///
/// The hash is only finalized when the stream is fully read.
/// Otherwise, returns `None`.
pub fn nar_hash_and_size(&self) -> Option<&(DigestOutput<Sha256>, usize)> {
self.nar_compute.get()
}
/// Returns the file hash and size.
///
/// The hash is only finalized when the stream is fully read.
/// Otherwise, returns `None`.
pub fn file_hash_and_size(&self) -> Option<&(DigestOutput<Sha256>, usize)> {
self.file_compute.get()
}
}
+1
View File
@@ -15,6 +15,7 @@
pub mod access;
mod api;
mod compression;
pub mod config;
pub mod database;
pub mod error;