Skip to main content

max / goingson

Upload attachments through the streaming blob path The old path read each attachment whole into memory before sealing it, and could not upload anything above the server's one-shot PUT ceiling. Peak memory is now one part regardless of attachment size. Dedup moves into the same call, ahead of any file read, so an attachment the server already holds costs one round trip and no disk I/O. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-21 15:52 UTC
Commit: 482b72ce704df028c42217eaead03236b5709ba2
Parent: 3a245f5
1 file changed, +11 insertions, -22 deletions
@@ -5,7 +5,7 @@ use std::path::Path;
5 5 use goingson_core::CoreError;
6 6 use sha2::{Digest, Sha256};
7 7 use sqlx::SqlitePool;
8 - use synckit_client::SyncKitClient;
8 + use synckit_client::{BlobUploadOutcome, SyncKitClient};
9 9 use tracing::{debug, info, warn};
10 10
11 11 use crate::commands::attachment::blob_path;
@@ -44,32 +44,21 @@ pub async fn upload_pending_blobs(
44 44 continue; // No local blob to upload
45 45 }
46 46
47 - // Request upload URL — server tells us if blob already exists
48 - let upload_resp = match client.blob_upload_url(hash, *size).await {
49 - Ok(r) => r,
50 - Err(e) => {
51 - warn!("Failed to get upload URL for blob {}: {}", short(hash), e);
47 + // Streams the file a part at a time rather than reading the whole
48 + // attachment into memory, and it is the only path the server accepts
49 + // above its one-shot PUT ceiling. The server's dedup check runs inside,
50 + // before any read, so an attachment it already holds costs one round
51 + // trip and no disk I/O.
52 + match client.blob_upload_streaming(hash, &path).await {
53 + Ok(BlobUploadOutcome::AlreadyPresent) => {
54 + debug!("Blob {} already on server, skipping", short(hash));
52 55 continue;
53 56 }
54 - };
55 -
56 - if upload_resp.already_exists {
57 - debug!("Blob {} already on server, skipping", short(hash));
58 - continue;
59 - }
60 -
61 - // Read and upload
62 - let data = match tokio::fs::read(&path).await {
63 - Ok(d) => d,
57 + Ok(BlobUploadOutcome::Uploaded) => {}
64 58 Err(e) => {
65 - warn!("Failed to read blob {}: {}", short(hash), e);
59 + warn!("Failed to upload blob {}: {}", short(hash), e);
66 60 continue;
67 61 }
68 - };
69 -
70 - if let Err(e) = client.blob_upload(hash, &upload_resp.upload_url, data).await {
71 - warn!("Failed to upload blob {}: {}", short(hash), e);
72 - continue;
73 62 }
74 63
75 64 // Confirm upload