1use crate::files::Files;
8use crate::FileReadError;
9use coven_database::{
10 CopyReservation, DbError, DeviceOnlyCopy, FileHasher, FilePlace, FileRef, FolderCopy,
11 PreparedDeviceOnly,
12};
13use coven_foundation::{
14 files::{CopyFolder, CopyPathError, FileError, FlushBatch, Flushed, RelativePath},
15 id_source::FileId,
16 progress::{Progress, ProgressReport},
17};
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20use tokio::sync::watch;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum DeviceOnlyStep {
28 Downloading {
30 file: FileId,
32 before: u64,
34 all: u64,
36 },
37}
38
39#[derive(Debug, thiserror::Error)]
43pub enum DeviceOnlyError {
44 #[error("file {file} is named twice")]
46 RepeatedFile {
47 file: FileId,
49 },
50 #[error(transparent)]
52 Path(CopyPathError),
53 #[error(transparent)]
60 Database(DbError),
61 #[error("downloading file {file} failed: {error}")]
63 Download {
64 file: FileId,
66 #[source]
68 error: FileReadError,
69 },
70 #[error(transparent)]
72 Disk(FileError),
73 #[error("cancelled")]
75 Cancelled,
76 #[error("{error}; removing the copies failed: {cleanup}")]
79 Cleanup {
80 error: Box<DeviceOnlyError>,
82 #[source]
84 cleanup: DbError,
85 },
86 #[error(transparent)]
88 Bug(#[from] coven_foundation::task::Bug),
89}
90
91impl From<CopyPathError> for DeviceOnlyError {
92 fn from(error: CopyPathError) -> Self {
93 match error {
94 CopyPathError::Bug(bug) => Self::Bug(bug),
95 error => Self::Path(error),
96 }
97 }
98}
99
100impl From<DbError> for DeviceOnlyError {
101 fn from(error: DbError) -> Self {
102 match error {
103 DbError::Bug(bug) => Self::Bug(bug),
104 error => Self::Database(error),
105 }
106 }
107}
108
109impl From<FileError> for DeviceOnlyError {
110 fn from(error: FileError) -> Self {
111 match error {
112 FileError::Bug(bug) => Self::Bug(bug),
113 error => Self::Disk(error),
114 }
115 }
116}
117
118impl coven_foundation::task::CarriesBug for DeviceOnlyError {
119 fn bug(&self) -> Option<&coven_foundation::task::Bug> {
120 match self {
121 Self::Bug(bug) => Some(bug),
122 Self::Path(error) => error.bug(),
123 Self::Database(error) => error.bug(),
124 Self::Download { error, .. } => error.bug(),
125 Self::Disk(error) => error.bug(),
126 Self::Cleanup { error, cleanup } => error.bug().or_else(|| cleanup.bug()),
127 Self::RepeatedFile { .. } | Self::Cancelled => None,
128 }
129 }
130}
131
132impl Files {
133 pub async fn prepare_device_only(
147 &self,
148 folder: &Path,
149 files: Vec<(FileId, RelativePath)>,
150 cancel: &watch::Receiver<bool>,
151 progress: watch::Sender<Option<Progress<DeviceOnlyStep>>>,
152 ) -> Result<PreparedDeviceOnly, DeviceOnlyError> {
153 self.inner.check_open()?;
154 let mut ids = Vec::new();
155 let mut paths = Vec::new();
156 for (file, path) in files {
157 if ids.contains(&file) {
158 return Err(DeviceOnlyError::RepeatedFile { file });
159 }
160 ids.push(file);
161 paths.push(path);
162 }
163 let refs = self.inner.database.device_only_files(ids).await?;
165 let folder = Arc::new(
166 CopyFolder::open(folder.to_owned(), self.inner.database.bugs().clone()).await?,
167 );
168 let plan = folder.plan(paths).await?;
169 let copies: Vec<FolderCopy> = plan
170 .files
171 .iter()
172 .map(|path| FolderCopy {
173 path: path.clone(),
174 temporary: path
175 .with_file_name(format!(".coven-{}.download", self.inner.ids.new_id())),
176 folders: plan
177 .folders
178 .iter()
179 .filter(|folder| path.starts_with(folder))
180 .cloned()
181 .collect(),
182 })
183 .collect();
184 let reservation = self.inner.database.record_copies(copies.clone()).await?;
187 let mut cancel = cancel.clone();
188 let mut report = ProgressReport::new(progress, self.inner.clock.clone());
189 let downloaded = coven_foundation::task::caught(
192 "download files to make device-only",
193 download(
194 self,
195 &folder,
196 &reservation,
197 &plan.folders,
198 &refs,
199 &copies,
200 &mut cancel,
201 &mut report,
202 ),
203 )
204 .await;
205 let error = match downloaded {
206 Ok(Ok((switches, flushed))) => {
207 return Ok(self.inner.database.prepared_device_only(
208 reservation,
209 switches,
210 flushed,
211 )?)
212 }
213 Ok(Err(error)) => error,
214 Err(bug) => DeviceOnlyError::Bug(bug),
215 };
216 let made = copies.into_iter().map(|copy| copy.path).collect();
217 let abandoned = self.inner.database.abandon_copies(made).await;
218 drop(reservation);
219 match abandoned {
220 Ok(()) => Err(error),
221 Err(cleanup) => Err(DeviceOnlyError::Cleanup { error: Box::new(error), cleanup }),
222 }
223 }
224}
225
226#[expect(clippy::too_many_arguments, reason = "the call's parts, each its own")]
240async fn download(
241 handle: &Files,
242 folder: &Arc<CopyFolder>,
243 reservation: &CopyReservation,
244 folders: &[PathBuf],
245 files: &[FileRef],
246 copies: &[FolderCopy],
247 cancel: &mut watch::Receiver<bool>,
248 report: &mut ProgressReport<DeviceOnlyStep>,
249) -> Result<(Vec<DeviceOnlyCopy>, Flushed), DeviceOnlyError> {
250 let bugs = handle.inner.database.bugs().clone();
251 for made in folders {
252 check(cancel)?;
253 let (owner, folder, made) = (handle.inner.clone(), folder.clone(), made.clone());
254 step(reservation, "make a folder for copies", &bugs, async move {
255 if folder.create_folder(made.clone()).await? {
257 owner.database.made_folder(&made).await?;
258 }
259 Ok(())
260 })
261 .await?;
262 }
263 let mut switches = Vec::new();
264 let mut batch = FlushBatch::new(handle.inner.database.bugs().clone());
265 let all = files.iter().map(FileRef::size).sum();
266 let mut before = 0;
267 for (file, copy) in files.iter().zip(copies) {
268 check(cancel)?;
269 report.begin(DeviceOnlyStep::Downloading { file: file.id(), before, all }, file.size());
270 let mut done = 0;
271 let download = |error| DeviceOnlyError::Download { file: file.id(), error };
272 let FilePlace::Cloud { location, .. } = file.place() else {
273 return Err(DbError::NotInCloud { file: file.id() }.into());
274 };
275 let location = location.clone();
276 let stream = handle.open_file_stream(file).await.map_err(download)?;
277 let mut writer = {
278 let (folder, temporary) = (folder.clone(), copy.temporary.clone());
279 step(reservation, "create a copy", &bugs, async move {
280 Ok(folder.create_copy(temporary).await?)
281 })
282 .await?
283 };
284 let mut range = stream.read_range(0, file.size()).map_err(download)?;
288 let mut hashes = FileHasher::new();
289 loop {
290 let bytes = tokio::select! {
291 biased;
292 () = cancelled(cancel) => return Err(DeviceOnlyError::Cancelled),
293 bytes = range.next() => bytes.map_err(download)?,
294 };
295 let Some(bytes) = bytes else { break };
296 hashes.update(&bytes);
297 done += bytes.len() as u64;
298 writer = {
299 #[cfg(test)]
300 let owner = handle.inner.clone();
301 step(reservation, "append to a copy", &bugs, async move {
302 writer.append(&bytes).await?;
303 #[cfg(test)]
304 owner.pass_device_only_gate(DeviceOnlyPoint::Appended).await;
305 Ok(writer)
306 })
307 .await?
308 };
309 report.done(done);
310 }
311 before += file.size();
312 batch = {
313 let (owner, folder, path) = (handle.inner.clone(), folder.clone(), copy.path.clone());
314 step(reservation, "place a copy", &bugs, async move {
315 writer.publish(&path, &mut batch).await?;
316 #[cfg(test)]
317 owner.pass_device_only_gate(DeviceOnlyPoint::Renamed).await;
318 let observed = folder.observe(&path).await.map_err(DbError::from)?;
319 owner.database.place_copy(&observed).await?;
320 Ok(batch)
321 })
322 .await?
323 };
324 switches.push(DeviceOnlyCopy {
325 file: file.id(),
326 location,
327 path: copy.path.clone(),
328 hashes: hashes.finish(),
329 });
330 }
331 check(cancel)?;
332 Ok((switches, batch.flush().await?))
333}
334
335#[cfg(test)]
337#[derive(Clone, Copy, Debug, PartialEq, Eq)]
338pub(crate) enum DeviceOnlyPoint {
339 Appended,
341 Renamed,
343}
344
345#[cfg(test)]
348pub(crate) type DeviceOnlyGate =
349 (DeviceOnlyPoint, tokio::sync::oneshot::Sender<()>, tokio::sync::oneshot::Receiver<()>);
350
351impl crate::files::FilesInner {
352 #[cfg(test)]
354 async fn pass_device_only_gate(&self, point: DeviceOnlyPoint) {
355 let gate = {
356 let mut gate = coven_foundation::locks::lock_unpoisoned(&self.device_only_gate);
357 if gate.as_ref().is_some_and(|(at, _, _)| *at == point) {
358 gate.take()
359 } else {
360 None
361 }
362 };
363 if let Some((_, reached, resume)) = gate {
364 reached.send(()).ok();
365 resume.await.ok();
366 }
367 }
368}
369
370async fn step<T: Send + 'static>(
373 reservation: &CopyReservation,
374 operation: &'static str,
375 bugs: &coven_foundation::task::BugSinkRef,
376 work: impl std::future::Future<Output = Result<T, DeviceOnlyError>> + Send + 'static,
377) -> Result<T, DeviceOnlyError> {
378 reservation.run_step(operation, bugs.clone(), work).await.map_err(FileError::from)?
379}
380
381fn check(cancel: &watch::Receiver<bool>) -> Result<(), DeviceOnlyError> {
382 if *cancel.borrow() {
383 Err(DeviceOnlyError::Cancelled)
384 } else {
385 Ok(())
386 }
387}
388
389async fn cancelled(cancel: &mut watch::Receiver<bool>) {
391 loop {
392 if *cancel.borrow_and_update() {
393 return;
394 }
395 if cancel.changed().await.is_err() {
396 std::future::pending::<()>().await;
397 }
398 }
399}