1use crate::*;
2use alux_ext::ext;
3use alux_sdk::case_mapping;
4use ambassador::delegatable_trait;
5
6#[delegatable_trait]
11pub trait SyncCommandAlg: RsyncSorts {
12 fn sync_command(
14 &self,
15 program: impl Into<String>,
16 arguments: impl IntoIterator<Item = String>,
17 ) -> Self::Command;
18}
19
20#[delegatable_trait]
22pub trait SyncCommandViewAlg: RsyncSorts {
23 fn command_program<'a>(&self, command: &'a Self::Command) -> &'a str;
25 fn command_arguments<'a>(&self, command: &'a Self::Command) -> impl Iterator<Item = &'a str>;
27}
28
29pub const SYNC_PROGRAM: &str = "rsync";
31
32pub const SYNC_OUT_FORMAT: &str = "--out-format=%i|%l|%n";
34
35pub const SYNC_REHEARSAL: &str = "--dry-run";
37
38pub const SYNC_MIRROR: &str = "--delete";
40
41#[ext(name = SyncCommandExt)]
43pub impl<This> This
44where
45 This: SyncCommandAlg + SyncCommandViewAlg + RsyncEndpointAlg + RsyncEndpointViewAlg,
46{
47 fn transfer_command(
54 &self,
55 source: &This::Endpoint,
56 destination: &This::Endpoint,
57 mode: SyncMode,
58 ) -> This::Command {
59 let arguments = [
60 "--archive",
62 "--partial",
64 "--itemize-changes",
66 "--itemize-changes",
67 SYNC_OUT_FORMAT,
68 "--info=progress2",
69 ]
70 .into_iter()
71 .map(str::to_owned)
72 .chain(
73 mode.profile
74 .profile_arguments()
75 .iter()
76 .map(|argument| (*argument).to_owned()),
77 )
78 .chain(mode.rehearsal.then(|| SYNC_REHEARSAL.to_owned()))
79 .chain([self.endpoint_text(source), self.endpoint_text(destination)]);
80 self.sync_command(SYNC_PROGRAM, arguments)
81 }
82
83 fn command_rehearses(&self, command: &This::Command) -> bool {
85 self.command_arguments(command)
86 .any(|argument| argument == SYNC_REHEARSAL)
87 }
88
89 fn command_mirrors(&self, command: &This::Command) -> bool {
91 self.command_arguments(command)
92 .any(|argument| argument == SYNC_MIRROR)
93 }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub struct SyncMode {
99 pub profile: SyncProfile,
101 pub rehearsal: bool,
103}
104
105impl SyncMode {
106 #[must_use]
108 pub const fn rehearsal() -> Self {
109 Self {
110 profile: SyncProfile::Copy,
111 rehearsal: true,
112 }
113 }
114
115 #[must_use]
117 pub const fn transfer() -> Self {
118 Self {
119 profile: SyncProfile::Copy,
120 rehearsal: false,
121 }
122 }
123
124 #[must_use]
126 pub const fn rehearsed(mut self, rehearsal: bool) -> Self {
127 self.rehearsal = rehearsal;
128 self
129 }
130
131 #[must_use]
133 pub const fn profiled(mut self, profile: SyncProfile) -> Self {
134 self.profile = profile;
135 self
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum SyncProfile {
146 Copy,
148 Mirror,
150 MirrorKeeping,
152 MirrorWhole,
154 MirrorReadable,
156 SkipNewer,
158 CompareContent,
160 Resume,
162 Move,
164 LimitRate,
166 KeepMarks,
168 OneDisk,
170 SkipJunk,
172}
173
174case_mapping! {
175 SyncProfile, &'static str as &str,
176 Copy <=> "copy",
177 Mirror <=> "mirror",
178 MirrorKeeping <=> "mirror-keeping",
179 MirrorWhole <=> "mirror-whole",
180 MirrorReadable <=> "mirror-readable",
181 SkipNewer <=> "skip-newer",
182 CompareContent <=> "compare-content",
183 Resume <=> "resume",
184 Move <=> "move",
185 LimitRate <=> "limit-rate",
186 KeepMarks <=> "keep-marks",
187 OneDisk <=> "one-disk",
188 SkipJunk <=> "skip-junk",
189}
190
191impl SyncProfile {
192 #[must_use]
197 pub const fn summary(self) -> &'static str {
198 match self {
199 Self::Copy => "adds and replaces, removes nothing, good for topping a folder up",
200 Self::Mirror => "makes the destination match exactly, good for keeping a copy current",
201 Self::MirrorKeeping => {
202 "mirrors, keeps what it replaces, compresses, good for remote backups"
203 }
204 Self::MirrorWhole => "mirrors by sending whole files, good for local disks and SSDs",
205 Self::MirrorReadable => {
206 "mirrors and makes everything readable, good for publishing a web root"
207 }
208 Self::SkipNewer => {
209 "replaces nothing newer at the destination, good for merging two copies"
210 }
211 Self::CompareContent => {
212 "compares what files hold, not when they changed, good after clock drift"
213 }
214 Self::Resume => "continues large files where they left off, good for flaky links",
215 Self::Move => "moves, removing what arrived safely, good for clearing a staging area",
216 Self::LimitRate => "leaves room on the line, good for a shared connection",
217 Self::KeepMarks => {
218 "keeps links, ownership, and every file mark, good for system backups"
219 }
220 Self::OneDisk => "stays on the disk it started on, good for roots with mounts under",
221 Self::SkipJunk => "leaves behind what no one meant to keep, good for editor droppings",
222 }
223 }
224
225 #[must_use]
227 pub const fn profile_arguments(self) -> &'static [&'static str] {
228 match self {
229 Self::Copy => &[],
230 Self::Mirror => &["--delete"],
231 Self::MirrorKeeping => &["--compress", "--delete", "--backup"],
232 Self::MirrorWhole => &["--whole-file", "--delete"],
233 Self::MirrorReadable => &["--delete", "--chmod=D755,F644"],
234 Self::SkipNewer => &["--update"],
235 Self::CompareContent => &["--checksum"],
236 Self::Resume => &["--inplace", "--append-verify"],
237 Self::Move => &["--remove-source-files"],
238 Self::LimitRate => &["--bwlimit=2000"],
239 Self::KeepMarks => &["--hard-links", "--acls", "--xattrs"],
240 Self::OneDisk => &["--one-file-system"],
241 Self::SkipJunk => &[
242 "--exclude=.DS_Store",
243 "--exclude=Thumbs.db",
244 "--exclude=*.tmp",
245 ],
246 }
247 }
248}