1#[cfg(windows)]
15use super::atomic_file::{create_new, rename_new};
16use super::atomic_file::{remove, sync_file};
17#[cfg(unix)]
18use super::flush_batch::BatchFolder;
19use super::observed_file::observe_metadata;
20use super::relative_path::local_name;
21use super::{FileError, FlushBatch, ObservationError, ObservedFile, RelativePath};
22use crate::task::BugSinkRef;
23use std::collections::BTreeMap;
24use std::fs;
25use std::io::{self, Write};
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28use std::time::SystemTime;
29
30#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct CopyPlan {
35 pub folders: Vec<PathBuf>,
37 pub files: Vec<PathBuf>,
39}
40
41#[derive(Debug, thiserror::Error)]
43pub enum CopyPathError {
44 #[error("{} is not a folder", .0.display())]
47 NotAFolder(PathBuf),
48 #[error("{} is a symbolic link", .0.display())]
50 Link(PathBuf),
51 #[error("{} is taken", .0.display())]
53 Exists(PathBuf),
54 #[error("{path} and {other} name one path")]
61 Clash {
62 path: RelativePath,
64 other: RelativePath,
66 },
67 #[error("{} is not valid Unicode", .0.display())]
69 NotUnicode(PathBuf),
70 #[error(transparent)]
72 File(FileError),
73 #[error(transparent)]
76 Bug(#[from] crate::task::Bug),
77}
78
79impl From<FileError> for CopyPathError {
80 fn from(error: FileError) -> Self {
81 match error {
82 FileError::Bug(bug) => Self::Bug(bug),
83 error => Self::File(error),
84 }
85 }
86}
87
88impl crate::task::CarriesBug for CopyPathError {
89 fn bug(&self) -> Option<&crate::task::Bug> {
90 match self {
91 Self::Bug(bug) => Some(bug),
92 Self::File(error) => error.bug(),
93 _ => None,
94 }
95 }
96}
97
98pub struct CopyFolder {
100 path: PathBuf,
101 #[cfg(unix)]
102 handle: Arc<rustix::fd::OwnedFd>,
103 bugs: BugSinkRef,
105}
106
107impl CopyFolder {
108 pub async fn open(folder: PathBuf, bugs: BugSinkRef) -> Result<Self, CopyPathError> {
113 let opened = bugs.clone();
114 blocking("open folder", &opened, move || {
115 if !folder.is_absolute() {
116 return Err(CopyPathError::NotAFolder(folder));
117 }
118 if folder.to_str().is_none() {
119 return Err(CopyPathError::NotUnicode(folder));
120 }
121 match fs::metadata(&folder) {
122 Ok(metadata) if metadata.is_dir() => {}
123 Ok(_) => return Err(CopyPathError::NotAFolder(folder)),
124 Err(error) if error.kind() == io::ErrorKind::NotFound => {
125 return Err(CopyPathError::NotAFolder(folder))
126 }
127 Err(source) => return Err(FileError::at("inspect folder", &folder, source).into()),
128 }
129 #[cfg(unix)]
130 {
131 use rustix::fs::{Mode, OFlags};
132 let handle = rustix::fs::open(
133 &folder,
134 OFlags::DIRECTORY | OFlags::RDONLY | OFlags::CLOEXEC,
135 Mode::empty(),
136 )
137 .map_err(|errno| FileError::at("open folder", &folder, errno.into()))?;
138 Ok(Self { path: folder, handle: Arc::new(handle), bugs })
139 }
140 #[cfg(windows)]
141 Ok(Self { path: folder, bugs })
142 })
143 .await
144 }
145
146 pub async fn plan(&self, paths: Vec<RelativePath>) -> Result<CopyPlan, CopyPathError> {
150 let folder = self.path.clone();
151 blocking("plan copies", &self.bugs, move || plan(&folder, &paths)).await
152 }
153
154 pub async fn create_folder(&self, path: PathBuf) -> Result<bool, FileError> {
158 let folder = self.clone_handle();
159 blocking("make folder", &self.bugs, move || folder.make_folder(&path)).await
160 }
161
162 pub async fn create_copy(&self, temporary: PathBuf) -> Result<CopyWriter, FileError> {
164 let folder = self.clone_handle();
165 blocking("create copy", &self.bugs, move || folder.make_copy(temporary)).await
166 }
167
168 pub async fn observe(&self, path: &Path) -> Result<ObservedFile, ObservationError> {
172 self.names(path).map_err(|source| FileError::at("observe copy", path, source))?;
173 observe_metadata(path).await
174 }
175
176 fn clone_handle(&self) -> CopyFolder {
177 Self {
178 path: self.path.clone(),
179 #[cfg(unix)]
180 handle: self.handle.clone(),
181 bugs: self.bugs.clone(),
182 }
183 }
184
185 fn names<'a>(&self, path: &'a Path) -> io::Result<Vec<&'a std::ffi::OsStr>> {
187 let names: Vec<_> = path
188 .strip_prefix(&self.path)
189 .map_err(|_outside| io::Error::new(io::ErrorKind::InvalidInput, "outside the folder"))?
190 .iter()
191 .collect();
192 if names.is_empty() {
193 return Err(io::Error::new(io::ErrorKind::InvalidInput, "the folder itself"));
194 }
195 Ok(names)
196 }
197
198 #[cfg(unix)]
201 fn parent<'a>(
202 &self,
203 path: &'a Path,
204 ) -> io::Result<(Option<rustix::fd::OwnedFd>, &'a std::ffi::OsStr)> {
205 use rustix::fd::AsFd;
206 use rustix::fs::{Mode, OFlags};
207 let names = self.names(path)?;
208 let (last, folders) = names.split_last().expect("a path below the folder names something");
209 let mut opened: Option<rustix::fd::OwnedFd> = None;
210 for name in folders {
211 let at = opened.as_ref().map_or(self.handle.as_fd(), |fd| fd.as_fd());
212 opened = Some(rustix::fs::openat(
213 at,
214 *name,
215 OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::RDONLY | OFlags::CLOEXEC,
216 Mode::empty(),
217 )?);
218 }
219 Ok((opened, last))
220 }
221
222 #[cfg(unix)]
223 fn make_folder(&self, path: &Path) -> Result<bool, FileError> {
224 use rustix::fd::AsFd;
225 use rustix::fs::{AtFlags, FileType, Mode};
226 let at = |source: io::Error| FileError::at("create folder", path, source);
227 let (opened, name) = self.parent(path).map_err(at)?;
228 let parent = opened.as_ref().map_or(self.handle.as_fd(), |fd| fd.as_fd());
229 match rustix::fs::mkdirat(parent, name, Mode::from(0o777)) {
230 Ok(()) => {
231 rustix::fs::fsync(parent).map_err(|errno| at(errno.into()))?;
232 Ok(true)
233 }
234 Err(rustix::io::Errno::EXIST) => {
235 let stat = rustix::fs::statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
236 .map_err(|errno| at(errno.into()))?;
237 if FileType::from_raw_mode(stat.st_mode) == FileType::Directory {
238 Ok(false)
239 } else {
240 Err(at(rustix::io::Errno::EXIST.into()))
241 }
242 }
243 Err(errno) => Err(at(errno.into())),
244 }
245 }
246
247 #[cfg(unix)]
248 fn make_copy(&self, temporary: PathBuf) -> Result<CopyWriter, FileError> {
249 use rustix::fd::AsFd;
250 use rustix::fs::{Mode, OFlags};
251 let at = |source: io::Error| FileError::at("create copy", &temporary, source);
252 let (opened, name) = self.parent(&temporary).map_err(at)?;
253 let parent = match opened {
254 Some(opened) => opened,
255 None => rustix::io::fcntl_dupfd_cloexec(self.handle.as_fd(), 0)
256 .map_err(|errno| at(errno.into()))?,
257 };
258 let file = rustix::fs::openat(
259 parent.as_fd(),
260 name,
261 OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
262 Mode::from(0o666),
263 )
264 .map_err(|errno| at(errno.into()))?;
265 let name = name.to_owned();
266 Ok(CopyWriter {
267 file: Arc::new(tokio::sync::Mutex::new(fs::File::from(file))),
268 path: temporary,
269 parent: Arc::new(parent),
270 name,
271 bugs: self.bugs.clone(),
272 })
273 }
274
275 #[cfg(windows)]
278 fn check_below(&self, path: &Path) -> io::Result<()> {
279 let names = self.names(path)?;
280 let mut below = self.path.clone();
281 for name in &names[..names.len() - 1] {
282 below.push(name);
283 let metadata = fs::symlink_metadata(&below)?;
284 if metadata.file_type().is_symlink() || !metadata.is_dir() {
285 return Err(io::Error::new(io::ErrorKind::InvalidInput, "not a folder"));
286 }
287 }
288 Ok(())
289 }
290
291 #[cfg(windows)]
292 fn make_folder(&self, path: &Path) -> Result<bool, FileError> {
293 let at = |source: io::Error| FileError::at("create folder", path, source);
294 self.check_below(path).map_err(at)?;
295 match fs::create_dir(path) {
296 Ok(()) => Ok(true),
297 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
298 let metadata = fs::symlink_metadata(path).map_err(at)?;
299 if metadata.is_dir() && !metadata.file_type().is_symlink() {
300 Ok(false)
301 } else {
302 Err(at(error))
303 }
304 }
305 Err(error) => Err(at(error)),
306 }
307 }
308
309 #[cfg(windows)]
310 fn make_copy(&self, temporary: PathBuf) -> Result<CopyWriter, FileError> {
311 let at = |source: io::Error| FileError::at("create copy", &temporary, source);
312 self.check_below(&temporary).map_err(at)?;
313 let file = create_new(&temporary).map_err(at)?;
314 Ok(CopyWriter {
315 file: Arc::new(tokio::sync::Mutex::new(file)),
316 folder: self.clone_handle(),
317 path: temporary,
318 bugs: self.bugs.clone(),
319 })
320 }
321}
322
323fn plan(folder: &Path, paths: &[RelativePath]) -> Result<CopyPlan, CopyPathError> {
324 if !folder.is_absolute() {
325 return Err(CopyPathError::NotAFolder(folder.to_owned()));
326 }
327 if folder.to_str().is_none() {
328 return Err(CopyPathError::NotUnicode(folder.to_owned()));
329 }
330 match fs::metadata(folder) {
331 Ok(metadata) if metadata.is_dir() => {}
332 Ok(_) => return Err(CopyPathError::NotAFolder(folder.to_owned())),
333 Err(error) if error.kind() == io::ErrorKind::NotFound => {
334 return Err(CopyPathError::NotAFolder(folder.to_owned()))
335 }
336 Err(source) => return Err(FileError::at("inspect folder", folder, source).into()),
337 }
338 let mut plan = CopyPlan { folders: Vec::new(), files: Vec::new() };
339 let mut named: BTreeMap<Vec<String>, Named> = BTreeMap::new();
343 for relative in paths {
344 let names: Vec<&str> = relative.names().collect();
345 let (last, folders) = names.split_last().expect("a relative path names something");
346 let mut path = folder.to_path_buf();
347 let mut key = Vec::new();
348 for (index, name) in folders.iter().enumerate() {
349 let local = local_name(name);
350 key.push(one_name(&local));
351 path.push(local);
352 let here = relative.first(index + 1);
353 match named.get(&key) {
354 Some(Named::Folder(other)) if *other == here => continue,
355 Some(Named::Folder(other) | Named::File(other)) => {
356 return Err(CopyPathError::Clash { path: here, other: other.clone() })
357 }
358 None => {}
359 }
360 named.insert(key.clone(), Named::Folder(here));
361 match fs::symlink_metadata(&path) {
362 Ok(metadata) if metadata.file_type().is_symlink() => {
363 return Err(CopyPathError::Link(path))
364 }
365 Ok(metadata) if metadata.is_dir() => {}
366 Ok(_) => return Err(CopyPathError::NotAFolder(path)),
367 Err(error) if error.kind() == io::ErrorKind::NotFound => {
368 plan.folders.push(path.clone());
369 }
370 Err(source) => return Err(FileError::at("inspect folder", &path, source).into()),
371 }
372 }
373 let local = local_name(last);
374 key.push(one_name(&local));
375 path.push(local);
376 if let Some(Named::Folder(other) | Named::File(other)) = named.get(&key) {
377 return Err(CopyPathError::Clash { path: relative.clone(), other: other.clone() });
378 }
379 named.insert(key, Named::File(relative.clone()));
380 match fs::symlink_metadata(&path) {
381 Ok(_) => return Err(CopyPathError::Exists(path)),
382 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
383 Err(source) => return Err(FileError::at("inspect file", &path, source).into()),
384 }
385 plan.files.push(path);
386 }
387 Ok(plan)
388}
389
390enum Named {
393 Folder(RelativePath),
394 File(RelativePath),
395}
396
397fn one_name(name: &str) -> String {
403 let decompose = icu_normalizer::DecomposingNormalizerBorrowed::new_nfd();
404 let folded =
405 icu_casemap::CaseMapperBorrowed::new().fold_string(&decompose.normalize(name)).into_owned();
406 decompose.normalize(&folded).into_owned()
407}
408
409pub struct CopyWriter {
412 file: Arc<tokio::sync::Mutex<fs::File>>,
413 path: PathBuf,
414 #[cfg(unix)]
416 parent: Arc<rustix::fd::OwnedFd>,
417 #[cfg(unix)]
418 name: std::ffi::OsString,
419 #[cfg(windows)]
420 folder: CopyFolder,
421 bugs: BugSinkRef,
423}
424
425impl CopyWriter {
426 pub async fn append(&mut self, bytes: &[u8]) -> Result<(), FileError> {
428 let bytes = bytes.to_vec();
429 self.run("write copy", move |file| file.write_all(&bytes)).await
430 }
431
432 pub async fn publish(self, path: &Path, batch: &mut FlushBatch) -> Result<(), FileError> {
437 self.run("sync copy", |file| sync_file(file)).await?;
438 let to = path.to_owned();
439 #[cfg(unix)]
440 {
441 let (parent, from) = (self.parent.clone(), self.name.clone());
442 let folder = blocking("rename copy", &self.bugs, move || {
443 use rustix::fs::RenameFlags;
444 let (name, folder) = match (to.file_name(), to.parent()) {
445 (Some(name), Some(folder)) => (name, folder),
446 _ => {
447 return Err(FileError::at(
448 "rename copy",
449 &to,
450 io::Error::new(io::ErrorKind::InvalidInput, "file name required"),
451 ))
452 }
453 };
454 rustix::fs::renameat_with(&*parent, &from, &*parent, name, RenameFlags::NOREPLACE)
455 .map_err(|errno| FileError::at("rename copy", &to, errno.into()))?;
456 BatchFolder::held(folder.to_owned(), parent, None)
457 .map_err(|source| FileError::at("inspect copy folder", &to, source))
458 })
459 .await?;
460 batch.add(folder);
461 Ok(())
462 }
463 #[cfg(windows)]
464 {
465 let (folder, from) = (self.folder.clone_handle(), self.path.clone());
466 blocking("rename copy", &self.bugs, move || {
467 folder
468 .check_below(&to)
469 .map_err(|source| FileError::at("rename copy", &to, source))?;
470 rename_new(&from, &to).map_err(|source| FileError::at("rename copy", &to, source))
471 })
472 .await?;
473 batch.add();
474 Ok(())
475 }
476 }
477
478 async fn run(
479 &self,
480 operation: &'static str,
481 run: impl FnOnce(&mut fs::File) -> io::Result<()> + Send + 'static,
482 ) -> Result<(), FileError> {
483 let mut file = self.file.clone().lock_owned().await;
485 let path = self.path.clone();
486 blocking(operation, &self.bugs, move || {
487 run(&mut file).map_err(|source| FileError::at(operation, &path, source))
488 })
489 .await
490 }
491}
492
493#[derive(Debug)]
498pub struct RecordedCopy {
499 path: PathBuf,
500 placed: Option<(u64, SystemTime)>,
501}
502
503impl RecordedCopy {
504 pub fn writing(temporary: PathBuf) -> Self {
506 Self { path: temporary, placed: None }
507 }
508
509 pub fn placed(path: PathBuf, size: u64, modified_at: SystemTime) -> Self {
511 Self { path, placed: Some((size, modified_at)) }
512 }
513
514 pub fn remove(&self) -> Result<(), FileError> {
518 let Some((size, modified_at)) = self.placed else {
519 return remove(&self.path);
520 };
521 let path = self.path.as_path();
522 let metadata = match fs::symlink_metadata(path) {
523 Ok(metadata) => metadata,
524 Err(error) if error.kind() == io::ErrorKind::NotFound => {
525 tracing::debug!(path = %path.display(), "copy is already absent");
526 return Ok(());
527 }
528 Err(source) => return Err(FileError::at("inspect copy", path, source)),
529 };
530 let modified = metadata
531 .modified()
532 .map_err(|source| FileError::at("read modification time", path, source))?;
533 if !metadata.is_file() || metadata.len() != size || modified != modified_at {
534 tracing::debug!(path = %path.display(), "leaving a copy the person changed");
535 return Ok(());
536 }
537 remove(path)
538 }
539}
540
541#[derive(Debug)]
544pub struct MadeFolder {
545 path: PathBuf,
546}
547
548impl MadeFolder {
549 pub fn at(path: PathBuf) -> Self {
551 Self { path }
552 }
553
554 pub fn remove(&self) -> Result<(), FileError> {
557 super::atomic_file::remove_empty_folder(&self.path)
558 }
559}
560
561async fn blocking<T, E>(
566 operation: &'static str,
567 bugs: &BugSinkRef,
568 run: impl FnOnce() -> Result<T, E> + Send + 'static,
569) -> Result<T, E>
570where
571 T: Send + 'static,
572 E: From<FileError> + crate::task::CarriesBug + Send + 'static,
573{
574 crate::task::blocking_completing(operation, bugs.clone(), run).await.map_err(FileError::from)?
575}
576
577#[cfg(test)]
578#[path = "folder_copy_tests.rs"]
579mod tests;