Skip to main content

rsynko_rsync/
command.rs

1use crate::*;
2use alux_ext::ext;
3use alux_sdk::case_mapping;
4use ambassador::delegatable_trait;
5
6/// Provides the carrier and constructor for exactly the command one transfer runs.
7///
8/// The specification owns the command because the command *is* the request: an interpreter runs
9/// it and moves its output, and never decides what a transfer should ask for.
10#[delegatable_trait]
11pub trait SyncCommandAlg: RsyncSorts {
12    /// Defines the command running one program with exactly these arguments, in this order.
13    fn sync_command(&self, program: impl Into<String>, arguments: impl IntoIterator<Item = String>) -> Self::Command;
14}
15
16/// Specifies what one command states about itself.
17#[delegatable_trait]
18pub trait SyncCommandViewAlg: RsyncSorts {
19    /// Observes the program the transfer is performed by.
20    fn command_program<'a>(&self, command: &'a Self::Command) -> &'a str;
21    /// Observes every argument, in the order the program is given them.
22    fn command_arguments<'a>(&self, command: &'a Self::Command) -> impl Iterator<Item = &'a str>;
23}
24
25/// Names the program a transfer is performed by.
26pub const SYNC_PROGRAM: &str = "rsync";
27
28/// States each transferred path as its change, its byte count, and its name.
29pub const SYNC_OUT_FORMAT: &str = "--out-format=%i|%l|%n";
30
31/// States that the transfer changes nothing and only says what it would do.
32pub const SYNC_REHEARSAL: &str = "--dry-run";
33
34/// States that the destination is made to match the source exactly.
35pub const SYNC_MIRROR: &str = "--delete";
36
37/// Derives the command one folder transfer runs from the ends it joins.
38#[ext(name = SyncCommandExt)]
39pub impl<This> This
40where
41    This: SyncCommandAlg + SyncCommandViewAlg + RsyncEndpointAlg + RsyncEndpointViewAlg,
42{
43    /// States the command transferring one endpoint into another.
44    ///
45    /// What is transferred may be one file or a whole folder, and each end is stated exactly as
46    /// it was written. A source ending with a separator names the contents of a folder; one
47    /// without names the path itself, which comes to rest inside the destination. Both are
48    /// wanted, so neither is invented here.
49    fn transfer_command(&self, source: &This::Endpoint, destination: &This::Endpoint, mode: SyncMode) -> This::Command {
50        let arguments = [
51            // Everything a folder states about itself is part of the folder.
52            "--archive",
53            // An interrupted transfer resumes rather than starting the file again.
54            "--partial",
55            // Stated twice, so the transfer also names what it would leave exactly as it is.
56            "--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    /// Observes whether the command states what it would do instead of doing it.
70    fn command_rehearses(&self, command: &This::Command) -> bool {
71        self.command_arguments(command).any(|argument| argument == SYNC_REHEARSAL)
72    }
73
74    /// Observes whether the command removes what the source no longer holds.
75    fn command_mirrors(&self, command: &This::Command) -> bool {
76        self.command_arguments(command).any(|argument| argument == SYNC_MIRROR)
77    }
78}
79
80/// Selects what one transfer is allowed to do.
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub struct SyncMode {
83    /// Selects the way the path is transferred.
84    pub profile: SyncProfile,
85    /// States what the transfer would do instead of doing it.
86    pub rehearsal: bool,
87}
88
89impl SyncMode {
90    /// Selects a transfer that states what it would do and changes nothing.
91    #[must_use]
92    pub const fn rehearsal() -> Self {
93        Self { profile: SyncProfile::Copy, rehearsal: true }
94    }
95
96    /// Selects a transfer that adds and replaces, and removes nothing.
97    #[must_use]
98    pub const fn transfer() -> Self {
99        Self { profile: SyncProfile::Copy, rehearsal: false }
100    }
101
102    /// Selects whether the transfer states what it would do instead of doing it.
103    #[must_use]
104    pub const fn rehearsed(mut self, rehearsal: bool) -> Self {
105        self.rehearsal = rehearsal;
106        self
107    }
108
109    /// Selects the way the path is transferred.
110    #[must_use]
111    pub const fn profiled(mut self, profile: SyncProfile) -> Self {
112        self.profile = profile;
113        self
114    }
115}
116
117/// Selects one commonly wanted way of transferring a path.
118///
119/// A transfer has a great many arguments and almost nobody wants an arbitrary combination of
120/// them: what people want is one of a handful of familiar jobs. Each of these names what it
121/// *does* — not what it is for — and states exactly the arguments that make it do that.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum SyncProfile {
124    /// Adds and replaces, and removes nothing.
125    Copy,
126    /// Makes the destination match the source exactly.
127    Mirror,
128    /// Mirrors, keeping what it replaces and compressing what it sends.
129    MirrorKeeping,
130    /// Mirrors by sending whole files rather than comparing them piece by piece.
131    MirrorWhole,
132    /// Mirrors, and makes everything readable where it lands.
133    MirrorReadable,
134    /// Replaces nothing that is newer at the destination.
135    SkipNewer,
136    /// Compares what files hold rather than when they changed.
137    CompareContent,
138    /// Continues large files where an interrupted transfer left them.
139    Resume,
140    /// Moves, removing from the source whatever arrived safely.
141    Move,
142    /// Leaves room on the line for everything else.
143    LimitRate,
144    /// Keeps links, ownership, and every other mark a file carries.
145    KeepMarks,
146    /// Stays on the disk it started on.
147    OneDisk,
148    /// Leaves behind what no one meant to keep.
149    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    /// States what the transfer does, and then what that is usually wanted for.
171    ///
172    /// A reader chooses by what a way of transferring does, and confirms the choice by what it is
173    /// good for, so both are stated and in that order.
174    #[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    /// States exactly the arguments that make the transfer this job and no other.
194    #[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}