coven_sync/
file_status.rs1use 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#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum FileStatus {
21 Available,
23 Uploading,
25 DeviceOnly {
29 device: DeviceId,
31 },
32 Missing {
34 device: DeviceId,
36 reason: FileMissingReason,
38 },
39}
40
41pub(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
50pub(crate) type Local = Option<Option<FileSourceFailure>>;
54
55pub(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 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 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 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 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 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#[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}