1use std::io::Write as _;
2use std::path::Path;
3use std::{fs, process};
4
5use super::entry::FileEntry;
6use super::error::IoResultExt as _;
7use super::path::{RepositoryPath, ensure_no_symlink_parent, set_mode};
8use super::planner::{DirtyFiles, Operation, Plan, PlanOrigin, Precondition};
9use super::repository::Repository;
10use crate::{FilesError, PathError};
11
12impl Plan {
13 pub fn apply(&self, dirty: DirtyFiles) -> Result<(), FilesError> {
29 if self.origin != PlanOrigin::Repository {
30 return Err(FilesError::SnapshotPlan);
31 }
32 if !self.foreign_paths().is_empty() {
33 return Err(FilesError::Foreign {
34 paths: self.foreign_paths().to_vec(),
35 });
36 }
37 if dirty == DirtyFiles::Refuse
38 && let Some(path) = self.dirty_paths().first()
39 {
40 return Err(FilesError::Unsafe { path: path.clone() });
41 }
42 let root = &self.root;
43 for precondition in &self.preconditions {
44 match precondition {
45 Precondition::Missing(relative) => {
46 let path = root.join(relative.as_path());
47 ensure_no_symlink_parent(root, &path)?;
48 if path.symlink_metadata().is_ok() {
49 return Err(FilesError::Stale {
50 path: relative.clone(),
51 });
52 }
53 }
54 Precondition::Existing {
55 path: relative,
56 entry,
57 } => {
58 if Repository::current_entry(root, relative)?.as_ref() != Some(entry) {
59 return Err(FilesError::Stale {
60 path: relative.clone(),
61 });
62 }
63 }
64 }
65 }
66 for operation in self.operations() {
67 operation.apply(root)?;
68 }
69 Ok(())
70 }
71
72 pub fn stage(&self) -> Result<(), FilesError> {
81 Repository::open(&self.root)?.stage(self.operations())
82 }
83}
84impl Operation {
85 #[must_use]
87 pub const fn path(&self) -> &RepositoryPath {
88 match self {
89 Self::Create { path, entry: _ }
90 | Self::Replace { path, entry: _ }
91 | Self::Delete { path } => path,
92 }
93 }
94
95 fn apply(&self, root: &Path) -> Result<(), FilesError> {
96 let path = root.join(self.path().as_path());
97 ensure_no_symlink_parent(root, &path)?;
98 match self {
99 Self::Create { entry, path: _ } => {
100 if let Some(parent) = path.parent() {
101 fs::create_dir_all(parent).at_path(parent)?;
102 }
103 entry.write_new(&path)
104 }
105 Self::Replace { entry, path: _ } => {
106 let parent = path
107 .parent()
108 .ok_or_else(|| PathError::Outside(path.clone()))?;
109 let name = path
110 .file_name()
111 .ok_or_else(|| PathError::Outside(path.clone()))?
112 .to_string_lossy();
113 let temp = parent.join(format!(".{name}.ephemeris-{}", process::id()));
114 let result = (|| {
115 entry.write_new(&temp)?;
116 fs::rename(&temp, &path).at_path(&path)
117 })();
118 if result.is_err() {
119 drop(fs::remove_file(&temp));
120 }
121 result
122 }
123 Self::Delete { path: _ } => fs::remove_file(&path).at_path(path),
124 }
125 }
126}
127
128impl FileEntry {
129 fn write_new(&self, path: &Path) -> Result<(), FilesError> {
131 let mut file = fs::OpenOptions::new()
132 .write(true)
133 .create_new(true)
134 .open(path)
135 .at_path(path)?;
136 file.write_all(&self.content).at_path(path)?;
137 set_mode(path, self.mode)
138 }
139}
140
141#[cfg(test)]
142mod tests;