1mod error;
2#[cfg(feature = "operations")]
3mod eta;
4#[cfg(feature = "operations")]
5mod handle;
6#[cfg(any(feature = "operations", feature = "watch"))]
12mod operations;
13#[cfg(feature = "operations")]
16mod paths;
17#[cfg(feature = "operations")]
18mod planner;
19#[cfg(feature = "operations")]
20mod profiler;
21#[cfg(feature = "operations")]
22mod progress;
23#[cfg(feature = "watch")]
24mod watch_event;
25#[cfg(feature = "watch")]
26mod watch_handle;
27
28pub use error::{Error, Result};
29#[cfg(feature = "operations")]
30pub use eta::EtaEstimator;
31#[cfg(feature = "operations")]
32pub use handle::Handle;
33#[cfg(feature = "operations")]
34pub use progress::Progress;
35#[cfg(feature = "watch")]
36pub use watch_event::{WatchEvent, WatchEventKind};
37#[cfg(feature = "watch")]
38pub use watch_handle::WatchHandle;
39
40#[cfg(feature = "sync")]
41pub use operations::diff::DiffStrategy;
42#[cfg(feature = "operations")]
43pub use operations::CopyBuilder;
44#[cfg(feature = "operations")]
45pub use operations::MoveBuilder;
46#[cfg(feature = "watch")]
47pub use operations::WatchBuilder;
48#[cfg(feature = "compress")]
49pub use operations::{CompressBuilder, CompressFormat};
50#[cfg(feature = "sync")]
51pub use operations::{SyncBuilder, SyncOutcome};
52
53#[cfg(feature = "operations")]
61pub use planner::{ErrorStrategy, OperationOutcome, SortOrder, StopReason};
62#[cfg(feature = "operations")]
63pub use profiler::Entry;
64
65pub struct FileEngine;
66
67impl Default for FileEngine {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl FileEngine {
74 pub fn new() -> Self {
75 FileEngine
76 }
77
78 #[cfg(feature = "operations")]
79 pub fn copy(
80 &self,
81 source: impl Into<std::path::PathBuf>,
82 dest: impl Into<std::path::PathBuf>,
83 ) -> CopyBuilder {
84 CopyBuilder::new(source, dest)
85 }
86
87 #[cfg(feature = "operations")]
88 pub fn move_path(
89 &self,
90 source: impl Into<std::path::PathBuf>,
91 dest: impl Into<std::path::PathBuf>,
92 ) -> MoveBuilder {
93 MoveBuilder::new(source, dest)
94 }
95
96 #[cfg(feature = "watch")]
97 pub fn watch(&self, path: impl Into<std::path::PathBuf>) -> WatchBuilder {
98 WatchBuilder::new(path)
99 }
100
101 #[cfg(feature = "sync")]
102 pub fn sync(
103 &self,
104 source: impl Into<std::path::PathBuf>,
105 dest: impl Into<std::path::PathBuf>,
106 ) -> SyncBuilder {
107 SyncBuilder::new(source, dest)
108 }
109
110 #[cfg(feature = "compress")]
111 pub fn compress(
112 &self,
113 source: impl Into<std::path::PathBuf>,
114 dest: impl Into<std::path::PathBuf>,
115 ) -> CompressBuilder {
116 CompressBuilder::new(source, dest)
117 }
118}
119
120#[cfg(all(test, feature = "operations", feature = "sync", feature = "compress"))]
121mod tests {
122 use std::fs;
123
124 use tempfile::tempdir;
125 use tokio_stream::StreamExt;
126
127 use super::*;
128
129 fn assert_well_formed(events: &[Progress], expected_entries: usize) {
134 let started_at = events
135 .iter()
136 .position(|e| matches!(e, Progress::Started { .. }));
137 assert!(started_at.is_some(), "expected a Started event");
138
139 let planned_at = events
143 .iter()
144 .position(|e| matches!(e, Progress::Planned { .. }))
145 .expect("expected a Planned event");
146 assert_eq!(planned_at, 0, "Planned must be the first event");
147
148 let planned_entries = events
149 .iter()
150 .find_map(|e| match e {
151 Progress::Planned {
152 small_files,
153 large_files,
154 ..
155 } => Some(small_files + large_files),
156 _ => None,
157 })
158 .unwrap();
159 assert_eq!(
160 planned_entries, expected_entries,
161 "Planned's file counts must agree with what actually ran"
162 );
163
164 if let Some(first_entry_started) = events
165 .iter()
166 .position(|e| matches!(e, Progress::EntryStarted { .. }))
167 {
168 assert!(
169 started_at.unwrap() < first_entry_started,
170 "Started must come before any EntryStarted"
171 );
172 }
173
174 let entries_total = events
175 .iter()
176 .find_map(|e| match e {
177 Progress::Started { entries_total, .. } => Some(*entries_total),
178 _ => None,
179 })
180 .unwrap();
181 assert_eq!(entries_total, expected_entries);
182
183 let terminal_count = events
184 .iter()
185 .filter(|e| {
186 matches!(
187 e,
188 Progress::EntryCompleted { .. } | Progress::EntryFailed { .. }
189 )
190 })
191 .count();
192 assert_eq!(terminal_count, expected_entries);
193 }
194
195 #[tokio::test]
196 async fn copy_end_to_end_through_the_public_api() {
197 let src_dir = tempdir().unwrap();
198 let dest_dir = tempdir().unwrap();
199 fs::write(src_dir.path().join("a.txt"), b"hello").unwrap();
200
201 let engine = FileEngine::new();
202 let mut handle = engine
203 .copy(src_dir.path(), dest_dir.path())
204 .start()
205 .unwrap();
206
207 let mut events = Vec::new();
208 while let Some(event) = handle.progress().next().await {
209 events.push(event);
210 }
211
212 let outcome = handle.await.unwrap();
213
214 assert_eq!(outcome.succeeded.len(), 1);
215 assert_eq!(fs::read(dest_dir.path().join("a.txt")).unwrap(), b"hello");
216 assert_well_formed(&events, 1);
217 assert!(
218 outcome.duration > std::time::Duration::ZERO,
219 "duration should be stamped by the time the handle resolves"
220 );
221 }
222
223 #[tokio::test]
224 async fn move_end_to_end_through_the_public_api() {
225 let root = tempdir().unwrap();
229 let src_file = root.path().join("a.txt");
230 let dest_file = root.path().join("dst.txt");
231 fs::write(&src_file, b"hello").unwrap();
232
233 let engine = FileEngine::new();
234 let handle = engine
235 .move_path(root.path().join("a.txt"), dest_file.clone())
236 .start()
237 .unwrap();
238 let outcome = handle.await.unwrap();
239
240 assert!(outcome.succeeded.is_empty());
242 assert!(!src_file.exists());
243 assert_eq!(fs::read(&dest_file).unwrap(), b"hello");
244 assert!(outcome.duration > std::time::Duration::ZERO);
246 }
247
248 #[tokio::test]
249 async fn sync_end_to_end_through_the_public_api() {
250 let src_dir = tempdir().unwrap();
251 let dest_dir = tempdir().unwrap();
252 fs::write(src_dir.path().join("new.txt"), b"new").unwrap();
253 fs::write(dest_dir.path().join("orphan.txt"), b"stale").unwrap();
254
255 let engine = FileEngine::new();
256 let handle = engine
257 .sync(src_dir.path(), dest_dir.path())
258 .start()
259 .unwrap();
260 let outcome = handle.await.unwrap();
261
262 assert_eq!(outcome.copy.succeeded.len(), 1);
263 assert_eq!(outcome.delete.succeeded.len(), 1);
264 assert_eq!(fs::read(dest_dir.path().join("new.txt")).unwrap(), b"new");
265 assert!(!dest_dir.path().join("orphan.txt").exists());
266 assert!(outcome.copy.duration > std::time::Duration::ZERO);
268 assert!(outcome.delete.duration > std::time::Duration::ZERO);
269 }
270
271 #[tokio::test]
272 async fn compress_end_to_end_through_the_public_api() {
273 let src_dir = tempdir().unwrap();
274 let out_dir = tempdir().unwrap();
275 fs::write(src_dir.path().join("a.txt"), b"a").unwrap();
276 let dest = out_dir.path().join("archive.zip");
277
278 let engine = FileEngine::new();
279 let mut handle = engine.compress(src_dir.path(), &dest).start().unwrap();
280
281 let mut events = Vec::new();
282 while let Some(event) = handle.progress().next().await {
283 events.push(event);
284 }
285
286 let outcome = handle.await.unwrap();
287
288 assert_eq!(outcome.succeeded.len(), 1);
289 assert!(dest.exists());
290 assert_well_formed(&events, 1);
291 assert!(outcome.duration > std::time::Duration::ZERO);
292 }
293}