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(&self, program: impl Into<String>, arguments: impl IntoIterator<Item = String>) -> Self::Command;
14}
15
16#[delegatable_trait]
18pub trait SyncCommandViewAlg: RsyncSorts {
19 fn command_program<'a>(&self, command: &'a Self::Command) -> &'a str;
21 fn command_arguments<'a>(&self, command: &'a Self::Command) -> impl Iterator<Item = &'a str>;
23}
24
25pub const SYNC_PROGRAM: &str = "rsync";
27
28pub const SYNC_OUT_FORMAT: &str = "--out-format=%i|%l|%n";
30
31pub const SYNC_REHEARSAL: &str = "--dry-run";
33
34pub const SYNC_MIRROR: &str = "--delete";
36
37#[ext(name = SyncCommandExt)]
39pub impl<This> This
40where
41 This: SyncCommandAlg + SyncCommandViewAlg + RsyncEndpointAlg + RsyncEndpointViewAlg,
42{
43 fn transfer_command(&self, source: &This::Endpoint, destination: &This::Endpoint, mode: SyncMode) -> This::Command {
50 let arguments = [
51 "--archive",
53 "--partial",
55 "--itemize-changes",
57 "--itemize-changes",
58 SYNC_OUT_FORMAT,
59 "--info=progress2",
60 ]
61 .into_iter()
62 .map(str::to_owned)
63 .chain(mode.profile.profile_arguments().iter().map(|argument| (*argument).to_owned()))
64 .chain(mode.rehearsal.then(|| SYNC_REHEARSAL.to_owned()))
65 .chain([self.endpoint_text(source), self.endpoint_text(destination)]);
66 self.sync_command(SYNC_PROGRAM, arguments)
67 }
68
69 fn command_rehearses(&self, command: &This::Command) -> bool {
71 self.command_arguments(command).any(|argument| argument == SYNC_REHEARSAL)
72 }
73
74 fn command_mirrors(&self, command: &This::Command) -> bool {
76 self.command_arguments(command).any(|argument| argument == SYNC_MIRROR)
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub struct SyncMode {
83 pub profile: SyncProfile,
85 pub rehearsal: bool,
87}
88
89impl SyncMode {
90 #[must_use]
92 pub const fn rehearsal() -> Self {
93 Self { profile: SyncProfile::Copy, rehearsal: true }
94 }
95
96 #[must_use]
98 pub const fn transfer() -> Self {
99 Self { profile: SyncProfile::Copy, rehearsal: false }
100 }
101
102 #[must_use]
104 pub const fn rehearsed(mut self, rehearsal: bool) -> Self {
105 self.rehearsal = rehearsal;
106 self
107 }
108
109 #[must_use]
111 pub const fn profiled(mut self, profile: SyncProfile) -> Self {
112 self.profile = profile;
113 self
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum SyncProfile {
124 Copy,
126 Mirror,
128 MirrorKeeping,
130 MirrorWhole,
132 MirrorReadable,
134 SkipNewer,
136 CompareContent,
138 Resume,
140 Move,
142 LimitRate,
144 KeepMarks,
146 OneDisk,
148 SkipJunk,
150}
151
152case_mapping! {
153 SyncProfile, &'static str as &str,
154 Copy <=> "copy",
155 Mirror <=> "mirror",
156 MirrorKeeping <=> "mirror-keeping",
157 MirrorWhole <=> "mirror-whole",
158 MirrorReadable <=> "mirror-readable",
159 SkipNewer <=> "skip-newer",
160 CompareContent <=> "compare-content",
161 Resume <=> "resume",
162 Move <=> "move",
163 LimitRate <=> "limit-rate",
164 KeepMarks <=> "keep-marks",
165 OneDisk <=> "one-disk",
166 SkipJunk <=> "skip-junk",
167}
168
169impl SyncProfile {
170 #[must_use]
175 pub const fn summary(self) -> &'static str {
176 match self {
177 Self::Copy => "adds and replaces, removes nothing, good for topping a folder up",
178 Self::Mirror => "makes the destination match exactly, good for keeping a copy current",
179 Self::MirrorKeeping => "mirrors, keeps what it replaces, compresses, good for remote backups",
180 Self::MirrorWhole => "mirrors by sending whole files, good for local disks and SSDs",
181 Self::MirrorReadable => "mirrors and makes everything readable, good for publishing a web root",
182 Self::SkipNewer => "replaces nothing newer at the destination, good for merging two copies",
183 Self::CompareContent => "compares what files hold, not when they changed, good after clock drift",
184 Self::Resume => "continues large files where they left off, good for flaky links",
185 Self::Move => "moves, removing what arrived safely, good for clearing a staging area",
186 Self::LimitRate => "leaves room on the line, good for a shared connection",
187 Self::KeepMarks => "keeps links, ownership, and every file mark, good for system backups",
188 Self::OneDisk => "stays on the disk it started on, good for roots with mounts under",
189 Self::SkipJunk => "leaves behind what no one meant to keep, good for editor droppings",
190 }
191 }
192
193 #[must_use]
195 pub const fn profile_arguments(self) -> &'static [&'static str] {
196 match self {
197 Self::Copy => &[],
198 Self::Mirror => &["--delete"],
199 Self::MirrorKeeping => &["--compress", "--delete", "--backup"],
200 Self::MirrorWhole => &["--whole-file", "--delete"],
201 Self::MirrorReadable => &["--delete", "--chmod=D755,F644"],
202 Self::SkipNewer => &["--update"],
203 Self::CompareContent => &["--checksum"],
204 Self::Resume => &["--inplace", "--append-verify"],
205 Self::Move => &["--remove-source-files"],
206 Self::LimitRate => &["--bwlimit=2000"],
207 Self::KeepMarks => &["--hard-links", "--acls", "--xattrs"],
208 Self::OneDisk => &["--one-file-system"],
209 Self::SkipJunk => &["--exclude=.DS_Store", "--exclude=Thumbs.db", "--exclude=*.tmp"],
210 }
211 }
212}