Skip to main content

coven_database/
file_write_device_only.rs

1//! Making cloud files device-only on this device (§17.3): who may, and the
2//! write that switches every file of one call to this device, its copy
3//! becoming the original this device records.
4
5use super::FileWrite;
6use crate::{
7    file_hashes::FileHashes,
8    file_location::{FileLocation, Place},
9    file_table::Record,
10    sqlite::DatabaseConnection,
11    write_schema::WriteSchema,
12    DbError, Provenance,
13};
14use coven_foundation::id_source::{DeviceId, FileId};
15use coven_merge::RowId;
16use std::path::PathBuf;
17
18/// A cloud file to make device-only here: its id, the stored bytes its copy
19/// was downloaded from, the copy's path, which becomes the original this
20/// device records, and the hashes of the bytes written to it, which a later
21/// move to the cloud checks the copy against (§17.3).
22#[derive(Debug)]
23pub struct DeviceOnlyCopy {
24    /// The file.
25    pub file: FileId,
26    /// The stored bytes the copy holds.
27    pub location: FileLocation,
28    /// The copy, placed in its final path.
29    pub path: PathBuf,
30    /// The content hash and chunk hashes of the bytes written to the copy.
31    pub hashes: FileHashes,
32}
33
34/// The record of `file` once this device may make it device-only (§17.3):
35/// its record is here and in the cloud, of a user namespace; this device's
36/// member is in its audience; and it may change every row referencing it,
37/// which an `owner_only` table's rows another member created refuse. An
38/// admin gets no exception, as with any row (§9.6).
39pub(crate) fn permitted(
40    db: &DatabaseConnection,
41    schema: &WriteSchema,
42    device: DeviceId,
43    file: FileId,
44) -> Result<Record, DbError> {
45    let record = Record::read(db, file)?.ok_or(DbError::FileAbsent { file })?;
46    schema.namespace_of(&record.namespace, Provenance::UserProvided)?;
47    if !matches!(record.place, Place::Cloud(_)) {
48        return Err(DbError::NotInCloud { file });
49    }
50    crate::write_commit::check_membership(db, device, &record.audience)?;
51    let mut owners = crate::judge::Owners::new(db, schema, device, db.schema_version()?)?;
52    for (table, key) in db.query(
53        "SELECT DISTINCT table_name,key FROM _coven_file_references WHERE file=?1
54         ORDER BY table_name,key",
55        [file],
56        |r| Ok((r.get::<_, String>(0)?, r.get::<_, Vec<u8>>(1)?)),
57    )? {
58        let row = RowId { table, key, audience: record.audience.clone() };
59        if let Some(owner) = owners.foreign_owner(&row)? {
60            return Err(DbError::NotOwner {
61                key: crate::RowKey::decode(&row.key)?,
62                table: row.table,
63                owner,
64            });
65        }
66    }
67    Ok(record)
68}
69
70impl FileWrite<'_> {
71    /// Switch a prepared call's files to their copies in this write. Every
72    /// copy must be reserved in this store (`PreparedElsewhere`); the
73    /// reservation is let go once the write has decided.
74    pub(crate) fn make_prepared_device_only(
75        &self,
76        prepared: crate::PreparedDeviceOnly,
77    ) -> Result<(), DbError> {
78        super::held_here(self.files.copying, &prepared.paths())?;
79        let (copies, paths) = prepared.take();
80        self.files.held_copies.borrow_mut().extend(paths);
81        self.make_device_only(copies)
82    }
83
84    /// Switch each file to this device only, off the stored bytes its copy
85    /// was downloaded from, in this write (§17.3). Each must still be
86    /// permitted, and still stored there: a file another device's switch
87    /// took off them meanwhile is `NotInCloud`.
88    fn make_device_only(&self, copies: Vec<DeviceOnlyCopy>) -> Result<(), DbError> {
89        let db = self.database;
90        for copy in copies {
91            let record = permitted(db, self.files.schema, self.files.device, copy.file)?;
92            if record.place != Place::Cloud(copy.location.clone()) {
93                return Err(DbError::NotInCloud { file: copy.file });
94            }
95            crate::file_table::device_only(db, copy.file, self.files.device)?;
96            self.files.local.borrow_mut().push(copy);
97        }
98        Ok(())
99    }
100
101    /// Once the write applied, record each copy it switched as its file's
102    /// original here, with the size and modification time the copy had when
103    /// it was placed and the hashes of the bytes written to it, and keep the
104    /// copy (§17.3). An original recorded for the file before is forgotten,
105    /// never touched.
106    #[expect(
107        clippy::map_err_ignore,
108        reason = "a stored value that won't convert or decode is DamagedDatabase"
109    )]
110    pub(super) fn keep_device_only(&self) -> Result<(), DbError> {
111        let db = self.database;
112        for copy in self.files.local.take() {
113            let path = crate::database::file_database::copies::text(&copy.path)?;
114            let (size, modified_at) = db
115                .query(
116                    "SELECT size,modified_at FROM _coven_copies WHERE path=?1 AND state='placed'",
117                    [path],
118                    |r| Ok((r.get::<_, i64>(0)?, r.get::<_, Vec<u8>>(1)?)),
119                )?
120                .pop()
121                .ok_or(DbError::Invariant {
122                    what: "a copy is placed before its file switches to it",
123                })?;
124            let original = crate::UserFile {
125                path: copy.path.clone(),
126                size: u64::try_from(size).map_err(|_| DbError::DamagedDatabase)?,
127                modified_at: crate::user_file::decode_time(&modified_at)?,
128            };
129            crate::user_file::forget(db, copy.file)?;
130            crate::user_file::record(db, copy.file, &original, &copy.hashes)?;
131            crate::database::file_database::copies::keep(db, path)?;
132        }
133        Ok(())
134    }
135}