Skip to main content

coven/
coven_write.rs

1//! The app's write: its SQL and coven's steps in one transaction (§4, E3).
2
3use crate::*;
4use coven_database::SqlContext;
5use coven_format::admin::StoreChange;
6
7/// One write's SQL and coven's steps, in one transaction (§4, E3). A step
8/// runs where it is called: the write's later SQL sees what it changed, and
9/// it commits or rolls back with the app's statements. A failed step fails
10/// the write, even if the closure goes on and returns success. Returning an
11/// error rolls back every statement and step.
12///
13/// An administrative change, making a circle, and approving a join
14/// request, is checked against the state this device has applied and
15/// journaled as an operation, with or without storage (§11.1). Its step
16/// returns the operation's id, which `coven_operations` lists from the
17/// write's commit until the app acknowledges its outcome, or `None` when the
18/// applied state already has what it asks for. A change the applied
19/// state refuses fails with `Rejected`. Once the write commits, a pass
20/// catches up, validates the change again and claims its slot; a change the
21/// caught-up state refuses is undone and ends `Rejected` (§11.2).
22pub struct CovenWrite<'write> {
23    sql: SqlContext<'write, 'write>,
24    /// The store's operation steps, which run in this write.
25    steps: &'write coven_sync::OperationSteps,
26    files: &'write coven_sync::Files,
27}
28
29impl<'write> CovenWrite<'write> {
30    pub(crate) fn new(
31        sql: SqlContext<'write, 'write>,
32        steps: &'write coven_sync::OperationSteps,
33        files: &'write coven_sync::Files,
34    ) -> Self {
35        Self { sql, steps, files }
36    }
37
38    /// Puts files `CovenHandle::stage_files` staged into this write, and
39    /// returns what its batch returned, such as their ids, for this write's
40    /// rows to reference (§17.2). The write makes each file's record, in the
41    /// audience of the rows that name it, and queues its upload for the
42    /// cloud. Files staged by another open store, or by this one before it
43    /// closed, fail with `PreparedElsewhere`; a file no row names fails the
44    /// write with `FileUnreferenced`.
45    pub fn put_staged<T>(&self, staged: StagedFiles<T>) -> Result<T, DbError> {
46        self.sql.put_staged(staged)
47    }
48
49    /// Stages a user's original, read whole once already, as a new file in
50    /// a user namespace and returns its id for a row to reference: on this
51    /// device only, or for the cloud, with its upload queued. Coven records
52    /// the original's path, size, modification time and the hashes its read
53    /// found, which its upload checks, now or when the file moves to the
54    /// cloud; it never changes the original, nor reads it here (§17.1,
55    /// §17.3). An undeclared namespace fails with `UndeclaredNamespace`, an
56    /// app one with `NamespaceKind`.
57    pub fn put_user_file(
58        &self,
59        namespace: &str,
60        original: PreparedUserFile,
61        place: Place,
62    ) -> Result<FileId, DbError> {
63        self.sql.put_user_file(namespace, original, place)
64    }
65
66    /// Stages a new file over an existing file's bytes, for a row in any
67    /// audience, and returns its id for that row to reference. The write
68    /// makes it in the audience of the rows that reference it, with
69    /// `from`'s namespace, size, place and key; nothing is uploaded again. A
70    /// file on this device only gives a new file on this device only, over
71    /// the same original or coven's copy. The write fails with `FileAbsent`
72    /// if this device has no record of `from`, and with `NotThisDevice` if
73    /// `from` is on another device only (§17.2).
74    pub fn existing_file(&self, from: FileId) -> FileId {
75        self.sql.existing_file(from)
76    }
77
78    /// Records a move of these files to the cloud (§17.3).
79    pub fn move_to_cloud(&self, files: Vec<MoveFile>) -> Result<MoveId, DbError> {
80        self.sql.move_to_cloud(files)
81    }
82
83    /// Cancels a move: every file stays on this device only (§17.3).
84    pub fn cancel_move(&self, id: MoveId) -> Result<(), DbError> {
85        self.sql.cancel_move(id)
86    }
87
88    /// Removes these files' copies from this device: every cache entry of
89    /// their stored bytes, pinned or not, and the partial download a pin
90    /// left (§17.8). Nothing else is evicted. A file this device has no
91    /// record of, or one on one device only, has nothing to remove. A file
92    /// kept here whose pin coven has not yet made has its pin request ended
93    /// too: `coven_pin_requests` loses it, and coven never makes that pin
94    /// (§17.8). The bytes go once the write has committed.
95    pub fn unpin(&self, files: &[FileId]) -> Result<(), DbError> {
96        self.sql.unpin(files)
97    }
98
99    /// Clears a declared namespace's cache: every unpinned file and chunk
100    /// of it goes (§17.8). Pinned copies, the partial downloads pins left
101    /// and other namespaces stay. The bytes go once the write has
102    /// committed. An undeclared namespace fails with `UndeclaredNamespace`.
103    pub fn clear_cache(&self, namespace: &str) -> Result<(), DbError> {
104        self.sql.clear_cache(namespace)
105    }
106
107    /// Removes a stored file's cached copies, pinned or not, without
108    /// changing storage or a local original (§17.8). A file this device has
109    /// no record of, or one on one device only, has none. The bytes go once
110    /// the write has committed.
111    pub fn evict_file(&self, file: FileId) -> Result<(), DbError> {
112        self.sql.evict_file(file)
113    }
114
115    /// Sets a declared namespace's cache budget and evicts its unpinned
116    /// files and chunks, least recently read first, until they are within it
117    /// (§17.8). Pinned bytes don't count toward the budget. An
118    /// undeclared namespace fails with `UndeclaredNamespace`. The evicted
119    /// bytes go once the write has committed.
120    pub fn set_cache_budget(&self, namespace: &str, max_bytes: u64) -> Result<(), DbError> {
121        self.sql.set_cache_budget(namespace, max_bytes)
122    }
123
124    /// Pauses trailing uploads, or resumes them (§17.9). A paused upload
125    /// keeps its place, including a provider upload session in progress,
126    /// and stops between confirmed parts; the setting persists. Bound
127    /// uploads run on, since writes wait for them (§17.4).
128    pub fn set_uploads_paused(&self, paused: bool) -> Result<(), DbError> {
129        self.files.set_uploads_paused(&self.sql, paused)
130    }
131
132    /// Moves files only on unmetered networks, or on any (§17.9). While the
133    /// network is metered, trailing uploads start no more and stop between
134    /// confirmed parts, and pins' downloads wait before their next request
135    /// to storage; bound uploads and reads run on. The setting persists, on
136    /// this device only. A retry asked after the write returns is never
137    /// answered from the setting it replaced.
138    pub fn set_unmetered_only(&self, unmetered_only: bool) -> Result<(), DbError> {
139        self.files.set_unmetered_only(&self.sql, unmetered_only)
140    }
141
142    /// Dismisses lost values the app has dealt with (§8.4): the write's
143    /// record carries the acknowledgements, and the removed rows' deletes
144    /// with their cascades, after the app's own changes, so every device
145    /// drops them from `_coven_lost`. A removed row is deleted for good and
146    /// never comes back. A value no longer lost as it was is left alone.
147    /// Setting a value and dismissing its loss in one write keeps the two
148    /// together; a write that also changes a row its dismissals delete fails
149    /// with `DismissedRowChanged`.
150    pub fn dismiss_lost_values(&self, values: &[LostValue]) -> Result<(), DbError> {
151        self.sql.dismiss_lost_values(values)
152    }
153
154    /// Keeps a prepared pin's files whole on this device, regardless of the
155    /// cache budget, which their bytes don't count toward (§17.8): each whole copy becomes the file's pinned cache
156    /// entry, and each copy already kept stays, all in this write.
157    /// Files prepared in another open store, or in this one before it
158    /// closed, fail with `PreparedElsewhere`; a file whose record left its
159    /// stored bytes since with `NoLongerStored`; and a copy already kept
160    /// that was evicted or unpinned since with `PinChanged`, which preparing again
161    /// downloads.
162    pub fn pin(&self, prepared: PreparedPin) -> Result<(), DbError> {
163        self.sql.pin(prepared)
164    }
165
166    /// Makes a prepared call's files device-only on this device, each
167    /// switched to its copy, which becomes the original this device
168    /// records, in this write (§17.3). Each must still be one this device
169    /// may make device-only, and still stored where its copy came from
170    /// (`NotInCloud`); copies prepared in another open store, or in this one
171    /// before it closed, fail with `PreparedElsewhere`. A write that also
172    /// removes such a file's last reference fails with
173    /// `DeviceOnlyUnreferenced`. A write that fails leaves the copies
174    /// unswitched, and they are removed once it has.
175    pub fn make_device_only(&self, prepared: PreparedDeviceOnly) -> Result<(), DbError> {
176        self.sql.make_device_only(prepared)
177    }
178
179    /// Makes an operation's retry or undo due now, from its recorded
180    /// progress (E6): its pending record and retry wait end in this write.
181    /// It fails with `NotPending` for an operation that has finished or
182    /// never existed, and with `Pending` and the record for one whose
183    /// record never retries, or only after an update not installed yet. It
184    /// can't bypass a provider cooldown. Once the write commits, a pass runs
185    /// the operation as soon as storage is connected.
186    pub fn retry_pending_operation(&self, operation: OperationId) -> Result<(), OperationError> {
187        self.steps.retry_pending_operation(&self.sql, operation)
188    }
189
190    /// Renames the store, as an admin (§10, E9).
191    pub fn rename_store(&self, name: &str) -> Result<Option<OperationId>, SyncError> {
192        self.steps.administer(&self.sql, StoreChange::RenameStore { name: name.into() })
193    }
194
195    /// Changes a member's role, as an admin (§10, E9).
196    pub fn set_member_role(
197        &self,
198        member: &MemberId,
199        role: MemberRole,
200    ) -> Result<Option<OperationId>, SyncError> {
201        self.steps.administer(&self.sql, StoreChange::ChangeRole { member: *member, role })
202    }
203
204    /// Removes a member and all their devices, as an admin (§14.1, E9).
205    /// Provider work appears in `coven_pending` (§14.2). An admin removing
206    /// themselves leaves: once the removal applies, this device deletes
207    /// their saved restore code and S3 key, and stops.
208    pub fn remove_member(&self, member: &MemberId) -> Result<Option<OperationId>, SyncError> {
209        self.steps.administer(&self.sql, StoreChange::RemoveMember { member: *member })
210    }
211
212    /// Removes a device: their own, or any as an admin (§14.1, E9). It also
213    /// says how the device's member signs out of the provider and signs in
214    /// again on the devices they keep, read from the recorded location: none
215    /// on S3, where nothing at the provider changes (§14.2). A store with no
216    /// recorded location fails with `NoStorage`.
217    pub fn remove_device(
218        &self,
219        device: DeviceId,
220    ) -> Result<(Option<OperationId>, Option<ProviderSignOut>), SyncError> {
221        self.steps.remove_device(&self.sql, device)
222    }
223
224    /// Publishes a prepared public pair for another active member, as an
225    /// admin, expecting the pair the applied state binds to them now (§10,
226    /// §12.3, E9). Replacing this member's own pair fails with
227    /// `Rejected(NotAllowed)`: that is `CovenHandle::replace_member_keys`.
228    pub fn replace_member_keys_for(
229        &self,
230        member: &MemberId,
231        keys: MemberPublicKeys,
232    ) -> Result<Option<OperationId>, SyncError> {
233        self.steps.replace_member_keys_for(&self.sql, *member, keys)
234    }
235
236    /// Makes a circle with this member as its first member, and returns its
237    /// id with the operation's (§15.5, E12). Any active member of the store
238    /// may make one. Its first key, the copy that hands it to this member
239    /// and its introduction follow in a pass, so rows go into the circle
240    /// once its slot applies. A name that isn't a valid name fails with
241    /// `Format`.
242    pub fn create_circle(&self, name: &str) -> Result<(CircleId, OperationId), SyncError> {
243        self.steps.create_circle(&self.sql, name.into())
244    }
245
246    /// Renames a circle this member is in, without changing its key,
247    /// members or rows (§15.5, E12).
248    pub fn rename_circle(
249        &self,
250        circle: CircleId,
251        name: &str,
252    ) -> Result<Option<OperationId>, SyncError> {
253        self.steps.administer(&self.sql, StoreChange::RenameCircle { circle, name: name.into() })
254    }
255
256    /// Deletes a circle this member is in: from its slot the deleted-circle
257    /// rule hides the circle's rows (§15.9, E12).
258    pub fn delete_circle(&self, circle: CircleId) -> Result<Option<OperationId>, SyncError> {
259        self.steps.administer(&self.sql, StoreChange::DeleteCircle { circle })
260    }
261
262    /// Adds a store member to a circle this member is in, sealing every key
263    /// the circle has had to them (§15.7, E12).
264    pub fn add_circle_member(
265        &self,
266        circle: CircleId,
267        member: &MemberId,
268    ) -> Result<Option<OperationId>, SyncError> {
269        self.steps.administer(&self.sql, StoreChange::AddCircleMember { circle, member: *member })
270    }
271
272    /// Removes a member from a circle this member is in; the remaining
273    /// members replace the exposed circle key in a later pass (§15.8, E12).
274    pub fn remove_circle_member(
275        &self,
276        circle: CircleId,
277        member: &MemberId,
278    ) -> Result<Option<OperationId>, SyncError> {
279        self.steps
280            .administer(&self.sql, StoreChange::RemoveCircleMember { circle, member: *member })
281    }
282
283    /// Makes an invite, as an admin, and returns its id, the member id it
284    /// fixes and its operation (§13.2, E10). A new person gets a fresh
285    /// member id; recovering an existing active member keeps their id, role,
286    /// circles and devices. The write checks the invite against the state
287    /// this device has applied and journals it, with or without storage; it
288    /// is listed in `coven_invites` from the commit, without a code. Once
289    /// the write commits, a pass checks it again against the history it
290    /// catches up with, grants the storage access and fixes the code, which
291    /// `coven_invites` then shows. A check that fails there, or a failed
292    /// grant, is the invite's pending record under its operation in
293    /// `coven_pending`; cancelling the invite ends it. It expires `lifetime`
294    /// after it is made. Made by a device that isn't a registered admin it
295    /// fails with
296    /// `Rejected(NotAllowed)`; an empty name with `Format`; an App-folder
297    /// Dropbox store with `DropboxAppFolderNotShareable`.
298    pub fn create_invite(
299        &self,
300        target: InviteTarget,
301        access: InviteAccess,
302        lifetime: InviteLifetime,
303        inviter: String,
304    ) -> Result<(InviteId, MemberId, OperationId), SyncError> {
305        self.steps
306            .create_invite(&self.sql, coven_sync::NewInvite { target, access, lifetime, inviter })
307    }
308
309    /// Approves exactly this checked request (§13.2, E10), and returns the
310    /// operation that binds its pair, which `coven_operations` lists from
311    /// this write's commit until the app acknowledges its outcome. The write
312    /// journals it, with or without storage, without checking it against the
313    /// state this device has applied: one the caught-up history refuses,
314    /// such as this admin's demotion, is the operation's outcome. Once it
315    /// commits, a pass catches up and claims one admission: the entry adding
316    /// the new member with the invite's role, or rebinding the existing
317    /// member's pair, and registering the request's device, its prefix
318    /// carrying the joiner's copies of every store key and the held circle
319    /// keys. It waits, claiming nothing, while this device lacks a store key
320    /// it must copy. Once begun, approval finishes. A rejection, met before
321    /// anything is claimed, ends the invite as a decline does, and is the
322    /// operation's outcome; it is not retried with a new entry. An admission
323    /// too large for one slot is refused with `AdmissionTooLarge`. A request the invite no longer shows, and an invite
324    /// whose lifetime has ended, fail with `InvitationChanged`.
325    pub fn approve_join_request(&self, request: &JoinRequest) -> Result<OperationId, SyncError> {
326        self.steps.approve_join_request(&self.sql, request)
327    }
328
329    /// Declines an open invite's checked request (§13.2, E10). The write
330    /// ends the invite, which leaves `coven_invites` with it; once it
331    /// commits, a pass deletes the request, so the requester's join ends as
332    /// declined, and takes back the storage access the invite granted. On
333    /// S3 the key stays pending until deleted and cleared under E9. A
334    /// request the invite no longer shows, an invite whose approval has
335    /// begun, and one whose lifetime has ended fail with
336    /// `InvitationChanged`; an ended one leaves the list when coven writes
337    /// its expiry.
338    pub fn decline_join_request(&self, request: &JoinRequest) -> Result<(), SyncError> {
339        self.steps.decline_join_request(&self.sql, request)
340    }
341
342    /// Cancels an invite before anyone joins with it (§13.2, E10). The
343    /// write ends the invite, which leaves `coven_invites` with it; once it
344    /// commits, a pass deletes a waiting request, so the requester's join
345    /// ends as declined, and takes back the storage access the invite
346    /// granted. On S3 the key stays pending until deleted and cleared under
347    /// E9. An invite whose approval has begun, and one whose lifetime has
348    /// ended, fail with `InvitationChanged`.
349    pub fn cancel_invite(&self, invite: &InviteId) -> Result<(), SyncError> {
350        self.steps.cancel_invite(&self.sql, *invite)
351    }
352
353    /// Acknowledges a finished operation's outcome, in this write, so it
354    /// leaves `coven_operations` (§11.4, E6). An app row that names the
355    /// operation changes in the same write. One still running fails with
356    /// `OperationRunning`; one not listed there with `OperationUnknown`.
357    pub fn acknowledge_operation(&self, operation: OperationId) -> Result<(), DbError> {
358        self.sql.acknowledge_operation(operation)
359    }
360
361    /// Acknowledges the new person's name a store a join published keeps,
362    /// in this write, so the store keeps it no longer: the write records the
363    /// app's profile of that person, or its request for one, and the two
364    /// commit together (§13.2). With no name kept, it succeeds and changes
365    /// nothing.
366    pub fn acknowledge_joined_invitee(&self) -> Result<(), DbError> {
367        self.sql.acknowledge_joined_invitee()
368    }
369
370    /// Execute one app statement with parameters.
371    pub fn execute<P: Params>(&self, sql: &str, params: P) -> SqliteResult<usize> {
372        self.sql.execute(sql, params)
373    }
374
375    /// Execute app statements within this write's transaction.
376    pub fn execute_batch(&self, sql: &str) -> SqliteResult<()> {
377        self.sql.execute_batch(sql)
378    }
379
380    /// Map one app row, including changes already made by this write.
381    pub fn query_row<T, P, F>(&self, sql: &str, params: P, map: F) -> SqliteResult<T>
382    where
383        P: Params,
384        F: FnOnce(&Row<'_>) -> SqliteResult<T>,
385    {
386        self.sql.query_row(sql, params, map)
387    }
388
389    /// Map all app rows produced by a query within this write.
390    pub fn query<T, P, F>(&self, sql: &str, params: P, map: F) -> SqliteResult<Vec<T>>
391    where
392        P: Params,
393        F: FnMut(&Row<'_>) -> SqliteResult<T>,
394    {
395        self.sql.query(sql, params, map)
396    }
397}
398
399impl SqlRead for CovenWrite<'_> {
400    fn query_row<T, P, F>(&self, sql: &str, params: P, map: F) -> SqliteResult<T>
401    where
402        P: Params,
403        F: FnOnce(&Row<'_>) -> SqliteResult<T>,
404    {
405        CovenWrite::query_row(self, sql, params, map)
406    }
407
408    fn query<T, P, F>(&self, sql: &str, params: P, map: F) -> SqliteResult<Vec<T>>
409    where
410        P: Params,
411        F: FnMut(&Row<'_>) -> SqliteResult<T>,
412    {
413        CovenWrite::query(self, sql, params, map)
414    }
415}
416
417#[cfg(test)]
418#[path = "coven_write_tests.rs"]
419mod tests;