coven_storage/cloud/
exact_upload.rs1use super::{BlobBody, CloudHomeError};
2use coven_protocol::objects::{ExactObjectRef, StorageError};
3use std::path::Path;
4
5pub(crate) fn accept_unchecked_create_response(
6 created_response_was_observed: bool,
7 object: &ExactObjectRef,
8) -> Result<(), CloudHomeError> {
9 if created_response_was_observed {
10 Ok(())
11 } else {
12 Err(CloudHomeError::AlreadyExists(
13 object.slot().logical_key().to_string(),
14 ))
15 }
16}
17
18pub(crate) async fn settle_exact_create<Verify, Verification>(
19 operation: Result<(), CloudHomeError>,
20 verify: Verify,
21) -> Result<super::ExactCreateOutcome, CloudHomeError>
22where
23 Verify: FnOnce(bool) -> Verification,
24 Verification: std::future::Future<Output = Result<(), CloudHomeError>>,
25{
26 match operation {
27 Ok(()) => {
28 verify(true).await?;
29 Ok(super::ExactCreateOutcome::Created)
30 }
31 Err(CloudHomeError::AlreadyExists(_)) => {
32 verify(false).await?;
33 Ok(super::ExactCreateOutcome::AlreadyPresent)
34 }
35 Err(operation) => match verify(false).await {
36 Ok(()) => Ok(super::ExactCreateOutcome::AlreadyPresent),
37 Err(CloudHomeError::NotFound(_)) => Err(operation),
38 Err(collision @ CloudHomeError::SlotCollision(_)) => Err(collision),
39 Err(settlement) => Err(CloudHomeError::UnresolvedOutcome {
40 operation: Box::new(operation),
41 settlement: Box::new(settlement),
42 }),
43 },
44 }
45}
46
47#[derive(Clone, Copy)]
51pub struct ExactUpload<'source> {
52 object: &'source ExactObjectRef,
53 source: ExactUploadSource<'source>,
54}
55
56#[derive(Clone, Copy)]
57pub enum ExactUploadSource<'source> {
58 Bytes(&'source [u8]),
59 File(&'source Path),
60}
61
62impl<'source> ExactUpload<'source> {
63 pub fn from_bytes(
64 object: &'source ExactObjectRef,
65 bytes: &'source [u8],
66 ) -> Result<Self, StorageError> {
67 object.verify(bytes)?;
68 Ok(Self {
69 object,
70 source: ExactUploadSource::Bytes(bytes),
71 })
72 }
73
74 pub async fn from_file(
75 object: &'source ExactObjectRef,
76 path: &'source Path,
77 ) -> Result<Self, StorageError> {
78 let (size, digest) = coven_foundation::local_file::file_facts(path)
79 .await
80 .map_err(StorageError::LocalFilesystem)?;
81 object.verify_stored_facts(
82 path,
83 size,
84 coven_protocol::store_commit::ObjectHash::from_digest(digest),
85 )?;
86 Ok(Self {
87 object,
88 source: ExactUploadSource::File(path),
89 })
90 }
91
92 pub fn object(&self) -> &ExactObjectRef {
93 self.object
94 }
95
96 pub fn source(&self) -> ExactUploadSource<'source> {
97 self.source
98 }
99
100 pub fn verify_stored_bytes(&self, bytes: &[u8]) -> Result<(), CloudHomeError> {
101 self.object.verify(bytes).map_err(|_| {
102 CloudHomeError::SlotCollision(self.object.slot().logical_key().to_string())
103 })
104 }
105
106 pub async fn body(&self) -> Result<BlobBody, CloudHomeError> {
107 match self.source {
108 ExactUploadSource::Bytes(bytes) => Ok(BlobBody::from_bytes(bytes.to_vec())),
109 ExactUploadSource::File(path) => BlobBody::from_file(path)
110 .await
111 .map_err(CloudHomeError::Local),
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use coven_protocol::objects::ObjectSlot;
120 use coven_protocol::store_commit::ObjectHash;
121
122 fn object() -> ExactObjectRef {
123 ExactObjectRef::new(
124 ObjectSlot::logical("unchecked/object".to_string()).expect("logical slot"),
125 5,
126 ObjectHash::digest(b"bytes"),
127 )
128 }
129
130 #[test]
131 fn unchecked_accepts_only_a_witnessed_successful_create() {
132 let object = object();
133
134 assert!(accept_unchecked_create_response(true, &object).is_ok());
135 assert!(matches!(
136 accept_unchecked_create_response(false, &object),
137 Err(CloudHomeError::AlreadyExists(key)) if key == "unchecked/object"
138 ));
139 }
140
141 #[tokio::test]
142 async fn unchecked_does_not_turn_occupied_or_ambiguous_results_into_success() {
143 let object = object();
144 let occupied = settle_exact_create(
145 Err(CloudHomeError::AlreadyExists(
146 "unchecked/object".to_string(),
147 )),
148 |_| async { accept_unchecked_create_response(false, &object) },
149 )
150 .await;
151 assert!(matches!(
152 occupied,
153 Err(CloudHomeError::AlreadyExists(key)) if key == "unchecked/object"
154 ));
155
156 let ambiguous = settle_exact_create(
157 Err(CloudHomeError::Transport("lost response".to_string())),
158 |_| async { accept_unchecked_create_response(false, &object) },
159 )
160 .await;
161 assert!(matches!(
162 ambiguous,
163 Err(CloudHomeError::UnresolvedOutcome { operation, settlement })
164 if matches!(*operation, CloudHomeError::Transport(_))
165 && matches!(*settlement, CloudHomeError::AlreadyExists(_))
166 ));
167 }
168}