1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//! StorageClient capability trait and types for blob / object stores.
//!
//! A `StorageClientPlugin` is the runtime side of an object store: get a
//! key, put a key, delete a key, list keys under a prefix, and mint a
//! presigned URL. Backends range from S3 / R2 / MinIO / B2 to local
//! filesystem and in-memory stores for tests. The trait is narrow on
//! purpose — anything that looks like "named blob in a flat keyspace"
//! fits; anything richer (consistency tiers, object versioning beyond a
//! single `etag`, multipart upload) stays backend-specific.
//!
//! Design notes:
//!
//! - Payloads are `Vec<u8>`, not `Bytes`. Matches the rest of the crate,
//! keeps the WASM ABI flat, and lets callers wrap in `Bytes` themselves
//! when they want the cheap-clone semantics. Bytes-in-the-trait would
//! pull `bytes` into every WASM guest, which is not worth the ergonomic
//! win for a leaf crate.
//!
//! - `list` is paginated via [`StorageClientPlugin::list_page`], not a
//! `BoxStream`. Reasons: (1) keeps the trait synchronous, matching every
//! other E1/E2 capability; (2) avoids dragging `futures` into a leaf
//! crate that otherwise only depends on `serde` + `serde_json`; (3)
//! mirrors how every real object store exposes list (S3 / R2 / GCS /
//! Azure all hand out a continuation token). Callers who want a
//! stream-shaped surface can build one on top in two lines.
//!
//! - Errors are a flat enum with four variants — `NotFound`,
//! `AccessDenied`, `QuotaExceeded`, `Backend(String)` — because callers
//! branch on the classification. A route that returns 404 on `NotFound`,
//! 403 on `AccessDenied`, and 500 otherwise is a common pattern, and
//! forcing callers to parse a stringly-typed error to implement it is a
//! papercut we can avoid cheaply. Matches the shape of
//! [`crate::session::SessionError`].
//!
//! - The trait is sync-only, matching the rest of the plugin API. Native
//! backends that need async I/O (aws-sdk-s3, etc.) drive their own
//! runtime inside each method via `tokio::runtime::Handle::block_on`
//! or a dedicated `Runtime`. WASM guests go through the host-function
//! bridge.
/// Errors a storage backend can return.
///
/// Kept as a flat enum so the shape is stable across backends. Callers
/// branch on the variant to map to HTTP status codes (e.g. 404 / 403 /
/// 413 / 500) without having to parse a message string.
/// A single object as observed via [`StorageClientPlugin::list_page`].
///
/// `etag` is the backend's content-addressable identifier — opaque to
/// callers, but stable for a given (key, content) pair. Useful for cache
/// validators and idempotent writes. `size` is the object size in bytes.
/// `last_modified_ms` is the unix millisecond of the backend's last-write
/// timestamp, or `0` if the backend does not expose one.
/// Options for [`StorageClientPlugin::put`].
///
/// All fields default to empty / sensible, so callers can pass
/// `PutOpts::default()` for the common case of "upload these bytes".
/// Operation a presigned URL is minted for.
///
/// Deliberately narrow — the two operations every backend supports. We
/// can add `Delete` or `Head` later if a concrete caller needs one.
/// A plugin that owns a blob / object store.
///
/// Implementations are expected to be thread-safe and to hold any
/// connection-pool or client state internally behind `&self`. All
/// methods are synchronous — backends that need async I/O drive their
/// own runtime (see module docs).
///
/// Concurrency: callers may invoke any method from multiple threads
/// concurrently. `put` is last-write-wins unless the backend supports
/// conditional writes (not yet exposed in this trait).
/// Fuel budgets for WASM storage plugin calls.
///
/// Matches the shape in [`crate::types::fuel`]. Object I/O is
/// dominated by network cost from the host's perspective, so per-call
/// fuel only covers the plugin-side marshalling; the real rate
/// limiting happens in the host's outbound HTTP budget.