1use {
10 bytes::Bytes,
11 futures_util::{StreamExt as _, TryStreamExt as _},
12 object_store::{
13 GetOptions, GetRange, MultipartUpload, ObjectMeta, ObjectStoreExt as _, PutOptions,
14 PutPayload, PutResult, UploadPart, WriteMultipart, path::Path,
15 },
16 sha2::{Digest as _, Sha256},
17 std::sync::Arc,
18 tokio::io::AsyncReadExt as _,
19};
20
21#[cfg(test)]
22mod tests;
23
24use {
25 crate::{
26 error::ObjectStoreError,
27 ops::{ObjectOps, ObjectStream, PutOutcome, PutRequest},
28 read::read_capped,
29 },
30 std::{
31 future,
32 io::{self, ErrorKind},
33 },
34 tokio::{fs, fs::File, runtime::Handle},
35};
36
37const MULTIPART_MAX_CONCURRENCY: usize = 8;
39
40#[derive(Clone)]
46pub struct ObjectStoreClient {
47 inner: Arc<dyn object_store::ObjectStore>,
48}
49
50#[derive(Debug)]
51struct AbortOnDrop {
52 upload: Option<Box<dyn MultipartUpload>>,
53 key: Path,
54}
55
56impl AbortOnDrop {
57 fn new(upload: Box<dyn MultipartUpload>, key: &Path) -> Self {
58 Self {
59 upload: Some(upload),
60 key: key.clone(),
61 }
62 }
63
64 fn upload(&mut self) -> object_store::Result<&mut Box<dyn MultipartUpload>> {
66 self.upload
67 .as_mut()
68 .ok_or_else(|| object_store::Error::Generic {
69 store: "multipart upload",
70 source: "the upload already completed or aborted".into(),
71 })
72 }
73}
74
75#[async_trait::async_trait]
76impl MultipartUpload for AbortOnDrop {
77 fn put_part(&mut self, data: PutPayload) -> UploadPart {
78 match self.upload() {
79 Ok(upload) => upload.put_part(data),
80 Err(error) => Box::pin(future::ready(Err(error))),
81 }
82 }
83
84 async fn complete(&mut self) -> object_store::Result<PutResult> {
85 let result = self.upload()?.complete().await;
86 if result.is_ok() {
87 self.upload.take();
88 }
89 result
90 }
91
92 async fn abort(&mut self) -> object_store::Result<()> {
93 let Some(mut upload) = self.upload.take() else {
94 return Ok(());
95 };
96 let key = self.key.clone();
97 tokio::spawn(async move {
98 let result = upload.abort().await;
99 if let Err(error) = &result {
100 tracing::warn!(%key, %error, "failed to abort multipart upload");
101 }
102 result
103 })
104 .await
105 .map_err(|error| object_store::Error::Generic {
106 store: "multipart upload",
107 source: Box::new(error),
108 })?
109 }
110}
111
112impl Drop for AbortOnDrop {
113 fn drop(&mut self) {
114 let Some(mut upload) = self.upload.take() else {
115 return;
116 };
117 let key = self.key.clone();
118 let Ok(runtime) = Handle::try_current() else {
119 tracing::warn!(%key, "cannot abort multipart upload outside a Tokio runtime");
120 return;
121 };
122 runtime.spawn(async move {
123 if let Err(error) = upload.abort().await {
124 tracing::warn!(%key, %error, "failed to abort multipart upload");
125 }
126 });
127 }
128}
129
130impl ObjectStoreClient {
131 #[must_use]
133 pub fn new(inner: Arc<dyn object_store::ObjectStore>) -> Self { Self { inner } }
134}
135
136#[krabka_macros::object_ops]
137#[async_trait::async_trait]
138impl ObjectOps for ObjectStoreClient {
139 put_from_path!({
140 if chunk_size == 0 {
141 return Err(io::Error::new(
142 ErrorKind::InvalidInput,
143 "multipart chunk size must be greater than zero",
144 )
145 .into());
146 }
147
148 let len = fs::metadata(src).await?.len();
149 if len < threshold {
150 return self.put(key, fs::read(src).await?.into(), req).await;
151 }
152 if matches!(req.mode, object_store::PutMode::Create) {
157 self.inner
158 .put_opts(key, PutPayload::from_bytes(Bytes::new()), PutOptions {
159 mode: object_store::PutMode::Create,
160 ..PutOptions::default()
161 })
162 .await?;
163 }
164 let upload = Box::new(AbortOnDrop::new(self.inner.put_multipart(key).await?, key));
165 let mut writer = WriteMultipart::new_with_chunk_size(upload, chunk_size);
166 let mut file = File::open(src).await?;
167 let mut buf = vec![0u8; chunk_size];
168 let mut hasher = req.digest.then(Sha256::new);
169 let mut size_bytes = 0u64;
170 loop {
171 writer.wait_for_capacity(MULTIPART_MAX_CONCURRENCY).await?;
172 let n = file.read(&mut buf).await?;
173 if n == 0 {
174 break;
175 }
176 let Some(read) = buf.get(..n) else {
177 return Err(io::Error::new(
178 ErrorKind::InvalidData,
179 "a read reported more bytes than its buffer holds",
180 )
181 .into());
182 };
183 if let Some(hasher) = hasher.as_mut() {
184 hasher.update(read);
185 }
186 size_bytes += n as u64;
187 writer.write(read);
188 }
189 let result = writer.finish().await?;
190 let expected_sha256 = hasher.map(|hasher| hasher.finalize().into());
191 let mut e_tag = result.e_tag;
192 let mut version_id = result.version;
193 if matches!(req.mode, object_store::PutMode::Create) {
194 let meta = self.inner.head(key).await?;
195 if version_id.is_some() && meta.version != version_id {
196 return Err(ObjectStoreError::Precondition {
197 key: key.clone(),
198 reason: format!(
199 "completed multipart version {:?} does not match HEAD version {:?}",
200 version_id, meta.version
201 ),
202 });
203 }
204 version_id = meta.version;
205 if let Some(expected) = expected_sha256 {
206 let options = GetOptions {
207 version: version_id.clone(),
208 ..GetOptions::default()
209 };
210 let mut stored = self.inner.get_opts(key, options).await?.into_stream();
211 let mut hasher = Sha256::new();
212 while let Some(chunk) = stored.try_next().await? {
213 hasher.update(&chunk);
214 }
215 let actual: [u8; 32] = hasher.finalize().into();
216 if expected != actual {
217 return Err(ObjectStoreError::Precondition {
218 key: key.clone(),
219 reason: "completed multipart object does not match the uploaded SHA-256"
220 .into(),
221 });
222 }
223 }
224 e_tag = meta.e_tag;
225 }
226 Ok(PutOutcome {
227 size_bytes,
228 sha256: expected_sha256,
229 e_tag,
230 version_id,
231 create_precondition: false,
232 })
233 });
234
235 async fn put(
236 &self,
237 key: &Path,
238 bytes: Bytes,
239 req: PutRequest,
240 ) -> Result<PutOutcome, ObjectStoreError> {
241 let create_precondition = matches!(req.mode, object_store::PutMode::Create);
242 let size_bytes = bytes.len() as u64;
243 let sha256 = req.digest.then(|| Sha256::digest(&bytes).into());
244 let result = self
245 .inner
246 .put_opts(key, PutPayload::from_bytes(bytes), PutOptions {
247 mode: req.mode,
248 ..Default::default()
249 })
250 .await?;
251 Ok(PutOutcome {
252 size_bytes,
253 sha256,
254 e_tag: result.e_tag,
255 version_id: result.version,
256 create_precondition,
257 })
258 }
259
260 async fn get(&self, key: &Path) -> Result<Bytes, ObjectStoreError> {
261 Ok(self.inner.get(key).await?.bytes().await?)
262 }
263
264 async fn get_capped(&self, key: &Path, max_bytes: u64) -> Result<Bytes, ObjectStoreError> {
265 read_capped(&self.inner, key, max_bytes).await
266 }
267
268 async fn get_range(&self, key: &Path, range: GetRange) -> Result<Bytes, ObjectStoreError> {
269 let opts = GetOptions {
270 range: Some(range),
271 ..Default::default()
272 };
273 Ok(self.inner.get_opts(key, opts).await?.bytes().await?)
274 }
275
276 async fn head(&self, key: &Path) -> Result<ObjectMeta, ObjectStoreError> {
277 Ok(self.inner.head(key).await?)
278 }
279
280 async fn list(&self, prefix: Option<Path>) -> Result<Vec<ObjectMeta>, ObjectStoreError> {
281 self.list_stream(prefix).try_collect::<Vec<_>>().await
282 }
283
284 fn list_stream(&self, prefix: Option<Path>) -> ObjectStream {
285 self.inner
289 .list(prefix.as_ref())
290 .map(|result| result.map_err(ObjectStoreError::from))
291 .boxed()
292 }
293
294 async fn delete(&self, key: &Path) -> Result<(), ObjectStoreError> {
295 self.inner.delete(key).await?;
296 Ok(())
297 }
298}