Skip to main content

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}