Skip to main content

coven_database/
file_location.rs

1//! Where a file is, as its record's `coordinates` cell holds it (format.md
2//! D12): stored at a fixed location with its key, or only on one device.
3
4use crate::DbError;
5use coven_crypto::{FileKey, SecretText};
6use coven_format::{
7    coordinates::Coordinates, file_reference::FileReference, path::ObjectPath, value::Value,
8};
9use coven_foundation::id_source::{DeviceId, IdSource, ObjectId, StoreId};
10
11/// A stored file's fixed location: the uploader, the stored object's id and
12/// the file's key. Equality compares the whole text, so another object or
13/// key is another location. Diagnostics never show the key.
14#[derive(Clone, Debug)]
15pub struct FileLocation {
16    uploader: DeviceId,
17    object: ObjectId,
18    text: SecretText,
19}
20
21impl FileLocation {
22    /// The device that uploaded the bytes, which owns deleting them.
23    pub fn uploader(&self) -> DeviceId {
24        self.uploader
25    }
26
27    /// The stored object's id, the last part of its path
28    /// `<store>/files/<uploader>/<object>`.
29    pub fn object(&self) -> ObjectId {
30        self.object
31    }
32
33    /// The stored object's path in `store`.
34    pub fn path(&self, store: StoreId) -> ObjectPath {
35        ObjectPath::file(store, self.uploader, self.object)
36    }
37
38    /// A new location for bytes this device uploads: a random object id and
39    /// a new key (ยง17.3).
40    pub(crate) fn new(device: DeviceId, ids: &dyn IdSource) -> Result<Self, DbError> {
41        Self::from_reference(&FileReference {
42            device,
43            object: ObjectId(ids.new_id()),
44            key: FileKey::generate().map_err(DbError::FileKey)?,
45        })
46    }
47
48    fn from_reference(reference: &FileReference) -> Result<Self, DbError> {
49        Ok(Self { uploader: reference.device, object: reference.object, text: reference.encode() })
50    }
51
52    #[expect(
53        clippy::map_err_ignore,
54        reason = "a stored value that won't decode is DamagedDatabase"
55    )]
56    pub(crate) fn decode(text: &str) -> Result<Self, DbError> {
57        let reference = FileReference::decode(text).map_err(|_| DbError::DamagedDatabase)?;
58        Self::from_reference(&reference)
59    }
60
61    pub(crate) fn text(&self) -> &str {
62        self.text.as_str()
63    }
64}
65
66impl PartialEq for FileLocation {
67    fn eq(&self, other: &Self) -> bool {
68        self.text.as_str() == other.text.as_str()
69    }
70}
71
72/// Crate-to-crate access to a stored file's key, for coven's owners; not
73/// re-exported by the app API.
74pub trait FileLocationKey {
75    /// The uploader, object id and key, which encrypt and decrypt the bytes.
76    fn reference(&self) -> Result<FileReference, DbError>;
77}
78
79impl FileLocationKey for FileLocation {
80    fn reference(&self) -> Result<FileReference, DbError> {
81        Ok(FileReference::decode(self.text.as_str())?)
82    }
83}
84
85/// A file's coordinates as coven keeps them.
86#[derive(Clone, Debug, PartialEq)]
87pub(crate) enum Place {
88    /// Stored at its location.
89    Cloud(FileLocation),
90    /// Only on one device, which may be this one.
91    DeviceOnly(DeviceId),
92}
93
94impl Place {
95    /// Read a `coordinates` cell. Anything but one canonical form is damage.
96    #[expect(
97        clippy::map_err_ignore,
98        reason = "a stored value that won't decode is DamagedDatabase"
99    )]
100    pub(crate) fn decode(value: &Value) -> Result<Self, DbError> {
101        let Value::Text(text) = value else {
102            return Err(DbError::DamagedDatabase);
103        };
104        match Coordinates::decode(text).map_err(|_| DbError::DamagedDatabase)? {
105            Coordinates::Cloud(reference) => {
106                Ok(Self::Cloud(FileLocation::from_reference(&reference)?))
107            }
108            Coordinates::DeviceOnly(device) => Ok(Self::DeviceOnly(device)),
109        }
110    }
111
112    /// The `coordinates` cell's value.
113    pub(crate) fn value(&self) -> Value {
114        Value::Text(match self {
115            Self::Cloud(location) => location.text().to_owned(),
116            Self::DeviceOnly(device) => {
117                Coordinates::DeviceOnly(*device).encode().as_str().to_owned()
118            }
119        })
120    }
121}
122
123#[cfg(test)]
124#[path = "file_location_tests.rs"]
125mod tests;