synckit-client: document the already_exists consumer contract
BlobUploadUrlResponse.already_exists is a server-asserted bool that gates
whether the client sends bytes it holds locally, and nothing said what a
consumer may conclude from it. A server answering true for content it does
not have turns the upload into a silent no-op, so the blob is simply absent
later. That is a liveness failure, not an integrity one: download_one
re-hashes what it receives and rejects a mismatch with IntegrityFailed, so a
lying server can withhold a blob but cannot substitute one. The field doc now
says that, and says to read a skipped upload as unconfirmed rather than as
proof the server holds the content.
Also notes at the skip site that on_uploaded is deliberately not fired there:
nothing was uploaded and the paired confirm never happened. Inert today, since
the trait default is a no-op and TestPolicy is the only implementor, but a
policy that clears a presence flag in that hook would re-offer the blob every
pass. The SyncStore ports are what add real policies, and audiofiles'
cloud_only is the shape that would hit it.
Docs only, no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 files changed,
+27 insertions,
-1 deletion
| 28 |
28 |
|
/// Request a presigned upload URL for a blob.
|
| 29 |
29 |
|
/// Returns (upload_url, already_exists). If `already_exists` is true,
|
| 30 |
30 |
|
/// the blob is already on the server and no upload is needed.
|
|
31 |
+ |
///
|
|
32 |
+ |
/// `already_exists` is the server's claim, not a verified fact. See
|
|
33 |
+ |
/// [`BlobUploadUrlResponse::already_exists`] for what a false `true` costs
|
|
34 |
+ |
/// (a withheld blob, never a substituted one).
|
| 31 |
35 |
|
#[instrument(skip(self))]
|
| 32 |
36 |
|
pub async fn blob_upload_url(
|
| 33 |
37 |
|
&self,
|
| 183 |
183 |
|
}
|
| 184 |
184 |
|
let target = client.upload_url(&blob.hash, blob.size).await?;
|
| 185 |
185 |
|
if target.already_exists {
|
| 186 |
|
- |
return Ok(true); // content-addressed dedup: already on the server
|
|
186 |
+ |
// Content-addressed dedup: the server says it holds this hash, so the
|
|
187 |
+ |
// bytes stay home. Two consequences worth knowing, both bounded:
|
|
188 |
+ |
// - The claim is unverified. A server that answers `true` wrongly
|
|
189 |
+ |
// withholds the blob from other devices; it cannot substitute one,
|
|
190 |
+ |
// because `download_one` re-hashes before committing the file.
|
|
191 |
+ |
// - `on_uploaded` is deliberately not fired: nothing was uploaded,
|
|
192 |
+ |
// and the confirm call that pairs with it never happened. A policy
|
|
193 |
+ |
// using that hook to clear a presence flag will keep re-offering
|
|
194 |
+ |
// the blob on every pass, costing one `upload_url` round trip each
|
|
195 |
+ |
// time. Harmless for the no-op default (the only implementors
|
|
196 |
+ |
// today), but a policy with an explicit flag needs its own way to
|
|
197 |
+ |
// settle the dedup case.
|
|
198 |
+ |
return Ok(true);
|
| 187 |
199 |
|
}
|
| 188 |
200 |
|
let data = tokio::fs::read(&path).await.map_err(|e| io_err("read", e))?;
|
| 189 |
201 |
|
client.upload(&blob.hash, &target.upload_url, data).await?;
|
| 511 |
511 |
|
/// Presigned S3 PUT URL. Empty string when `already_exists` is true.
|
| 512 |
512 |
|
pub upload_url: String,
|
| 513 |
513 |
|
/// True if the server already has a blob with this hash (skip upload).
|
|
514 |
+ |
///
|
|
515 |
+ |
/// Consumer contract: this is a *server-asserted* bool, and a consumer that
|
|
516 |
+ |
/// acts on it skips sending bytes it holds locally. A server that answers
|
|
517 |
+ |
/// `true` for content it does not actually have makes the upload a silent
|
|
518 |
+ |
/// no-op, so the blob is simply absent later. That is a liveness failure,
|
|
519 |
+ |
/// not an integrity one: nothing accepts unverified bytes as a result. The
|
|
520 |
+ |
/// download path re-hashes what it receives and rejects any mismatch
|
|
521 |
+ |
/// (`SyncKitError::IntegrityFailed`), so a lying server can withhold a blob
|
|
522 |
+ |
/// but cannot substitute one. Treat a skipped upload as unconfirmed rather
|
|
523 |
+ |
/// than as proof the server holds the content.
|
| 514 |
524 |
|
pub already_exists: bool,
|
| 515 |
525 |
|
}
|
| 516 |
526 |
|
|