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;