krabka_object_store/ops.rs
1//! The shared object-store operation surface.
2//!
3//! This module holds an async, mockable [`ObjectOps`] trait and its single
4//! concrete implementation [`ObjectStoreClient`] over `object_store`. Consumers
5//! route their put, get, delete, and multipart calls through it, so the
6//! operation logic lives in one place. That logic includes the
7//! multipart-threshold branch and the `object_store::Error` to
8//! [`ObjectStoreError`] mapping.
9
10/// Precondition for a write, re-exported so callers need not depend on
11/// `object_store` directly.
12pub use object_store::PutMode;
13use {
14 bytes::Bytes,
15 futures_util::stream::BoxStream,
16 object_store::{GetRange, ObjectMeta, path::Path},
17};
18
19mod client;
20
21#[cfg(test)]
22mod tests;
23
24pub use self::client::ObjectStoreClient;
25use crate::error::ObjectStoreError;
26
27/// A listing delivered one object at a time.
28///
29/// The stream is `'static` because the backend's own listing is: it owns the
30/// paging state rather than borrowing the store, so a caller can hold the
31/// stream past the borrow it was created from.
32pub type ObjectStream = BoxStream<'static, Result<ObjectMeta, ObjectStoreError>>;
33
34/// How one put should behave.
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
36pub struct PutRequest {
37 /// Precondition for the write. Defaults to `PutMode::Overwrite`.
38 pub mode: PutMode,
39 /// Whether to compute a SHA-256 digest of the payload during upload.
40 /// Defaults to `false`; only the WORM archive path needs it, and hashing
41 /// every tiered byte is not free.
42 pub digest: bool,
43}
44
45/// What a put produced.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct PutOutcome {
48 /// Bytes written.
49 pub size_bytes: u64,
50 /// Whole-object SHA-256, present only when `PutRequest::digest` was set.
51 pub sha256: Option<[u8; 32]>,
52 /// Backend entity tag, when the backend returned one.
53 pub e_tag: Option<String>,
54 /// Version id on a versioned bucket, when the backend returned one.
55 pub version_id: Option<String>,
56 /// Whether the backend applied an atomic create precondition to this put.
57 /// Multipart uploads return `false`: an atomic empty-object reservation
58 /// serializes creators, while bucket retention protects the completed
59 /// version.
60 pub create_precondition: bool,
61}
62
63/// Async object-store operations. The trait is `Send + Sync`, so tasks can
64/// share it.
65///
66/// The trait stays dyn-safe and `#[automock]`-able. It expresses multipart
67/// upload as [`ObjectOps::put_from_path`] over a filesystem path, not over a
68/// generic reader. The trait thus mocks cleanly for mutation-testable IO
69/// decision logic.
70#[krabka_macros::object_ops]
71#[cfg_attr(test, mockall::automock)]
72#[async_trait::async_trait]
73pub trait ObjectOps: Send + Sync {
74 /// Single-PUT an in-memory payload under the preconditions in `req`.
75 async fn put(
76 &self,
77 key: &Path,
78 bytes: Bytes,
79 req: PutRequest,
80 ) -> Result<PutOutcome, ObjectStoreError>;
81
82 /// Upload a local file. The method uses single-PUT below `threshold`
83 /// bytes, and streaming multipart in `chunk_size` parts at or above
84 /// `threshold`.
85 put_from_path!();
86
87 /// Fetch a whole object.
88 async fn get(&self, key: &Path) -> Result<Bytes, ObjectStoreError>;
89
90 /// Fetch a whole object with a byte limit enforced before buffering its body.
91 ///
92 /// Bind the GET to the HEAD version or etag when the backend provides one,
93 /// and reject an oversized response while streaming even without either.
94 async fn get_capped(&self, key: &Path, max_bytes: u64) -> Result<Bytes, ObjectStoreError>;
95
96 /// Fetch a byte range of an object.
97 async fn get_range(&self, key: &Path, range: GetRange) -> Result<Bytes, ObjectStoreError>;
98
99 /// Fetch object metadata, such as the size and the etag.
100 async fn head(&self, key: &Path) -> Result<ObjectMeta, ObjectStoreError>;
101
102 /// List objects under an optional prefix, collecting the whole listing.
103 ///
104 /// A prefix holding millions of keys costs that much memory here. Prefer
105 /// [`ObjectOps::list_stream`] whenever the caller can consume the listing
106 /// as it arrives.
107 async fn list(&self, prefix: Option<Path>) -> Result<Vec<ObjectMeta>, ObjectStoreError>;
108
109 /// List objects under an optional prefix as a stream, one object at a
110 /// time, so a caller never holds the whole listing at once.
111 fn list_stream(&self, prefix: Option<Path>) -> ObjectStream;
112
113 /// Delete an object.
114 async fn delete(&self, key: &Path) -> Result<(), ObjectStoreError>;
115}