1mod error;
2#[cfg(feature = "operations")]
3mod handle;
4#[cfg(any(feature = "operations", feature = "watch"))]
10mod operations;
11#[cfg(feature = "operations")]
15mod paths;
16#[cfg(feature = "operations")]
17mod planner;
18#[cfg(feature = "operations")]
19mod profiler;
20#[cfg(feature = "operations")]
21mod progress;
22#[cfg(feature = "watch")]
23mod watch_event;
24#[cfg(feature = "watch")]
25mod watch_handle;
26
27pub use error::{Error, Result};
28#[cfg(feature = "operations")]
29pub use handle::Handle;
30#[cfg(feature = "operations")]
31pub use progress::Progress;
32#[cfg(feature = "watch")]
33pub use watch_event::{WatchEvent, WatchEventKind};
34#[cfg(feature = "watch")]
35pub use watch_handle::WatchHandle;
36
37#[cfg(feature = "operations")]
38pub use operations::CopyBuilder;
39#[cfg(feature = "compress")]
40pub use operations::{CompressBuilder, CompressFormat};
41#[cfg(feature = "operations")]
42pub use operations::MoveBuilder;
43#[cfg(feature = "sync")]
44pub use operations::{SyncBuilder, SyncOutcome};
45#[cfg(feature = "watch")]
46pub use operations::WatchBuilder;
47#[cfg(feature = "sync")]
48pub use operations::diff::DiffStrategy;
49
50#[cfg(feature = "operations")]
58pub use planner::{ErrorStrategy, OperationOutcome, SortOrder, StopReason};
59#[cfg(feature = "operations")]
60pub use profiler::Entry;
61
62pub struct FileEngine;
63
64impl Default for FileEngine {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl FileEngine {
71 pub fn new() -> Self {
72 FileEngine
73 }
74
75 #[cfg(feature = "operations")]
76 pub fn copy(
77 &self,
78 source: impl Into<std::path::PathBuf>,
79 dest: impl Into<std::path::PathBuf>,
80 ) -> CopyBuilder {
81 CopyBuilder::new(source, dest)
82 }
83
84 #[cfg(feature = "operations")]
85 pub fn move_path(
86 &self,
87 source: impl Into<std::path::PathBuf>,
88 dest: impl Into<std::path::PathBuf>,
89 ) -> MoveBuilder {
90 MoveBuilder::new(source, dest)
91 }
92
93 #[cfg(feature = "watch")]
94 pub fn watch(&self, path: impl Into<std::path::PathBuf>) -> WatchBuilder {
95 WatchBuilder::new(path)
96 }
97
98 #[cfg(feature = "sync")]
99 pub fn sync(
100 &self,
101 source: impl Into<std::path::PathBuf>,
102 dest: impl Into<std::path::PathBuf>,
103 ) -> SyncBuilder {
104 SyncBuilder::new(source, dest)
105 }
106
107 #[cfg(feature = "compress")]
108 pub fn compress(
109 &self,
110 source: impl Into<std::path::PathBuf>,
111 dest: impl Into<std::path::PathBuf>,
112 ) -> CompressBuilder {
113 CompressBuilder::new(source, dest)
114 }
115}
116
117#[cfg(all(test, feature = "operations", feature = "sync", feature = "compress"))]
118mod tests {
119 use std::fs;
120
121 use tempfile::tempdir;
122 use tokio_stream::StreamExt;
123
124 use super::*;
125
126 fn assert_well_formed(events: &[Progress], expected_entries: usize) {
132 let started_at = events.iter().position(|e| matches!(e, Progress::Started { .. }));
133 assert!(started_at.is_some(), "expected a Started event");
134
135 if let Some(first_entry_started) = events.iter().position(|e| matches!(e, Progress::EntryStarted { .. })) {
136 assert!(started_at.unwrap() < first_entry_started, "Started must come before any EntryStarted");
137 }
138
139 let entries_total = events
140 .iter()
141 .find_map(|e| match e {
142 Progress::Started { entries_total, .. } => Some(*entries_total),
143 _ => None,
144 })
145 .unwrap();
146 assert_eq!(entries_total, expected_entries);
147
148 let terminal_count = events
149 .iter()
150 .filter(|e| matches!(e, Progress::EntryCompleted { .. } | Progress::EntryFailed { .. }))
151 .count();
152 assert_eq!(terminal_count, expected_entries);
153 }
154
155 #[tokio::test]
156 async fn copy_end_to_end_through_the_public_api() {
157 let src_dir = tempdir().unwrap();
158 let dest_dir = tempdir().unwrap();
159 fs::write(src_dir.path().join("a.txt"), b"hello").unwrap();
160
161 let engine = FileEngine::new();
162 let mut handle = engine.copy(src_dir.path(), dest_dir.path()).start().unwrap();
163
164 let mut events = Vec::new();
165 while let Some(event) = handle.progress().next().await {
166 events.push(event);
167 }
168
169 let outcome = handle.await.unwrap();
170
171 assert_eq!(outcome.succeeded.len(), 1);
172 assert_eq!(fs::read(dest_dir.path().join("a.txt")).unwrap(), b"hello");
173 assert_well_formed(&events, 1);
174 }
175
176 #[tokio::test]
177 async fn move_end_to_end_through_the_public_api() {
178 let root = tempdir().unwrap();
182 let src_file = root.path().join("a.txt");
183 let dest_file = root.path().join("dst.txt");
184 fs::write(&src_file, b"hello").unwrap();
185
186 let engine = FileEngine::new();
187 let handle = engine.move_path(root.path().join("a.txt"), dest_file.clone()).start().unwrap();
188 let outcome = handle.await.unwrap();
189
190 assert!(outcome.succeeded.is_empty());
192 assert!(!src_file.exists());
193 assert_eq!(fs::read(&dest_file).unwrap(), b"hello");
194 }
195
196 #[tokio::test]
197 async fn sync_end_to_end_through_the_public_api() {
198 let src_dir = tempdir().unwrap();
199 let dest_dir = tempdir().unwrap();
200 fs::write(src_dir.path().join("new.txt"), b"new").unwrap();
201 fs::write(dest_dir.path().join("orphan.txt"), b"stale").unwrap();
202
203 let engine = FileEngine::new();
204 let handle = engine.sync(src_dir.path(), dest_dir.path()).start().unwrap();
205 let outcome = handle.await.unwrap();
206
207 assert_eq!(outcome.copy.succeeded.len(), 1);
208 assert_eq!(outcome.delete.succeeded.len(), 1);
209 assert_eq!(fs::read(dest_dir.path().join("new.txt")).unwrap(), b"new");
210 assert!(!dest_dir.path().join("orphan.txt").exists());
211 }
212
213 #[tokio::test]
214 async fn compress_end_to_end_through_the_public_api() {
215 let src_dir = tempdir().unwrap();
216 let out_dir = tempdir().unwrap();
217 fs::write(src_dir.path().join("a.txt"), b"a").unwrap();
218 let dest = out_dir.path().join("archive.zip");
219
220 let engine = FileEngine::new();
221 let mut handle = engine.compress(src_dir.path(), &dest).start().unwrap();
222
223 let mut events = Vec::new();
224 while let Some(event) = handle.progress().next().await {
225 events.push(event);
226 }
227
228 let outcome = handle.await.unwrap();
229
230 assert_eq!(outcome.succeeded.len(), 1);
231 assert!(dest.exists());
232 assert_well_formed(&events, 1);
233 }
234}