Skip to main content

coven_database/
file_write_moves.rs

1//! The moves of this device's files to the cloud that a write records
2//! (§17.3): which files each moves, checked when the app names them, and
3//! the uploads queued for them once the write applied.
4
5use super::{FileWrite, StagedSource};
6use crate::{
7    file_hashes::FileHashes,
8    file_location::{FileLocation, Place},
9    file_moves::{self, MoveState},
10    file_table::Record,
11    DbError, MoveId, Provenance,
12};
13use coven_foundation::{files::StagedName, id_source::FileId};
14use std::collections::BTreeSet;
15
16/// A file of this device, on it only, to move to the cloud (§17.3, E3).
17#[derive(Clone, Copy, Debug)]
18pub enum MoveFile {
19    /// A user's original: its upload checks the bytes it sends against the
20    /// size, modification time and chunk hashes its staging recorded.
21    User(FileId),
22    /// As `User`, and kept whole here too once stored: the write switching
23    /// it to the cloud pins it on this device, which downloads it from
24    /// storage. The original is never read for the cache (§17.8).
25    UserKeptHere(FileId),
26    /// An app file, uploaded from coven's copy.
27    App(FileId),
28    /// As `App`, and kept whole here too once stored: the cache copy is
29    /// made from coven's copy (§17.8).
30    AppKeptHere(FileId),
31}
32
33/// A move this write recorded, with the uploads it queues once applied.
34pub(super) struct Moving {
35    id: MoveId,
36    uploads: Vec<MoveUpload>,
37}
38
39/// One upload of a move: the files over the same bytes it stores, each with
40/// whether this device keeps it whole once stored.
41struct MoveUpload {
42    files: Vec<(FileId, bool)>,
43    size: u64,
44    source: StagedSource,
45    hashes: FileHashes,
46}
47
48impl FileWrite<'_> {
49    /// Record a move of these files to the cloud (§17.3). Each must be on
50    /// this device only (`NotThisDevice`, `NotDeviceOnlyHere`) and in no
51    /// move, whether one this write recorded or a moving or failed one
52    /// (`FileInMove`). A user's original uploads with the facts and hashes
53    /// its staging recorded, so its upload fails the move if the original
54    /// changed since. The files switch to the cloud together, in one write
55    /// once every one is stored.
56    pub(crate) fn move_to_cloud(&self, files: Vec<MoveFile>) -> Result<MoveId, DbError> {
57        let db = self.database;
58        let mut uploads: Vec<MoveUpload> = Vec::new();
59        let mut named = BTreeSet::new();
60        for file in files {
61            let (id, keep) = match file {
62                MoveFile::User(id) | MoveFile::App(id) => (id, false),
63                MoveFile::UserKeptHere(id) | MoveFile::AppKeptHere(id) => (id, true),
64            };
65            if !named.insert(id) {
66                continue;
67            }
68            let record = Record::read(db, id)?.ok_or(DbError::FileAbsent { file: id })?;
69            match record.place {
70                Place::DeviceOnly(device) if device == self.files.device => {}
71                Place::DeviceOnly(_) => return Err(DbError::NotThisDevice { file: id }),
72                Place::Cloud(_) => return Err(DbError::NotDeviceOnlyHere { file: id }),
73            }
74            // A file is in at most one move: one this write recorded, or a
75            // moving or failed one, which the app cancels to free it.
76            let recorded = self
77                .files
78                .moving
79                .borrow()
80                .iter()
81                .find(|moving| {
82                    moving.uploads.iter().any(|upload| upload.files.iter().any(|(f, _)| *f == id))
83                })
84                .map(|moving| moving.id);
85            let holding = match recorded {
86                Some(of) => Some(of),
87                None => file_moves::of_file(db, id)?,
88            };
89            if let Some(of) = holding {
90                return Err(DbError::FileInMove { file: id, id: of });
91            }
92            let upload = match file {
93                MoveFile::User(_) | MoveFile::UserKeptHere(_) => {
94                    let wrong_kind = || DbError::NamespaceKind {
95                        namespace: record.namespace.clone(),
96                        kind: Provenance::AppProvided,
97                    };
98                    let original = crate::user_file::read(db, id)?.ok_or_else(wrong_kind)?;
99                    let hashes = crate::user_file::hashes(db, id)?.ok_or_else(wrong_kind)?;
100                    MoveUpload {
101                        files: vec![(id, keep)],
102                        size: original.size,
103                        source: StagedSource::Original(original),
104                        hashes,
105                    }
106                }
107                MoveFile::App(_) | MoveFile::AppKeptHere(_) => {
108                    let (name, hashes) = copy(db, id)?.ok_or_else(|| DbError::NamespaceKind {
109                        namespace: record.namespace.clone(),
110                        kind: Provenance::UserProvided,
111                    })?;
112                    MoveUpload {
113                        files: vec![(id, keep)],
114                        size: record.size,
115                        source: StagedSource::Copy(name),
116                        hashes,
117                    }
118                }
119            };
120            // Files over the same bytes here share one upload.
121            match uploads.iter_mut().find(|queued| same_bytes(queued, &upload)) {
122                Some(queued) => queued.files.push((id, keep)),
123                None => uploads.push(upload),
124            }
125        }
126        let id = file_moves::create(db)?;
127        self.files.moving.borrow_mut().push(Moving { id, uploads });
128        Ok(id)
129    }
130
131    /// Cancel a move: every file stays on this device only, and its record
132    /// ends (§17.3). Cancelling a cancelled move does nothing; a finished
133    /// move, whose files are in the cloud, can't be cancelled, nor can a
134    /// bound file's, whose held write would go out naming a file no other
135    /// device can read (§17.4).
136    pub(crate) fn cancel_move(&self, id: MoveId) -> Result<(), DbError> {
137        let db = self.database;
138        match file_moves::state(db, id)? {
139            None => Err(DbError::MoveUnknown { id }),
140            Some(MoveState::Finished) => Err(DbError::MoveFinished { id }),
141            Some(MoveState::Cancelled) => Ok(()),
142            Some(MoveState::Moving | MoveState::Failed) if file_moves::bound(db, id)? => {
143                Err(DbError::MoveBound { id })
144            }
145            Some(MoveState::Moving | MoveState::Failed) => {
146                self.files.moving.borrow_mut().retain(|moving| moving.id != id);
147                file_moves::end_unswitched(db, id, MoveState::Cancelled)?;
148                crate::pending::clear_move(db, id)
149            }
150        }
151    }
152
153    /// Queue the uploads of each move this write recorded, for the files
154    /// still on this device only once it applied. A move left with none
155    /// has nothing to switch, and ends.
156    pub(super) fn queue_moves(&self, now: std::time::SystemTime) -> Result<(), DbError> {
157        let db = self.database;
158        for moving in self.files.moving.take() {
159            let mut queued = false;
160            for upload in moving.uploads {
161                let mut files = Vec::new();
162                for (file, keep) in upload.files {
163                    if Record::read(db, file)?
164                        .is_some_and(|record| record.place == Place::DeviceOnly(self.files.device))
165                    {
166                        files.push((file, keep));
167                    }
168                }
169                if files.is_empty() {
170                    continue;
171                }
172                let location = FileLocation::new(self.files.device, self.files.ids)?;
173                crate::file_queue::enqueue(
174                    db,
175                    crate::file_queue::Queued {
176                        files: &files,
177                        size: upload.size,
178                        source: &upload.source,
179                        hashes: &upload.hashes,
180                        of: moving.id,
181                        holds: None,
182                    },
183                    &location,
184                    now,
185                )?;
186                queued = true;
187            }
188            if !queued {
189                file_moves::finish(db, moving.id)?;
190            }
191        }
192        Ok(())
193    }
194}
195
196/// Whether two uploads would send the same bytes here: the same copy, or
197/// the same original as one read recorded it. Two stagings of one path
198/// recorded apart may have read different bytes, and at most one of them
199/// still matches it.
200fn same_bytes(a: &MoveUpload, b: &MoveUpload) -> bool {
201    match (&a.source, &b.source) {
202        (StagedSource::Original(original), StagedSource::Original(other)) => {
203            original == other && a.hashes.content == b.hashes.content
204        }
205        (StagedSource::Copy(a), StagedSource::Copy(b)) => a == b,
206        _ => false,
207    }
208}
209
210/// Coven's copy of an app file, with the hashes its first read recorded.
211fn copy(
212    db: &crate::sqlite::DatabaseConnection,
213    file: FileId,
214) -> Result<Option<(StagedName, FileHashes)>, DbError> {
215    let Some((path, content)) = db
216        .query("SELECT path,hash FROM _coven_device_files WHERE file=?1", [file], |r| {
217            Ok((r.get::<_, String>(0)?, r.get::<_, Vec<u8>>(1)?))
218        })?
219        .pop()
220    else {
221        return Ok(None);
222    };
223    let chunks = db.query(
224        "SELECT hash FROM _coven_copy_chunks WHERE path=?1 ORDER BY chunk",
225        [&path],
226        |r| r.get::<_, Vec<u8>>(0),
227    )?;
228    Ok(Some((
229        StagedName::parse(&path).ok_or(DbError::DamagedDatabase)?,
230        FileHashes::stored(content, chunks)?,
231    )))
232}