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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
use candid::{CandidType, Principal};
use serde::{Deserialize, Serialize};
use serde_bytes::{ByteArray, ByteBuf};
use std::{
collections::{BTreeMap, BTreeSet},
ops::Range,
};
pub const CHUNK_SIZE: u64 = 256 * 1024;
pub const MAX_PARTS: u64 = 1024;
// https://internetcomputer.org/docs/current/developer-docs/smart-contracts/maintain/resource-limits
pub const MAX_PAYLOAD_SIZE: u64 = 2000 * 1024;
// https://github.com/apache/arrow-rs/blob/main/object_store/src/lib.rs
#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct StateInfo {
pub name: String,
pub managers: BTreeSet<Principal>,
pub auditors: BTreeSet<Principal>,
pub governance_canister: Option<Principal>,
pub objects: u64,
pub next_etag: u64,
}
#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub enum PutMode {
/// Perform an atomic write operation, overwriting any object present at the provided path
#[default]
Overwrite,
/// Perform an atomic write operation, returning [`Error::AlreadyExists`] if an
/// object already exists at the provided path
Create,
/// Perform an atomic write operation if the current version of the object matches the
/// provided [`UpdateVersion`], returning [`Error::Precondition`] otherwise
Update(UpdateVersion),
}
#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct UpdateVersion {
/// The unique identifier for the newly created object
///
/// <https://datatracker.ietf.org/doc/html/rfc9110#name-etag>
pub e_tag: Option<String>,
/// A version indicator for the newly created object
pub version: Option<String>,
}
pub type PutResult = UpdateVersion;
#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum Attribute {
ContentDisposition,
ContentEncoding,
ContentLanguage,
ContentType,
CacheControl,
Metadata(String),
}
#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
pub struct PutOptions {
/// Configure the [`PutMode`] for this operation
pub mode: PutMode,
/// Provide a [`TagSet`] for this object
///
/// Implementations that don't support object tagging should ignore this
pub tags: String,
/// Provide a set of [`Attributes`]
///
/// Implementations that don't support an attribute should return an error
pub attributes: BTreeMap<Attribute, String>,
/// A nonce with AES256-GCM encryption
pub aes_nonce: Option<ByteArray<12>>,
/// A set of tags with AES256-GCM encryption
/// Each part of the object has its own tag
pub aes_tags: Option<Vec<ByteArray<16>>>,
}
#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
pub struct PutMultipartOptions {
/// Provide a [`TagSet`] for this object
///
/// Implementations that don't support object tagging should ignore this
pub tags: String,
/// Provide a set of [`Attributes`]
///
/// Implementations that don't support an attribute should return an error
pub attributes: BTreeMap<Attribute, String>,
/// A nonce with AES256-GCM encryption
pub aes_nonce: Option<ByteArray<12>>,
/// A set of tags with AES256-GCM encryption
/// Each part of the object has its own tag
pub aes_tags: Option<Vec<ByteArray<16>>>,
}
pub type PutMultipartOpts = PutMultipartOptions;
#[derive(CandidType, Default, Clone, Debug, Deserialize, Serialize)]
pub struct GetOptions {
/// Request will succeed if the `ObjectMeta::e_tag` matches
/// otherwise returning [`Error::Precondition`]
///
/// See <https://datatracker.ietf.org/doc/html/rfc9110#name-if-match>
///
/// Examples:
///
/// ```text
/// If-Match: "xyzzy"
/// If-Match: "xyzzy", "r2d2xxxx", "c3piozzzz"
/// If-Match: *
/// ```
pub if_match: Option<String>,
/// Request will succeed if the `ObjectMeta::e_tag` does not match
/// otherwise returning [`Error::NotModified`]
///
/// See <https://datatracker.ietf.org/doc/html/rfc9110#section-13.1.2>
///
/// Examples:
///
/// ```text
/// If-None-Match: "xyzzy"
/// If-None-Match: "xyzzy", "r2d2xxxx", "c3piozzzz"
/// If-None-Match: *
/// ```
pub if_none_match: Option<String>,
/// Request will succeed if the object has been modified since
///
/// <https://datatracker.ietf.org/doc/html/rfc9110#section-13.1.3>
pub if_modified_since: Option<u64>,
/// Request will succeed if the object has not been modified since
/// otherwise returning [`Error::Precondition`]
///
/// Some stores, such as S3, will only return `NotModified` for exact
/// timestamp matches, instead of for any timestamp greater than or equal.
///
/// <https://datatracker.ietf.org/doc/html/rfc9110#section-13.1.4>
pub if_unmodified_since: Option<u64>,
/// Request transfer of only the specified range of bytes
/// otherwise returning [`Error::NotModified`]
///
/// <https://datatracker.ietf.org/doc/html/rfc9110#name-range>
pub range: Option<GetRange>,
/// Request a particular object version
pub version: Option<String>,
/// Request transfer of no content
///
/// <https://datatracker.ietf.org/doc/html/rfc9110#name-head>
pub head: bool,
}
impl GetOptions {
/// Returns an error if the modification conditions on this request are not satisfied
///
/// <https://datatracker.ietf.org/doc/html/rfc7232#section-6>
pub fn check_preconditions(&self, meta: &ObjectMeta) -> Result<()> {
// The use of the invalid etag "*" means no ETag is equivalent to never matching
let etag = meta.e_tag.as_deref().unwrap_or("*");
let last_modified = meta.last_modified;
if let Some(m) = &self.if_match {
if m != "*" && m.split(',').map(str::trim).all(|x| x != etag) {
return Err(Error::Precondition {
path: meta.location.to_string(),
error: format!("{etag} does not match {m}"),
});
}
} else if let Some(date) = self.if_unmodified_since {
if last_modified > date {
return Err(Error::Precondition {
path: meta.location.to_string(),
error: format!("{date} < {last_modified}"),
});
}
}
if let Some(m) = &self.if_none_match {
if m == "*" || m.split(',').map(str::trim).any(|x| x == etag) {
return Err(Error::NotModified {
path: meta.location.to_string(),
error: format!("{etag} matches {m}"),
});
}
} else if let Some(date) = self.if_modified_since {
if last_modified <= date {
return Err(Error::NotModified {
path: meta.location.to_string(),
error: format!("{date} >= {last_modified}"),
});
}
}
Ok(())
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
pub enum GetRange {
/// Request a specific range of bytes
///
/// If the given range is zero-length or starts after the end of the object,
/// an error will be returned. Additionally, if the range ends after the end
/// of the object, the entire remainder of the object will be returned.
/// Otherwise, the exact requested range will be returned.
Bounded(u64, u64),
/// Request all bytes starting from a given byte offset
Offset(u64),
/// Request up to the last n bytes
Suffix(u64),
}
impl GetRange {
/// Convert to a [`Range`] if valid.
pub fn into_range(self, len: u64) -> Result<Range<u64>, String> {
match self {
Self::Bounded(start, end) => {
if start >= end {
return Err(format!(
"wanted range starting at {start} and ending at {end}, but start >= end"
));
}
if start >= len {
Err(format!(
"wanted range starting at {start}, but object was only {len} bytes long"
))
} else if end > len {
Ok(start..len)
} else {
Ok(start..end)
}
}
Self::Offset(start) => {
if start >= len {
Err(format!(
"wanted range starting at {start}, but object was only {len} bytes long"
))
} else {
Ok(start..len)
}
}
Self::Suffix(n) => Ok(len.saturating_sub(n)..len),
}
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
pub struct GetResult {
pub payload: ByteBuf,
pub meta: ObjectMeta,
pub range: (u64, u64),
pub attributes: BTreeMap<Attribute, String>,
}
#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
pub struct ObjectMeta {
/// The full path to the object
pub location: String,
/// The last modified time
pub last_modified: u64,
/// The size in bytes of the object
pub size: u64,
/// The unique identifier for the object
///
/// <https://datatracker.ietf.org/doc/html/rfc9110#name-etag>
pub e_tag: Option<String>,
/// A version indicator for this object
pub version: Option<String>,
/// A nonce with AES256-GCM encryption
pub aes_nonce: Option<ByteArray<12>>,
/// A set of tags with AES256-GCM encryption
/// Each part of the object has its own tag
pub aes_tags: Option<Vec<ByteArray<16>>>,
}
#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
pub struct ListResult {
/// Prefixes that are common (like directories)
pub common_prefixes: Vec<String>,
/// Object metadata for the listing
pub objects: Vec<ObjectMeta>,
}
pub type MultipartId = String;
#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
pub struct PartId {
/// Id of this part
pub content_id: String,
}
#[derive(CandidType, Debug, Deserialize, Serialize, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// A fallback error type when no variant matches
#[error("Generic error: {:?}", error)]
Generic {
/// The wrapped error
error: String,
},
/// Error when the object is not found at given location
#[error("Object at location {:?} not found", path)]
NotFound {
/// The path to file
path: String,
},
/// Error for invalid path
#[error("Encountered object with invalid path: {:?}", path)]
InvalidPath {
/// The wrapped error
path: String,
},
/// Error when the attempted operation is not supported
#[error("Operation not supported: {:?}", error)]
NotSupported {
/// The wrapped error
error: String,
},
/// Error when the object already exists
#[error("Object at location {:?} already exists", path)]
AlreadyExists {
/// The path to the
path: String,
},
/// Error when the required conditions failed for the operation
#[error("Request precondition failure for path {:?}: {:?}", path, error)]
Precondition {
/// The path to the file
path: String,
/// The wrapped error
error: String,
},
/// Error when the object at the location isn't modified
#[error("Object at location {:?} not modified: {:?}", path, error)]
NotModified {
/// The path to the file
path: String,
/// The wrapped error
error: String,
},
/// Error when an operation is not implemented
/// Error when an operation is not implemented
#[error("Operation {operation} not yet implemented by {implementer}.")]
NotImplemented {
/// What isn't implemented. Should include at least the method
/// name that was called; could also include other relevant
/// subcontexts.
operation: String,
/// Which driver this is that hasn't implemented this operation,
/// to aid debugging in contexts that may be using multiple implementations.
implementer: String,
},
/// Error when the used credentials don't have enough permission
/// to perform the requested operation
#[error(
"The operation lacked the necessary privileges to complete for path {:?}: {:?}",
path,
error
)]
PermissionDenied {
/// The path to the file
path: String,
/// The wrapped error
error: String,
},
/// Error when the used credentials lack valid authentication
#[error(
"The operation lacked valid authentication credentials for path {:?}: {:?}",
path,
error
)]
Unauthenticated {
/// The path to the file
path: String,
/// The wrapped error
error: String,
},
/// Error when a configuration key is invalid for the store used
#[error("Configuration key: '{}' is not valid", key)]
UnknownConfigurationKey {
/// The configuration key used
key: String,
},
}
pub type Result<T, E = Error> = std::result::Result<T, E>;