Skip to main content

coven_sync/
file_status.rs

1//! A file's availability: for a file this device uploads, from local facts
2//! alone; for a file only on another device, from its record; for a stored
3//! file, from one storage observation of its path. Never written to a record
4//! (§17.6).
5
6use super::FilesMode;
7use crate::FileSourceFailure;
8use crate::{
9    files::{file_failure::upload_subject, Files, FilesInner},
10    FileMissingReason, FileReadError, PendingReason, PendingRecord,
11};
12use coven_database::{DbError, FileLocation, FilePlace, FileRef};
13use coven_format::file::FileHeader;
14use coven_foundation::id_source::DeviceId;
15use coven_foundation::locks::read_unpoisoned;
16use coven_storage::StoredObject;
17
18/// A file's current availability (E8).
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum FileStatus {
21    /// Storage holds the complete file at its fixed path.
22    Available,
23    /// This device is uploading it; until then it is on this device only.
24    Uploading,
25    /// It is on one device only, with no upload: another device, which may
26    /// be gone, and then nothing here can read it, or this one, which reads
27    /// it from its bytes here. Nothing waits for it (§17.3).
28    DeviceOnly {
29        /// The device that has it.
30        device: DeviceId,
31    },
32    /// Unreadable from storage, with the reason.
33    Missing {
34        /// The device that uploaded, or is uploading, it.
35        device: DeviceId,
36        /// Why.
37        reason: FileMissingReason,
38    },
39}
40
41/// A file this device uploads: its upload, where its bytes go, its move,
42/// and its source's recorded failure.
43pub(crate) struct Uploading {
44    pub(crate) upload: i64,
45    pub(crate) location: FileLocation,
46    pub(crate) of: coven_database::MoveId,
47    pub(crate) failure: Option<FileSourceFailure>,
48}
49
50/// What this device knows locally about a file: `None` for a stored one,
51/// `Some(report)` for one it uploads, with its source failure if one is
52/// recorded (ST `Files.Local`).
53pub(crate) type Local = Option<Option<FileSourceFailure>>;
54
55/// §17.6, ST `Files.status`. A file this device uploads is judged locally.
56/// For a stored file, presence wins; a failed observation is never absence,
57/// and a confirmed absence is lost, whoever uploaded it.
58pub(crate) fn status<E>(
59    uploader: DeviceId,
60    remote: impl FnOnce() -> Result<bool, E>,
61    here: Local,
62) -> Result<FileStatus, E> {
63    match here {
64        Some(None) => Ok(FileStatus::Uploading),
65        Some(Some(failure)) => {
66            Ok(FileStatus::Missing { device: uploader, reason: FileMissingReason::Source(failure) })
67        }
68        None => Ok(if remote()? {
69            FileStatus::Available
70        } else {
71            FileStatus::Missing { device: uploader, reason: FileMissingReason::Lost }
72        }),
73    }
74}
75
76impl Files {
77    /// The file as its record now stands: a file this device uploads is
78    /// judged from local facts with no storage request, one only on another
79    /// device from its record, and a stored one by one fresh single-object
80    /// status call for its fixed path. Network failures and denied access are
81    /// errors, not absence.
82    pub async fn file_status(&self, file: &FileRef) -> Result<FileStatus, FileReadError> {
83        self.inner.check_open()?;
84        let file = self.inner.current(file).await?;
85        let location = match file.place() {
86            FilePlace::DeviceOnly { device } => {
87                return match self.inner.local(&file).await? {
88                    Some(here) => status::<FileReadError>(
89                        here.location.uploader(),
90                        || Ok(false),
91                        Some(here.failure),
92                    ),
93                    None => match self.inner.unreadable(&file, device).await {
94                        FileReadError::DeviceOnly { .. } => Ok(FileStatus::DeviceOnly { device }),
95                        error => Err(error),
96                    },
97                };
98            }
99            FilePlace::Cloud { location, .. } => location,
100        };
101        let storage =
102            read_unpoisoned(&self.inner.storage).clone().ok_or(FileReadError::NoStorage)?;
103        let path = location.path(self.inner.directory.id());
104        let remote = match storage.status(&path).await? {
105            Some(object) => complete(&file, &object).map(|()| true),
106            None => Ok(false),
107        };
108        status(location.uploader(), || remote, None)
109    }
110}
111
112impl<M: FilesMode> FilesInner<M> {
113    /// The file as its record now stands. A file never changes its bytes, so
114    /// a reference taken earlier, while the file uploaded, reads the stored
115    /// file once it is stored; one whose record went reads as it was named.
116    pub(crate) async fn current(&self, file: &FileRef) -> Result<FileRef, FileReadError> {
117        match self.database.file_ref(file.id()).await {
118            Ok(current) => Ok(current),
119            Err(DbError::FileAbsent { .. }) => Ok(file.clone()),
120            Err(error) => Err(error.into()),
121        }
122    }
123
124    /// Why a file on one device only can't be read here: this device has no
125    /// record of it any more, or it is only on that device.
126    pub(crate) async fn unreadable(&self, file: &FileRef, device: DeviceId) -> FileReadError {
127        match self.database.file_ref(file.id()).await {
128            Err(error @ DbError::FileAbsent { .. }) => error.into(),
129            _ => FileReadError::DeviceOnly { file: file.id(), device },
130        }
131    }
132
133    /// The upload of a file this device uploads, with the source failure
134    /// recorded about it: its original's, on its move's record, or coven's
135    /// copy's, on its upload's; `None` for any other file.
136    /// A read-only handle keeps no file records, so it knows no upload.
137    pub(crate) async fn local(&self, file: &FileRef) -> Result<Option<Uploading>, FileReadError> {
138        let Some(records) = self.database.records() else {
139            return Ok(None);
140        };
141        let evidence = records
142            .local_evidence::<PendingRecord>(file, upload_subject, |id| {
143                crate::PendingSubject::Move { id }
144            })
145            .await?;
146        Ok(evidence.map(|evidence| {
147            let original = evidence.moving.and_then(|record| match record.reason {
148                PendingReason::Original { path, failure }
149                    if evidence.original.as_ref() == Some(&path) =>
150                {
151                    Some(failure)
152                }
153                _ => None,
154            });
155            let copy = evidence.record.and_then(|record| match record.reason {
156                PendingReason::FileUnavailable(FileMissingReason::Source(failure)) => Some(failure),
157                _ => None,
158            });
159            Uploading {
160                upload: evidence.upload,
161                location: evidence.location,
162                of: evidence.of,
163                failure: original.or(copy),
164            }
165        }))
166    }
167
168    /// The typed read failure for stored bytes storage confirms are absent
169    /// at their path: lost (§17.6).
170    pub(crate) fn absence_error(&self, file: &FileRef, location: &FileLocation) -> FileReadError {
171        FileReadError::Missing { id: file.id().to_string(), device: location.uploader() }
172    }
173}
174
175/// An object at the fixed path counts as the file only at the file's complete
176/// encrypted size; anything else there is damage, not absence. Status and every
177/// ranged read check it, so they agree (ST `read_agrees_with_status`).
178#[expect(
179    clippy::map_err_ignore,
180    reason = "a size whose encrypted layout overflows fails the integrity check"
181)]
182pub(crate) fn complete(file: &FileRef, object: &StoredObject) -> Result<(), FileReadError> {
183    let expected = FileHeader::new(file.size())
184        .encrypted_size()
185        .map_err(|_| FileReadError::Integrity { id: file.id().to_string(), cause: None })?;
186    if object.size == expected {
187        Ok(())
188    } else {
189        Err(FileReadError::Integrity { id: file.id().to_string(), cause: None })
190    }
191}