1use super::{FileWrite, StagedSource};
6use crate::{
7 file_hashes::FileHashes,
8 file_location::{FileLocation, Place},
9 file_moves::{self, MoveState},
10 file_table::Record,
11 DbError, MoveId, Provenance,
12};
13use coven_foundation::{files::StagedName, id_source::FileId};
14use std::collections::BTreeSet;
15
16#[derive(Clone, Copy, Debug)]
18pub enum MoveFile {
19 User(FileId),
22 UserKeptHere(FileId),
26 App(FileId),
28 AppKeptHere(FileId),
31}
32
33pub(super) struct Moving {
35 id: MoveId,
36 uploads: Vec<MoveUpload>,
37}
38
39struct MoveUpload {
42 files: Vec<(FileId, bool)>,
43 size: u64,
44 source: StagedSource,
45 hashes: FileHashes,
46}
47
48impl FileWrite<'_> {
49 pub(crate) fn move_to_cloud(&self, files: Vec<MoveFile>) -> Result<MoveId, DbError> {
57 let db = self.database;
58 let mut uploads: Vec<MoveUpload> = Vec::new();
59 let mut named = BTreeSet::new();
60 for file in files {
61 let (id, keep) = match file {
62 MoveFile::User(id) | MoveFile::App(id) => (id, false),
63 MoveFile::UserKeptHere(id) | MoveFile::AppKeptHere(id) => (id, true),
64 };
65 if !named.insert(id) {
66 continue;
67 }
68 let record = Record::read(db, id)?.ok_or(DbError::FileAbsent { file: id })?;
69 match record.place {
70 Place::DeviceOnly(device) if device == self.files.device => {}
71 Place::DeviceOnly(_) => return Err(DbError::NotThisDevice { file: id }),
72 Place::Cloud(_) => return Err(DbError::NotDeviceOnlyHere { file: id }),
73 }
74 let recorded = self
77 .files
78 .moving
79 .borrow()
80 .iter()
81 .find(|moving| {
82 moving.uploads.iter().any(|upload| upload.files.iter().any(|(f, _)| *f == id))
83 })
84 .map(|moving| moving.id);
85 let holding = match recorded {
86 Some(of) => Some(of),
87 None => file_moves::of_file(db, id)?,
88 };
89 if let Some(of) = holding {
90 return Err(DbError::FileInMove { file: id, id: of });
91 }
92 let upload = match file {
93 MoveFile::User(_) | MoveFile::UserKeptHere(_) => {
94 let wrong_kind = || DbError::NamespaceKind {
95 namespace: record.namespace.clone(),
96 kind: Provenance::AppProvided,
97 };
98 let original = crate::user_file::read(db, id)?.ok_or_else(wrong_kind)?;
99 let hashes = crate::user_file::hashes(db, id)?.ok_or_else(wrong_kind)?;
100 MoveUpload {
101 files: vec![(id, keep)],
102 size: original.size,
103 source: StagedSource::Original(original),
104 hashes,
105 }
106 }
107 MoveFile::App(_) | MoveFile::AppKeptHere(_) => {
108 let (name, hashes) = copy(db, id)?.ok_or_else(|| DbError::NamespaceKind {
109 namespace: record.namespace.clone(),
110 kind: Provenance::UserProvided,
111 })?;
112 MoveUpload {
113 files: vec![(id, keep)],
114 size: record.size,
115 source: StagedSource::Copy(name),
116 hashes,
117 }
118 }
119 };
120 match uploads.iter_mut().find(|queued| same_bytes(queued, &upload)) {
122 Some(queued) => queued.files.push((id, keep)),
123 None => uploads.push(upload),
124 }
125 }
126 let id = file_moves::create(db)?;
127 self.files.moving.borrow_mut().push(Moving { id, uploads });
128 Ok(id)
129 }
130
131 pub(crate) fn cancel_move(&self, id: MoveId) -> Result<(), DbError> {
137 let db = self.database;
138 match file_moves::state(db, id)? {
139 None => Err(DbError::MoveUnknown { id }),
140 Some(MoveState::Finished) => Err(DbError::MoveFinished { id }),
141 Some(MoveState::Cancelled) => Ok(()),
142 Some(MoveState::Moving | MoveState::Failed) if file_moves::bound(db, id)? => {
143 Err(DbError::MoveBound { id })
144 }
145 Some(MoveState::Moving | MoveState::Failed) => {
146 self.files.moving.borrow_mut().retain(|moving| moving.id != id);
147 file_moves::end_unswitched(db, id, MoveState::Cancelled)?;
148 crate::pending::clear_move(db, id)
149 }
150 }
151 }
152
153 pub(super) fn queue_moves(&self, now: std::time::SystemTime) -> Result<(), DbError> {
157 let db = self.database;
158 for moving in self.files.moving.take() {
159 let mut queued = false;
160 for upload in moving.uploads {
161 let mut files = Vec::new();
162 for (file, keep) in upload.files {
163 if Record::read(db, file)?
164 .is_some_and(|record| record.place == Place::DeviceOnly(self.files.device))
165 {
166 files.push((file, keep));
167 }
168 }
169 if files.is_empty() {
170 continue;
171 }
172 let location = FileLocation::new(self.files.device, self.files.ids)?;
173 crate::file_queue::enqueue(
174 db,
175 crate::file_queue::Queued {
176 files: &files,
177 size: upload.size,
178 source: &upload.source,
179 hashes: &upload.hashes,
180 of: moving.id,
181 holds: None,
182 },
183 &location,
184 now,
185 )?;
186 queued = true;
187 }
188 if !queued {
189 file_moves::finish(db, moving.id)?;
190 }
191 }
192 Ok(())
193 }
194}
195
196fn same_bytes(a: &MoveUpload, b: &MoveUpload) -> bool {
201 match (&a.source, &b.source) {
202 (StagedSource::Original(original), StagedSource::Original(other)) => {
203 original == other && a.hashes.content == b.hashes.content
204 }
205 (StagedSource::Copy(a), StagedSource::Copy(b)) => a == b,
206 _ => false,
207 }
208}
209
210fn copy(
212 db: &crate::sqlite::DatabaseConnection,
213 file: FileId,
214) -> Result<Option<(StagedName, FileHashes)>, DbError> {
215 let Some((path, content)) = db
216 .query("SELECT path,hash FROM _coven_device_files WHERE file=?1", [file], |r| {
217 Ok((r.get::<_, String>(0)?, r.get::<_, Vec<u8>>(1)?))
218 })?
219 .pop()
220 else {
221 return Ok(None);
222 };
223 let chunks = db.query(
224 "SELECT hash FROM _coven_copy_chunks WHERE path=?1 ORDER BY chunk",
225 [&path],
226 |r| r.get::<_, Vec<u8>>(0),
227 )?;
228 Ok(Some((
229 StagedName::parse(&path).ok_or(DbError::DamagedDatabase)?,
230 FileHashes::stored(content, chunks)?,
231 )))
232}