Content dedup on both sides, stored checksums, spool-free zips, upload stats
ci / docker (push) Successful in 12s
ci / docker (push) Successful in 12s
- sha256+crc32 hashed during upload streaming; unique index per album - duplicate content returns the existing photo (race-safe via 23505) - client hashes locally (WebCrypto) and skips the transfer entirely for content the album already has - zip downloads stream S3->response directly using the stored crc32; pre-hash photos spool once and self-heal (crc via zip, sha via reprocess) - upload UI: overall progress bar, bytes, live speed, ETA
This commit is contained in:
+37
-4
@@ -62,7 +62,24 @@ pub async fn process_photo(state: &AppState, photo_id: Uuid) -> anyhow::Result<(
|
||||
.unwrap_or("bin")
|
||||
.to_lowercase();
|
||||
let src_path = dir.path().join(format!("original.{extension}"));
|
||||
download(state, &s3::original_key(photo_id, &photo.filename), &src_path).await?;
|
||||
let (sha256, crc32) =
|
||||
download(state, &s3::original_key(photo_id, &photo.filename), &src_path).await?;
|
||||
if photo.sha256.is_none() || photo.crc32.is_none() {
|
||||
// Best-effort backfill for pre-hash photos; a duplicate in the same
|
||||
// album trips the unique index, which is fine — skip silently.
|
||||
if let Err(e) = sqlx::query(
|
||||
"update photos set sha256 = coalesce(sha256, $2), crc32 = coalesce(crc32, $3)
|
||||
where id = $1",
|
||||
)
|
||||
.bind(photo_id)
|
||||
.bind(&sha256)
|
||||
.bind(crc32)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
{
|
||||
tracing::debug!("hash backfill skipped for {photo_id}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let meta = exif_metadata(&src_path).await?;
|
||||
|
||||
@@ -103,7 +120,11 @@ pub async fn process_photo(state: &AppState, photo_id: Uuid) -> anyhow::Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download(state: &AppState, key: &str, path: &Path) -> anyhow::Result<()> {
|
||||
/// Download the original, hashing along the way so legacy photos (uploaded
|
||||
/// before hashes existed) can be backfilled.
|
||||
async fn download(state: &AppState, key: &str, path: &Path) -> anyhow::Result<(String, i64)> {
|
||||
use sha2::Digest;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
let object = state
|
||||
.s3
|
||||
.get_object()
|
||||
@@ -114,8 +135,20 @@ async fn download(state: &AppState, key: &str, path: &Path) -> anyhow::Result<()
|
||||
.with_context(|| format!("fetching s3://{}/{key}", state.config.s3_bucket))?;
|
||||
let mut reader = object.body.into_async_read();
|
||||
let mut file = tokio::fs::File::create(path).await?;
|
||||
tokio::io::copy(&mut reader, &mut file).await?;
|
||||
Ok(())
|
||||
let mut sha = sha2::Sha256::new();
|
||||
let mut crc = crc32fast::Hasher::new();
|
||||
let mut buf = vec![0u8; 128 * 1024];
|
||||
loop {
|
||||
let n = reader.read(&mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
sha.update(&buf[..n]);
|
||||
crc.update(&buf[..n]);
|
||||
file.write_all(&buf[..n]).await?;
|
||||
}
|
||||
file.flush().await?;
|
||||
Ok((hex::encode(sha.finalize()), i64::from(crc.finalize())))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
Reference in New Issue
Block a user