coven_storage/storage.rs
1use crate::{
2 ByteProgress, CloudProvider, ObjectPath, ObjectPrefix, ObjectRead, PublicationRequest,
3 StorageConfig, StorageError, StorageFailure, StoredObject, UploadSession,
4};
5use async_trait::async_trait;
6
7/// A nonempty half-open byte range; end must not exceed the object's length.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct ByteRange {
10 start: u64,
11 end: u64,
12}
13
14#[cfg(test)]
15#[path = "storage_tests.rs"]
16mod tests;
17
18impl ByteRange {
19 /// Validate the bounds before issuing a ranged request.
20 pub fn new(start: u64, end: u64) -> Result<Self, StorageError> {
21 if start >= end {
22 return Err(StorageFailure::InvalidRange.into());
23 }
24 Ok(Self { start, end })
25 }
26 /// First byte included.
27 pub fn start(self) -> u64 {
28 self.start
29 }
30 /// First byte excluded.
31 pub fn end(self) -> u64 {
32 self.end
33 }
34 /// Number of requested bytes.
35 pub fn len(self) -> u64 {
36 self.end - self.start
37 }
38 /// A validated range is never empty.
39 pub fn is_empty(self) -> bool {
40 false
41 }
42 pub(crate) fn header(self) -> String {
43 format!("bytes={}-{}", self.start, self.end - 1)
44 }
45 #[cfg(any(test, feature = "test-utils"))]
46 pub(crate) fn select(self, bytes: &[u8]) -> Result<Vec<u8>, StorageError> {
47 let start = usize::try_from(self.start)
48 .map_err(|error| StorageFailure::InvalidRange.with_source(error))?;
49 let end = usize::try_from(self.end)
50 .map_err(|error| StorageFailure::InvalidRange.with_source(error))?;
51 bytes
52 .get(start..end)
53 .map(<[u8]>::to_vec)
54 .ok_or(StorageError::Failure(StorageFailure::InvalidRange))
55 }
56}
57
58/// A part the connection admitted (§17.9): no provider cooldown was live, the
59/// session names the connection's location, the part fits the session, and
60/// its destination could be published. Admitting a planned session's first
61/// part marked its plan. Sending the part consumes the permit.
62#[must_use = "a part goes to the provider only through its permit"]
63pub struct PartPermit<'a> {
64 pub(crate) bytes: &'a [u8],
65 /// Where the part starts and ends in the object.
66 pub(crate) part: std::ops::Range<u64>,
67 pub(crate) publication: PublicationRequest,
68 marked: bool,
69}
70
71impl<'a> PartPermit<'a> {
72 /// Admit `bytes` as the part `part` covers, marking a planned session's
73 /// plan.
74 pub(crate) fn admit(
75 session: &mut UploadSession,
76 bytes: &'a [u8],
77 part: std::ops::Range<u64>,
78 publication: PublicationRequest,
79 ) -> Self {
80 let marked = session.mark_start();
81 Self { bytes, part, publication, marked }
82 }
83 /// Whether admitting the part marked the session's plan, changing what
84 /// the caller records: save the session before sending the part, so a
85 /// recording that may name an upload says so (Appendix E.10).
86 pub fn marked(&self) -> bool {
87 self.marked
88 }
89}
90
91/// How a removed device's member cuts off its provider access (§14.2, E9).
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum ProviderSignOut {
94 /// Remove the app's access, then sign in again on retained devices.
95 RemoveAppAccess {
96 /// Dropbox or OneDrive.
97 provider: CloudProvider,
98 },
99 /// Remove the device from the Apple account.
100 RemoveFromAppleAccount,
101}
102
103pub use coven_format::MemberAccess;
104
105/// Granting access succeeds either through the provider or through an admin action.
106pub enum AccessGrant {
107 /// The provider shared the store with the account.
108 Granted {
109 /// Provider acceptance information to include in the encrypted invite code.
110 invitation: crate::StorageInvitation,
111 },
112 /// The admin makes a key in the provider console and enters it in the invite.
113 CreateAccessKey,
114}
115
116/// A grant that revocation left for the owner to inspect.
117#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
118pub struct RetainedAccess {
119 /// The native permission, membership or group id, scoped to this store.
120 pub provider_id: String,
121 /// Why removing this grant cannot revoke only the requested account.
122 pub reason: RetainedAccessReason,
123}
124
125/// The provider's reason that a grant was retained.
126#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
127pub enum RetainedAccessReason {
128 /// The grant also reaches other accounts, including public or group access.
129 OtherAccounts,
130 /// The grant comes from a parent location.
131 Inherited,
132 /// The provider did not identify the recipient sufficiently to remove it safely.
133 UnidentifiedAccount,
134 /// The grant belongs to the store's owner, whose access cannot be removed.
135 StoreOwner,
136}
137
138/// The operations coven needs from a provider (§20.1).
139///
140/// Implementations are capabilities assembled at composition roots. They own no
141/// database or key custody. Every mutation is awaited and failures reach the
142/// caller; no hidden background work repairs failed operations.
143#[async_trait]
144pub trait Storage: Send + Sync {
145 /// Install a member's replacement S3 key after credential custody commits.
146 /// Other provider kinds refuse this operation without changing sign-in state.
147 async fn set_s3_credentials(
148 &self,
149 _credentials: crate::S3Credentials,
150 ) -> Result<(), StorageError> {
151 Err(StorageFailure::InvalidConfiguration.with_source("provider does not use S3 keys"))
152 }
153 /// This provider's nonsecret location settings.
154 fn config(&self) -> StorageConfig;
155 /// The signed-in sharing account, for the member's admin access entry.
156 /// S3 uses its supplied access key id instead of an account lookup.
157 async fn account(&self) -> Result<String, StorageError> {
158 Err(StorageFailure::InvalidConfiguration.with_source("provider has no sharing account"))
159 }
160 /// Largest complete encrypted body the provider takes in one request, in
161 /// bytes. Objects past it, or past [`crate::BUFFERED_REQUEST_LIMIT`], go
162 /// in parts through resumable or multipart uploads
163 /// ([`crate::goes_in_parts`]). Buffered floor replacements accept
164 /// complete bodies up to this limit; OneDrive stages
165 /// the body in a revision-bound session before conditional publication.
166 /// Callers retaining sessions across crashes record `begin_upload` or
167 /// `begin_replace` sessions.
168 /// S3 uses 5 GiB, Dropbox 150 MiB and OneDrive 250,000,000 bytes.
169 /// CloudKit uses bounded 8 MiB assets.
170 fn single_request_limit(&self) -> u64;
171 /// The size of the parts a ranged read moves whole, counted from the
172 /// object's start, the last shorter: a range moves every part it
173 /// touches, as iCloud's reads do (Appendix E.1). `None` when a range
174 /// moves only its own bytes.
175 fn read_part_size(&self) -> Option<std::num::NonZeroU64> {
176 None
177 }
178 /// Install replacement OAuth tokens after the owner commits them to key custody.
179 /// S3 and CloudKit refuse OAuth tokens; they use different account credentials.
180 async fn set_oauth_tokens(&self, _tokens: crate::OAuthTokens) -> Result<(), StorageError> {
181 Err(StorageFailure::InvalidConfiguration.with_source("provider does not use OAuth"))
182 }
183 /// Create a complete encrypted object, refusing an occupied path with
184 /// [`StorageFailure::AlreadyExists`]. Occupation alone proves nothing: a
185 /// retry compares the occupant's complete bytes itself (§5.1, §19.3).
186 /// Bodies [`crate::goes_in_parts`] sends in parts use a session; a writes
187 /// slot can contain multiple writes in one object.
188 /// A failed transfer aborts the session;
189 /// if abort fails too, [`StorageError::Cleanup`] retains both causes.
190 /// Returns the object's metadata when the provider's one-request reply
191 /// carries it; otherwise exact status supplies it (§21.1).
192 async fn create(
193 &self,
194 path: &ObjectPath,
195 bytes: &[u8],
196 ) -> Result<Option<StoredObject>, StorageError> {
197 self.create_reporting(path, bytes, &ByteProgress::unread()).await
198 }
199 /// [`Self::create`], telling `sent` how many of the object's bytes the
200 /// connection has taken, as it takes them: Dropbox's, OneDrive's and
201 /// S3's bodies say as they go, CloudKit's client as the fraction of each
202 /// asset sent (§6.7).
203 async fn create_reporting(
204 &self,
205 path: &ObjectPath,
206 bytes: &[u8],
207 sent: &ByteProgress,
208 ) -> Result<Option<StoredObject>, StorageError>;
209 /// Replace the floor only if its observed native revision still matches.
210 /// A stale observation returns `Conflict` without changing the object.
211 /// The new bytes become visible together, or not at all. A body above
212 /// [`Self::single_request_limit`] returns [`StorageFailure::SingleRequestTooLarge`].
213 async fn replace(&self, previous: &StoredObject, bytes: &[u8]) -> Result<(), StorageError>;
214 /// Exact lookup. None means confirmed absence; failed lookups remain errors.
215 /// Pending multipart assets must not be reported as complete objects.
216 async fn status(&self, path: &ObjectPath) -> Result<Option<StoredObject>, StorageError>;
217 /// Read bytes and metadata from the same object revision. A range still
218 /// reports the complete encrypted size and whole-object checksum semantics.
219 async fn stream_object(
220 &self,
221 path: &ObjectPath,
222 range: Option<ByteRange>,
223 ) -> Result<crate::ObjectStream<'_>, StorageError> {
224 self.stream_object_reporting(path, range, &ByteProgress::unread()).await
225 }
226 /// [`Self::stream_object`], telling `received` how far through the
227 /// object the bytes received reach, as they arrive: as the stream reads
228 /// them, and for CloudKit, whose client fetches a record's asset whole,
229 /// as the fetch goes (§6.7).
230 async fn stream_object_reporting(
231 &self,
232 path: &ObjectPath,
233 range: Option<ByteRange>,
234 received: &ByteProgress,
235 ) -> Result<crate::ObjectStream<'_>, StorageError>;
236 /// Collect a bounded response through the streaming implementation. Large
237 /// writes and snapshots consume `stream_object` directly. This convenience
238 /// reader holds at most 32 MiB, regardless of provider metadata.
239 async fn read_object(
240 &self,
241 path: &ObjectPath,
242 range: Option<ByteRange>,
243 ) -> Result<ObjectRead, StorageError> {
244 self.stream_object(path, range)
245 .await?
246 .collect(crate::object_stream::BUFFERED_READ_LIMIT)
247 .await
248 }
249 /// Read a bounded whole encrypted object; the limit is the same as `read_object`.
250 async fn read(&self, path: &ObjectPath) -> Result<Vec<u8>, StorageError> {
251 Ok(self.read_object(path, None).await?.bytes)
252 }
253 /// Fetch only the requested bytes; never silently shorten a range.
254 async fn read_range(
255 &self,
256 path: &ObjectPath,
257 range: ByteRange,
258 ) -> Result<Vec<u8>, StorageError> {
259 Ok(self.read_object(path, Some(range)).await?.bytes)
260 }
261 /// List every object under the prefix, across all pages.
262 /// Missing or malformed metadata fails the call.
263 async fn list(&self, prefix: &ObjectPrefix) -> Result<Vec<StoredObject>, StorageError>;
264
265 /// Delete an object. An already absent object succeeds, making operation retries safe.
266 /// Sync chooses the deleting device under §16.6; storage exposes no deletion-rights
267 /// query. A provider refusal retains its cause, including a refused owned-object deletion.
268 async fn delete(&self, path: &ObjectPath) -> Result<(), StorageError>;
269 /// [`Self::delete`] for a caller that wants the object gone and has no use
270 /// for the difference between deleting it and finding nothing: a
271 /// `NotFound` failure, such as a provider reporting its folder missing,
272 /// is done too. Every other failure reaches the caller.
273 async fn delete_if_present(&self, path: &ObjectPath) -> Result<(), StorageError> {
274 match self.delete(path).await {
275 Err(error) if error.failure() == StorageFailure::NotFound => Ok(()),
276 result => result,
277 }
278 }
279 /// Share the store using its owner's account, or tell an S3 admin to create a key.
280 /// A signed-in sharing account that does not own the location gets `NotStoreOwner`.
281 /// Ownership comes from Dropbox's folder `access_type`,
282 /// OneDrive's current-account drive id, or CloudKit's current user record id compared with the zone owner.
283 /// S3's console-key instructions need no sharing-account check.
284 /// Inviting the account that holds the store gets
285 /// [`StorageFailure::InviteeIsStoreOwner`]. Dropbox and OneDrive refuse
286 /// before any sharing change; CloudKit learns it from the saved share,
287 /// which then lists only the owner.
288 ///
289 /// Dropbox upgrades viewers in place with `update_folder_member`; a pending
290 /// viewer without a native account id gets [`StorageFailure::AccountIdUnavailable`]
291 /// and keeps its invitation.
292 async fn grant_access(&self, account: &str) -> Result<AccessGrant, StorageError>;
293 /// Finish provider onboarding under the invited account, then check that the
294 /// location can be read. The facade verifies coven's store identity and keys.
295 /// Dropbox mounts the shared namespace. OneDrive checks the share's drive/folder
296 /// before redeeming it; CloudKit checks share metadata before native acceptance.
297 /// Native acceptance material comes from the encrypted [`crate::StorageInvitation`],
298 /// not a revocation grant id. Removal during joining returns the provider's
299 /// permission failure; an admin can invite again.
300 async fn join(
301 &self,
302 store: coven_foundation::id_source::StoreId,
303 invitation: &crate::StorageInvitation,
304 ) -> Result<(), StorageError> {
305 invitation.check(&self.config())?;
306 if !matches!(invitation.acceptance, crate::invitation::InvitationAcceptance::Granted) {
307 return Err(
308 StorageFailure::InvalidConfiguration.with_source("provider acceptance required")
309 );
310 }
311 self.status(&ObjectPath::floor(store)).await?;
312 Ok(())
313 }
314 /// Undo [`Self::join`]'s acceptance under the invited account, for a join
315 /// cancelled before its request was sent, keeping the invite usable
316 /// (§13.2, E.2). Dropbox unmounts the shared namespace, which leaves the
317 /// account a member, so joining again mounts it again; finding it
318 /// unmounted already, or the account no longer a member, is done. S3
319 /// accepts nothing. OneDrive and CloudKit undo nothing: Graph documents no
320 /// way for the recipient to give up a redeemed link's access, and a
321 /// CloudKit participant that deletes the share is removed from it.
322 async fn leave(&self, invitation: &crate::StorageInvitation) -> Result<(), StorageError> {
323 invitation.check(&self.config())?;
324 if !matches!(invitation.acceptance, crate::invitation::InvitationAcceptance::Granted) {
325 return Err(
326 StorageFailure::InvalidConfiguration.with_source("provider acceptance required")
327 );
328 }
329 Ok(())
330 }
331 /// Unshare through the owner's account. Coven shares nothing on S3, so
332 /// there is nothing to take back.
333 /// Ownership is checked as for [`Self::grant_access`]. Only grants exclusive to
334 /// this account are removed; owner, inherited, shared or unidentified grants
335 /// are returned for owner action. An empty list confirms revocation.
336 ///
337 /// OneDrive resolves native identities across all permission pages and keeps
338 /// email-bearing grants until dependent ID-only removals succeed. Dropbox pages direct and
339 /// inherited memberships separately and waits for the native removal job,
340 /// preserving its failure cause; group and parent access remain reported.
341 async fn revoke_access(&self, account: &str) -> Result<Vec<RetainedAccess>, StorageError>;
342 /// Begin a create-once upload, returning the value to record before parts are sent.
343 /// This also creates an absent floor without exposing partial bytes.
344 async fn begin_upload(
345 &self,
346 path: &ObjectPath,
347 total: u64,
348 ) -> Result<UploadSession, StorageError>;
349 /// Begin a floor upload bound to this observed native revision.
350 /// Record it before sending parts; restarting never adopts a newer revision.
351 async fn begin_replace(
352 &self,
353 previous: &StoredObject,
354 total: u64,
355 ) -> Result<UploadSession, StorageError>;
356 /// Begin a fresh recorded session after resumption or publication reports
357 /// `SessionExpired`. A conflicted S3 session must be aborted first; failed
358 /// cleanup prevents a replacement from starting.
359 /// The destination and total stay fixed; the caller records the returned value
360 /// and supplies the retained encrypted bytes again, starting at offset zero.
361 async fn restart_upload(&self, expired: &UploadSession) -> Result<UploadSession, StorageError> {
362 expired.check(&self.config())?;
363 if expired.is_complete() {
364 return Err(StorageFailure::InvalidPart.into());
365 }
366 if matches!(expired.state, crate::session::SessionState::S3Restart { .. }) {
367 self.abort_upload(expired).await?;
368 }
369 match expired.target() {
370 crate::UploadTarget::Create(path) => {
371 self.begin_upload(path, expired.total_bytes()).await
372 }
373 crate::UploadTarget::Replace(previous) => {
374 self.begin_replace(previous, expired.total_bytes()).await
375 }
376 }
377 }
378 /// Refresh a recorded session from the provider after interruption.
379 /// Persist the returned state even on failure, as with `finish_upload`.
380 async fn resume_upload(&self, session: &mut UploadSession) -> Result<(), StorageError>;
381 /// Admit `bytes` as the session's next part, the first it has still to
382 /// send ([`UploadSession::parts_to_send`]), sending nothing. Refused
383 /// while a provider cooldown is live, for a session naming another
384 /// location, for a part that doesn't fit the session, and while its
385 /// destination can't be published (§17.9). Admitting a planned
386 /// session's first part marks its plan, before any request that may
387 /// start its upload: when [`PartPermit::marked`] says so, save the
388 /// session before sending the part (Appendix E.10).
389 fn admit_part<'a>(
390 &self,
391 session: &mut UploadSession,
392 bytes: &'a [u8],
393 ) -> Result<PartPermit<'a>, StorageError>;
394 /// Send an admitted part, advancing the value only after confirmation,
395 /// and telling `sent` how far through the object the part's bytes the
396 /// connection has taken reach, as [`Self::create_reporting`] does. After
397 /// a lost publication reply this verifies the stored bytes instead; see
398 /// [`UploadSession`].
399 async fn send_part(
400 &self,
401 permit: PartPermit<'_>,
402 session: &mut UploadSession,
403 sent: &ByteProgress,
404 ) -> Result<(), StorageError>;
405 /// Send part `offset..` of a session whose parts are independent
406 /// ([`UploadSession::parts_independent`]), leaving the session as it is:
407 /// the provider's confirmation comes back, for the caller to add with
408 /// [`UploadSession::confirm`] and then record. Several parts may be in
409 /// flight at once and come back in any order (§17.9). Refused, sending
410 /// nothing, while a provider cooldown is live, for a session naming
411 /// another location or whose parts go in order, for a part that isn't a
412 /// whole one the session lacks, and while its destination can't be
413 /// published. `sent` hears the part's bytes as [`Self::send_part`]'s do.
414 async fn send_independent_part(
415 &self,
416 session: &UploadSession,
417 offset: u64,
418 bytes: &[u8],
419 sent: &ByteProgress,
420 ) -> Result<crate::ConfirmedPart, StorageError>;
421 /// Admit and send the next part, for a caller that records the session
422 /// only after the part: one that never resumes a planned session from a
423 /// recording.
424 async fn upload_part(
425 &self,
426 session: &mut UploadSession,
427 bytes: &[u8],
428 ) -> Result<(), StorageError> {
429 let permit = self.admit_part(session, bytes)?;
430 self.send_part(permit, session, &ByteProgress::unread()).await
431 }
432 /// Publish all stored parts at the destination. A repeated completion is safe.
433 /// Persist the session even on failure. `SessionExpired` requires a fresh
434 /// session with the same bytes; a `Cleanup` error must not bypass failed abort.
435 async fn finish_upload(&self, session: &mut UploadSession) -> Result<(), StorageError>;
436 /// Explicitly abandon an upload. Dropping a recorded value does not abort it.
437 /// Already closed or absent sessions succeed; published objects are preserved.
438 async fn abort_upload(&self, session: &UploadSession) -> Result<(), StorageError>;
439 /// End every unfinished session at `store`'s floor begun before the
440 /// current floor was published, whoever began it (§17.10): each observed
441 /// an older revision, so none can publish. Only where the provider dates
442 /// a session's start, certified per S3 profile (Appendix E.6); elsewhere
443 /// it sends nothing.
444 async fn end_superseded_floor_sessions(
445 &self,
446 store: coven_foundation::id_source::StoreId,
447 ) -> Result<(), StorageError>;
448 /// End every unfinished session at slots 1 to `through` of `store`,
449 /// which this device has applied, whoever began it (§17.10): create-once
450 /// refuses each of them there, so none can ever publish, and a claimant
451 /// whose session is ended settles its claim as lost, as a refused one
452 /// does. Ending one already ended succeeds. Only S3 under conditional
453 /// completion puts a claim's session at its slot's path.
454 async fn end_settled_claims(
455 &self,
456 store: coven_foundation::id_source::StoreId,
457 through: u64,
458 ) -> Result<(), StorageError>;
459 /// End every upload `device` left in `store` that this sign-in can see
460 /// (§17.10): abort its unfinished sessions and delete what it staged in
461 /// its reserved folder. One already ended succeeds, so devices may do it
462 /// at once. A Dropbox session, and a OneDrive native one, is seen by no
463 /// one else and expires on its own: on Dropbox this ends nothing, and on
464 /// OneDrive only staged temporaries (Appendix E.2).
465 async fn end_device_uploads(
466 &self,
467 store: coven_foundation::id_source::StoreId,
468 device: coven_foundation::id_source::DeviceId,
469 ) -> Result<(), StorageError>;
470}