borgbackup/
common.rs

1//! The common options of borg commands are defined here
2
3use std::fmt::{Display, Formatter, Write};
4use std::io::BufRead;
5use std::num::NonZeroU16;
6use std::process::Output;
7
8use log::{debug, error, info, trace, warn};
9use serde::{Deserialize, Serialize};
10
11use crate::errors::{CompactError, CreateError, InitError, ListError, MountError, PruneError};
12use crate::output::create::Create;
13use crate::output::list::ListRepository;
14use crate::output::logging::{LevelName, LoggingMessage, MessageId};
15use crate::utils::shell_escape;
16
17/// A pattern instruction.
18/// These instructions will be used for the `--pattern` command line parameter.
19///
20/// [PatternInstruction::Include] are useful to include paths that are contained in an excluded path.
21/// The first matching pattern is used so if an [PatternInstruction::Include] matches before
22/// an [PatternInstruction::Exclude], the file is backed up.
23/// If an [PatternInstruction::ExcludeNoRecurse] pattern matches a directory,
24/// it won't recurse into it and won't discover any potential matches for include rules
25/// below that directory.
26#[derive(Serialize, Deserialize, Debug, Clone)]
27pub enum PatternInstruction {
28    /// A plain root path to use as a starting point
29    Root(String),
30    /// Include matched files
31    Include(Pattern),
32    /// Exclude matched files
33    Exclude(Pattern),
34    /// If an [PatternInstruction::ExcludeNoRecurse] pattern matches a directory, it won't recurse into
35    /// it and won't discover any potential matches for include rules below that directory.
36    ExcludeNoRecurse(Pattern),
37}
38
39impl Display for PatternInstruction {
40    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
41        match self {
42            PatternInstruction::Root(path) => write!(f, "P {path}"),
43            PatternInstruction::Include(pattern) => write!(f, "+ {pattern}"),
44            PatternInstruction::Exclude(pattern) => write!(f, "- {pattern}"),
45            PatternInstruction::ExcludeNoRecurse(pattern) => write!(f, "! {pattern}"),
46        }
47    }
48}
49
50/// The path/filenames used as input for the pattern matching start from the
51/// currently active recursion root. You usually give the recursion root(s)
52/// when invoking borg and these can be either relative or absolute paths.
53///
54/// [Pattern::Regex], [Pattern::Shell] and [Pattern::FnMatch] patterns are all implemented on top
55/// of the Python SRE engine. It is very easy to formulate patterns for each of these types which
56/// requires an inordinate amount of time to match paths.
57///
58/// If untrusted users are able to supply patterns, ensure they cannot supply [Pattern::Regex]
59/// patterns. Further, ensure that [Pattern::Shell] and [Pattern::FnMatch] patterns only contain a
60/// handful of wildcards at most.
61///
62/// Note that this enum is only for use in [PatternInstruction].
63#[derive(Serialize, Deserialize, Debug, Clone)]
64pub enum Pattern {
65    /// Fnmatch <https://docs.python.org/3/library/fnmatch.html>:
66    ///
67    /// These patterns use a variant of shell pattern syntax, with '\*' matching
68    /// any number of characters, '?' matching any single character, '\[...]'
69    /// matching any single character specified, including ranges, and '\[!...]'
70    /// matching any character not specified.
71    ///
72    /// For the purpose of these patterns, the path separator
73    /// (backslash for Windows and '/' on other systems) is not treated specially.
74    /// Wrap meta-characters in brackets for a literal match
75    /// (i.e. \[?] to match the literal character ?).
76    /// For a path to match a pattern, the full path must match, or it must match
77    /// from the start of the full path to just before a path separator. Except
78    /// for the root path, paths will never end in the path separator when
79    /// matching is attempted.  Thus, if a given pattern ends in a path
80    /// separator, a '*' is appended before matching is attempted.
81    ///
82    /// A leading path separator is always removed.
83    FnMatch(String),
84    /// Like [Pattern::FnMatch] patterns these are similar to shell patterns.
85    /// The difference is that the pattern may include **/ for matching zero or more
86    /// directory levels, * for matching zero or more arbitrary characters with the exception of
87    /// any path separator.
88    ///
89    /// A leading path separator is always removed.
90    Shell(String),
91    /// Regular expressions similar to those found in Perl are supported. Unlike shell patterns,
92    /// regular expressions are not required to match the full path and any substring match
93    /// is sufficient.
94    ///
95    /// It is strongly recommended to anchor patterns to the start ('^'), to the end ('$') or both.
96    ///
97    /// Path separators (backslash for Windows and '/' on other systems) in paths are
98    /// always normalized to a forward slash ('/') before applying a pattern.
99    ///
100    /// The regular expression syntax is described in the Python documentation for the re module
101    /// <https://docs.python.org/3/library/re.html>.
102    Regex(String),
103    /// This pattern style is useful to match whole sub-directories.
104    ///
105    /// The pattern `root/somedir` matches `root/somedir` and everything therein.
106    /// A leading path separator is always removed.
107    PathPrefix(String),
108    /// This pattern style is (only) useful to match full paths.
109    ///
110    /// This is kind of a pseudo pattern as it can not have any variable or unspecified parts -
111    /// the full path must be given. `root/file.ext` matches `root/file.ext` only.
112    ///
113    /// A leading path separator is always removed.
114    ///
115    /// Implementation note: this is implemented via very time-efficient O(1)
116    /// hashtable lookups (this means you can have huge amounts of such patterns
117    /// without impacting performance much).
118    /// Due to that, this kind of pattern does not respect any context or order.
119    /// If you use such a pattern to include a file, it will always be included
120    /// (if the directory recursion encounters it).
121    /// Other include/exclude patterns that would normally match will be ignored.
122    /// Same logic applies for exclude.
123    PathFullMatch(String),
124}
125
126impl Display for Pattern {
127    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
128        match self {
129            Pattern::FnMatch(x) => write!(f, "fm:{x}"),
130            Pattern::Shell(x) => write!(f, "sh:{x}"),
131            Pattern::Regex(x) => write!(f, "re:{x}"),
132            Pattern::PathPrefix(x) => write!(f, "pp:{x}"),
133            Pattern::PathFullMatch(x) => write!(f, "pf:{x}"),
134        }
135    }
136}
137
138/// The compression modes of an archive.
139///
140/// The compression modes `auto` and `obfuscate` are currently not supported by this library.
141#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
142pub enum CompressionMode {
143    /// No compression
144    None,
145    /// Use lz4 compression. Very high speed, very low compression.
146    Lz4,
147    /// Use zstd ("zstandard") compression, a modern wide-range algorithm.
148    ///
149    /// Allowed values ranging from 1 to 22, where 1 is the fastest and 22 the highest compressing.
150    ///
151    /// Archives compressed with zstd are not compatible with borg < 1.1.4.
152    Zstd(u8),
153    /// Use zlib ("gz") compression. Medium speed, medium compression.
154    ///
155    /// Allowed values ranging from 0 to 9.
156    /// Giving level 0 (means "no compression", but still has zlib protocol overhead)
157    /// is usually pointless, you better use [CompressionMode::None] compression.
158    Zlib(u8),
159    /// Use lzma ("xz") compression. Low speed, high compression.
160    ///
161    /// Allowed values ranging from 0 to 9.
162    /// Giving levels above 6 is pointless and counterproductive because it does
163    /// not compress better due to the buffer size used by borg - but it wastes
164    /// lots of CPU cycles and RAM.
165    Lzma(u8),
166}
167
168impl Display for CompressionMode {
169    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
170        match self {
171            CompressionMode::None => write!(f, "none"),
172            CompressionMode::Lz4 => write!(f, "lz4"),
173            CompressionMode::Zstd(x) => write!(f, "zstd,{x}"),
174            CompressionMode::Zlib(x) => write!(f, "zlib,{x}"),
175            CompressionMode::Lzma(x) => write!(f, "lzma,{x}"),
176        }
177    }
178}
179
180/// The encryption mode of the repository.
181///
182/// See <https://borgbackup.readthedocs.io/en/stable/usage/init.html#more-encryption-modes>
183/// for further information about encryption modes.
184#[derive(Serialize, Deserialize, Debug, Clone)]
185pub enum EncryptionMode {
186    /// No encryption, nor hashing.
187    ///
188    /// This mode is not recommended.
189    None,
190    /// Uses no encryption, but authenticates repository contents through HMAC-SHA256 hashes
191    Authenticated(String),
192    /// Uses no encryption, but authenticates repository contents through keyed BLAKE2b-256 hashes
193    AuthenticatedBlake2(String),
194    /// Use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an
195    /// encrypt-then-MAC (EtM) construction.
196    ///
197    /// The chunk ID hash is HMAC-SHA256 as well (with a separate key).
198    ///
199    /// Stores the key in the repository.
200    Repokey(String),
201    /// Use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an
202    /// encrypt-then-MAC (EtM) construction.
203    ///
204    /// The chunk ID hash is HMAC-SHA256 as well (with a separate key).
205    ///
206    /// Stores the key locally.
207    Keyfile(String),
208    /// Use AES-CTR-256 for encryption and BLAKE2b-256 for authentication in an
209    /// encrypt-then-MAC (EtM) construction.
210    ///
211    /// The chunk ID hash is a keyed BLAKE2b-256 hash.
212    ///
213    /// Stores the key in the repository.
214    RepokeyBlake2(String),
215    /// Use AES-CTR-256 for encryption and BLAKE2b-256 for authentication in an
216    /// encrypt-then-MAC (EtM) construction.
217    ///
218    /// The chunk ID hash is a keyed BLAKE2b-256 hash.
219    ///
220    /// Stores the key locally.
221    KeyfileBlake2(String),
222}
223
224impl EncryptionMode {
225    pub(crate) fn get_passphrase(&self) -> Option<String> {
226        match self {
227            EncryptionMode::None => None,
228            EncryptionMode::Authenticated(x) => Some(x.clone()),
229            EncryptionMode::AuthenticatedBlake2(x) => Some(x.clone()),
230            EncryptionMode::Repokey(x) => Some(x.clone()),
231            EncryptionMode::Keyfile(x) => Some(x.clone()),
232            EncryptionMode::RepokeyBlake2(x) => Some(x.clone()),
233            EncryptionMode::KeyfileBlake2(x) => Some(x.clone()),
234        }
235    }
236}
237
238impl Display for EncryptionMode {
239    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
240        match self {
241            EncryptionMode::None => write!(f, "none"),
242            EncryptionMode::Authenticated(_) => write!(f, "authenticated"),
243            EncryptionMode::AuthenticatedBlake2(_) => write!(f, "authenticated-blake2"),
244            EncryptionMode::Repokey(_) => write!(f, "repokey"),
245            EncryptionMode::Keyfile(_) => write!(f, "keyfile"),
246            EncryptionMode::RepokeyBlake2(_) => write!(f, "repokey-blake2"),
247            EncryptionMode::KeyfileBlake2(_) => write!(f, "keyfile-blake2"),
248        }
249    }
250}
251
252/// The common options that can be used for every borg command
253#[derive(Deserialize, Serialize, Debug, Clone, Default)]
254pub struct CommonOptions {
255    /// The local path to the borg executable. (default = "borg")
256    pub local_path: Option<String>,
257    /// The remote path to the borg executable. (default = "borg")
258    pub remote_path: Option<String>,
259    /// set network upload rate limit in kiByte/s (0 = unlimited)
260    pub upload_ratelimit: Option<u64>,
261    /// Use this command to connect to the ‘borg serve’ process (default: "ssh")
262    ///
263    /// This can be useful to specify an alternative ssh key: "ssh -i /path/to/privkey"
264    pub rsh: Option<String>,
265}
266
267impl From<&CommonOptions> for String {
268    fn from(value: &CommonOptions) -> Self {
269        let mut s = String::new();
270
271        if let Some(rsh) = &value.rsh {
272            s = format!("{s} --rsh {} ", shell_escape(rsh));
273        }
274
275        if let Some(remote_path) = &value.remote_path {
276            s = format!("{s} --remote-path {} ", shell_escape(remote_path));
277        }
278
279        if let Some(upload_ratelimit) = &value.upload_ratelimit {
280            s = format!("{s} --upload-ratelimit {upload_ratelimit} ");
281        }
282
283        s
284    }
285}
286
287/// The quantifier for [PruneWithin]
288#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
289pub enum PruneWithinTime {
290    /// Hour quantifier
291    Hour,
292    /// Day quantifier
293    Day,
294    /// Week quantifier (7 days)
295    Week,
296    /// Month quantifier (31 days)
297    Month,
298    /// Year quantifier
299    Year,
300}
301
302impl Display for PruneWithinTime {
303    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
304        match self {
305            PruneWithinTime::Hour => write!(f, "H"),
306            PruneWithinTime::Day => write!(f, "d"),
307            PruneWithinTime::Week => write!(f, "w"),
308            PruneWithinTime::Month => write!(f, "m"),
309            PruneWithinTime::Year => write!(f, "y"),
310        }
311    }
312}
313
314/// The definition to specify an interval in which backups
315/// should not be pruned.
316#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
317pub struct PruneWithin {
318    /// quantifier
319    pub quantifier: NonZeroU16,
320    /// The time quantifier
321    pub time: PruneWithinTime,
322}
323
324impl Display for PruneWithin {
325    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
326        write!(f, "{}{}", self.quantifier, self.time)
327    }
328}
329
330/// Options for [crate::sync::prune]
331///
332/// A good procedure is to thin out more and more the older your backups get.
333/// As an example, `keep_daily` 7 means to keep the latest backup on each day, up to 7 most
334/// recent days with backups (days without backups do not count). The rules are applied from
335/// secondly to yearly, and backups selected by previous rules do not count towards those of
336/// later rules. The time that each backup starts is used for pruning purposes.
337/// Dates and times are interpreted in the local timezone, and weeks go from Monday to Sunday.
338/// Specifying a negative number of archives to keep means that there is no limit.
339///
340/// As of borg 1.2.0, borg will retain the oldest archive if any of the secondly, minutely,
341/// hourly, daily, weekly, monthly, or yearly rules was not otherwise able to meet
342/// its retention target. This enables the first chronological archive to continue aging until
343/// it is replaced by a newer archive that meets the retention criteria.
344#[derive(Serialize, Deserialize, Debug, Clone)]
345pub struct PruneOptions {
346    /// Path to the repository
347    ///
348    /// Example values:
349    /// - `/tmp/foo`
350    /// - `user@example.com:/opt/repo`
351    /// - `ssh://user@example.com:2323:/opt/repo`
352    pub repository: String,
353    /// The passphrase for the repository
354    ///
355    /// If using a repository with [EncryptionMode::None],
356    /// you can leave this option empty
357    pub passphrase: Option<String>,
358    /// The archives kept with this option do not count towards the totals specified
359    /// by any other options.
360    pub keep_within: Option<PruneWithin>,
361    /// number of secondly archives to keep
362    pub keep_secondly: Option<NonZeroU16>,
363    /// number of minutely archives to keep
364    pub keep_minutely: Option<NonZeroU16>,
365    /// number of hourly archives to keep
366    pub keep_hourly: Option<NonZeroU16>,
367    /// number of daily archives to keep
368    pub keep_daily: Option<NonZeroU16>,
369    /// number of weekly archives to keep
370    pub keep_weekly: Option<NonZeroU16>,
371    /// number of monthly archives to keep
372    pub keep_monthly: Option<NonZeroU16>,
373    /// number of yearly archives to keep
374    pub keep_yearly: Option<NonZeroU16>,
375    /// write checkpoint every SECONDS seconds (Default: 1800)
376    pub checkpoint_interval: Option<NonZeroU16>,
377    /// only consider archive names matching the glob.
378    ///
379    /// The pattern can use [Pattern::Shell]
380    pub glob_archives: Option<String>,
381}
382
383impl PruneOptions {
384    /// Create an new [PruneOptions]
385    pub fn new(repository: String) -> Self {
386        Self {
387            repository,
388            passphrase: None,
389            keep_within: None,
390            keep_secondly: None,
391            keep_minutely: None,
392            keep_hourly: None,
393            keep_daily: None,
394            keep_weekly: None,
395            keep_monthly: None,
396            keep_yearly: None,
397            checkpoint_interval: None,
398            glob_archives: None,
399        }
400    }
401}
402
403/// Options for [crate::sync::mount]
404///
405/// Mount an archive or repository as a FUSE filesystem. This is useful for
406/// browsing archives or repositories and interactive restoration.
407#[derive(Serialize, Deserialize, Debug, Clone)]
408pub enum MountSource {
409    /// Mount a repository
410    Repository {
411        /// Name of the repository you wish to mount
412        ///
413        /// Example values:
414        /// - `/tmp/foo`
415        /// - `user@example.com:/opt/repo`
416        /// - `ssh://user@example.com:2323:/opt/repo`
417        name: String,
418        /// Obtain the first N archives
419        first_n_archives: Option<NonZeroU16>,
420        /// Obtain the last N archives
421        last_n_archives: Option<NonZeroU16>,
422        /// only consider archive names matching the glob.
423        glob_archives: Option<String>,
424    },
425    /// Mount an archive (repo_name::archive_name)
426    Archive {
427        /// Path to the borg archive you wish to mount
428        ///
429        /// Example values:
430        /// - `/tmp/foo::my-archive`
431        /// - `user@example.com:/opt/repo::archive`
432        /// - `ssh://user@example.com:2323:/opt/repo`
433        archive_name: String,
434    },
435}
436
437/// Options for [crate::sync::mount]
438///
439/// Mount an archive or repository as a FUSE filesystem. This is useful for
440/// browsing archives or repositories and interactive restoration.
441#[derive(Serialize, Deserialize, Debug, Clone)]
442pub struct MountOptions {
443    /// The archive or repo you wish to mount
444    pub mount_source: MountSource,
445    /// The path where the repo or archive will be mounted.
446    ///
447    /// Example values:
448    /// - `/mnt/my-borg-backup`
449    pub mountpoint: String,
450    /// The passphrase for the repository
451    ///
452    /// If using a repository with [EncryptionMode::None],
453    /// you can leave this option empty
454    pub passphrase: Option<String>,
455    /// Paths to extract. If left empty all paths will be present. Useful
456    /// for whitelisting certain paths in the backup.
457    ///
458    /// Example values:
459    /// - `/a/path/I/actually/care/about`
460    /// - `**/some/intermediate/folder/*`
461    pub select_paths: Vec<Pattern>,
462}
463
464impl MountOptions {
465    /// Create an new [MountOptions]
466    pub fn new(mount_source: MountSource, mountpoint: String) -> Self {
467        Self {
468            mount_source,
469            mountpoint,
470            passphrase: None,
471            select_paths: vec![],
472        }
473    }
474}
475
476/// Options for [crate::sync::compact]
477#[derive(Serialize, Deserialize, Clone, Debug)]
478pub struct CompactOptions {
479    /// Path to the repository
480    ///
481    /// Example values:
482    /// - `/tmp/foo`
483    /// - `user@example.com:/opt/repo`
484    /// - `ssh://user@example.com:2323:/opt/repo`
485    pub repository: String,
486}
487
488/// The options for a borg create command
489#[derive(Serialize, Deserialize, Debug, Clone)]
490pub struct CreateOptions {
491    /// Path to the repository
492    ///
493    /// Example values:
494    /// - `/tmp/foo`
495    /// - `user@example.com:/opt/repo`
496    /// - `ssh://user@example.com:2323:/opt/repo`
497    pub repository: String,
498    /// Name of archive to create (must be also a valid directory name)
499    ///
500    /// The archive name needs to be unique.
501    /// It must not end in ‘.checkpoint’ or ‘.checkpoint.N’ (with N being a number),
502    /// because these names are used for checkpoints and treated in special ways.
503    ///
504    /// In the archive name, you may use the following placeholders:
505    /// {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others.
506    ///
507    /// See <https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-patterns> for further
508    /// information.
509    pub archive: String,
510    /// The passphrase for the repository
511    ///
512    /// If using a repository with [EncryptionMode::None],
513    /// you can leave this option empty
514    pub passphrase: Option<String>,
515    /// Add a comment text to the archive
516    pub comment: Option<String>,
517    /// Specify the compression mode that should be used.
518    ///
519    /// Defaults to [CompressionMode::Lz4].
520    pub compression: Option<CompressionMode>,
521    /// The paths to archive.
522    ///
523    /// All given paths will be recursively traversed.
524    pub paths: Vec<String>,
525    /// Exclude directories that contain a CACHEDIR.TAG file
526    /// (<http://www.bford.info/cachedir/spec.html>)
527    pub exclude_caches: bool,
528    /// The patterns to apply
529    ///
530    /// Using these, you may specify the backup roots (starting points)
531    /// and patterns for inclusion/exclusion.
532    ///
533    /// See [PatternInstruction] for further information.
534    ///
535    /// Note that the order of the instructions is important:
536    ///
537    /// ```bash
538    /// # backup pics, but not the ones from 2018, except the good ones:
539    /// borg create --pattern=+pics/2018/good --pattern=-pics/2018 repo::arch pics
540    /// ```
541    pub patterns: Vec<PatternInstruction>,
542    /// read include/exclude patterns from the given path, one per line
543    ///
544    /// Refer to <https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns>
545    /// for further information how to use pattern / exclude files.
546    pub pattern_file: Option<String>,
547    /// Exclude paths matching PATTERN.
548    ///
549    /// See [Pattern] for further information.
550    pub excludes: Vec<Pattern>,
551    /// read exclude patterns from the given path, one per line
552    ///
553    /// Refer to <https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns>
554    /// for further information how to use pattern / exclude files.
555    pub exclude_file: Option<String>,
556    /// Only store numeric user and group identifiers
557    pub numeric_ids: bool,
558    /// Detect sparse holes in input (supported only by fixed chunker)
559    pub sparse: bool,
560    /// Open and read block and char device files as well as FIFOs as if they were regular files.
561    ///
562    /// Also follows symlinks pointing to these kinds of files.
563    pub read_special: bool,
564    /// Do not read and store xattrs into archive
565    pub no_xattrs: bool,
566    /// Do not read and store ACLs into archive
567    pub no_acls: bool,
568    /// Do not read and store flags (e.g. NODUMP, IMMUTABLE) into archive
569    pub no_flags: bool,
570}
571
572impl CreateOptions {
573    /// Create an new [CreateOptions]
574    pub fn new(
575        repository: String,
576        archive: String,
577        paths: Vec<String>,
578        patterns: Vec<PatternInstruction>,
579    ) -> Self {
580        Self {
581            repository,
582            archive,
583            passphrase: None,
584            comment: None,
585            compression: None,
586            paths,
587            exclude_caches: false,
588            patterns,
589            pattern_file: None,
590            excludes: vec![],
591            exclude_file: None,
592            numeric_ids: false,
593            sparse: false,
594            read_special: false,
595            no_xattrs: false,
596            no_acls: false,
597            no_flags: false,
598        }
599    }
600}
601
602/// The options to provide to the [crate::sync::init] command
603#[derive(Serialize, Deserialize, Debug, Clone)]
604pub struct InitOptions {
605    /// Path to the repository
606    ///
607    /// Example values:
608    /// - `/tmp/foo`
609    /// - `user@example.com:/opt/repo`
610    /// - `ssh://user@example.com:2323:/opt/repo`
611    pub repository: String,
612    /// The mode to use for encryption
613    pub encryption_mode: EncryptionMode,
614    /// Set the repository to append_only mode.
615    /// Note that this only affects the low level structure of the repository,
616    /// and running delete or prune will still be allowed.
617    ///
618    /// See [Append-only mode (forbid compaction)](https://borgbackup.readthedocs.io/en/stable/usage/notes.html#append-only-mode)
619    /// in Additional Notes for more details.
620    pub append_only: bool,
621    /// Create the parent directories of the repo, if they are missing.
622    ///
623    /// Defaults to false
624    pub make_parent_dirs: bool,
625    /// Set storage quota of the new repository (e.g. 5G, 1.5T)
626    ///
627    /// No quota is set by default
628    pub storage_quota: Option<String>,
629}
630
631impl InitOptions {
632    /// Create new [InitOptions].
633    ///
634    /// `append_only`, `make_parent_dirs` and `storage_quota` are set to their defaults.
635    pub fn new(repository: String, encryption_mode: EncryptionMode) -> Self {
636        Self {
637            repository,
638            encryption_mode,
639            append_only: false,
640            make_parent_dirs: false,
641            storage_quota: None,
642        }
643    }
644}
645
646/// The options for the [crate::sync::list] command
647#[derive(Deserialize, Serialize, Debug, Clone)]
648pub struct ListOptions {
649    /// Path to the repository
650    ///
651    /// Example values:
652    /// - `/tmp/foo`
653    /// - `user@example.com:/opt/repo`
654    /// - `ssh://user@example.com:2323:/opt/repo`
655    pub repository: String,
656    /// The passphrase for the repository
657    ///
658    /// If using a repository with [EncryptionMode::None],
659    /// you can leave this option empty
660    pub passphrase: Option<String>,
661}
662
663pub(crate) fn init_fmt_args(options: &InitOptions, common_options: &CommonOptions) -> String {
664    format!(
665        "--log-json {common_options}init -e {e}{append_only}{make_parent_dirs}{storage_quota} {repository}",
666        common_options = String::from(common_options),
667        e = options.encryption_mode,
668        append_only = if options.append_only {
669            " --append-only"
670        } else {
671            ""
672        },
673        make_parent_dirs = if options.make_parent_dirs {
674            " --make-parent-dirs"
675        } else {
676            ""
677        },
678        storage_quota = options
679            .storage_quota
680            .as_ref()
681            .map_or("".to_string(), |x| format!(
682                " --storage-quota {quota}",
683                quota = shell_escape(x)
684            )),
685        repository = shell_escape(&options.repository),
686    )
687}
688
689fn log_message(level_name: LevelName, time: f64, name: String, message: String) {
690    match level_name {
691        LevelName::Debug => debug!("{time} {name}: {message}"),
692        LevelName::Info => info!("{time} {name}: {message}"),
693        LevelName::Warning => warn!("{time} {name}: {message}"),
694        LevelName::Error => error!("{time} {name}: {message}"),
695        LevelName::Critical => error!("{time} {name}: {message}"),
696    }
697}
698
699pub(crate) fn init_parse_result(res: Output) -> Result<(), InitError> {
700    let Some(exit_code) = res.status.code() else {
701        warn!("borg process was terminated by signal");
702        return Err(InitError::TerminatedBySignal);
703    };
704
705    let mut output = String::new();
706
707    for line in res.stderr.lines() {
708        let line = line.map_err(InitError::InvalidBorgOutput)?;
709        writeln!(output, "{line}").unwrap();
710
711        trace!("borg output: {line}");
712
713        let log_msg = LoggingMessage::from_str(&line)?;
714
715        if let LoggingMessage::LogMessage {
716            name,
717            message,
718            level_name,
719            time,
720            msg_id,
721        } = log_msg
722        {
723            log_message(level_name, time, name, message);
724
725            if let Some(msg_id) = msg_id {
726                match msg_id {
727                    MessageId::RepositoryAlreadyExists => {
728                        return Err(InitError::RepositoryAlreadyExists)
729                    }
730                    _ => {
731                        if exit_code > 1 {
732                            return Err(InitError::UnexpectedMessageId(msg_id));
733                        }
734                    }
735                }
736            }
737
738            if let Some(MessageId::RepositoryAlreadyExists) = msg_id {
739                return Err(InitError::RepositoryAlreadyExists);
740            } else if let Some(msg_id) = msg_id {
741                return Err(InitError::UnexpectedMessageId(msg_id));
742            }
743        }
744    }
745
746    if exit_code > 1 {
747        return Err(InitError::Unknown(output));
748    }
749
750    Ok(())
751}
752
753pub(crate) fn prune_fmt_args(options: &PruneOptions, common_options: &CommonOptions) -> String {
754    format!(
755        "--log-json {common_options} prune{keep_within}{keep_secondly}{keep_minutely}{keep_hourly}{keep_daily}{keep_weekly}{keep_monthly}{keep_yearly} {repository}",
756        common_options = String::from(common_options),
757        keep_within = options.keep_within.as_ref().map_or("".to_string(), |x| format!(" --keep-within {x}")),
758        keep_secondly = options.keep_secondly.as_ref().map_or("".to_string(), |x| format!(" --keep-secondly {x}")),
759        keep_minutely = options.keep_minutely.map_or("".to_string(), |x| format!(" --keep-minutely {x}")),
760        keep_hourly = options.keep_hourly.map_or("".to_string(), |x| format!(" --keep-hourly {x}")),
761        keep_daily = options.keep_daily.map_or("".to_string(), |x| format!(" --keep-daily {x}")),
762        keep_weekly = options.keep_weekly.map_or("".to_string(), |x| format!(" --keep-weekly {x}")),
763        keep_monthly = options.keep_monthly.map_or("".to_string(), |x| format!(" --keep-monthly {x}")),
764        keep_yearly = options.keep_yearly.map_or("".to_string(), |x| format!(" --keep-yearly {x}")),
765        repository = shell_escape(&options.repository)
766    )
767}
768
769pub(crate) fn prune_parse_output(res: Output) -> Result<(), PruneError> {
770    let Some(exit_code) = res.status.code() else {
771        warn!("borg process was terminated by signal");
772        return Err(PruneError::TerminatedBySignal);
773    };
774
775    let mut output = String::new();
776
777    for line in BufRead::lines(res.stderr.as_slice()) {
778        let line = line.map_err(PruneError::InvalidBorgOutput)?;
779        writeln!(output, "{line}").unwrap();
780
781        trace!("borg output: {line}");
782
783        let log_msg = LoggingMessage::from_str(&line)?;
784
785        if let LoggingMessage::LogMessage {
786            name,
787            message,
788            level_name,
789            time,
790            msg_id,
791        } = log_msg
792        {
793            log_message(level_name, time, name, message);
794
795            if let Some(msg_id) = msg_id {
796                if exit_code > 1 {
797                    return Err(PruneError::UnexpectedMessageId(msg_id));
798                }
799            }
800        }
801    }
802
803    if exit_code > 1 {
804        return Err(PruneError::Unknown(output));
805    }
806
807    Ok(())
808}
809
810pub(crate) fn mount_fmt_args(options: &MountOptions, common_options: &CommonOptions) -> String {
811    let mount_source_formatted = match &options.mount_source {
812        MountSource::Repository {
813            name,
814            first_n_archives,
815            last_n_archives,
816            glob_archives,
817        } => {
818            format!(
819                "{name}{first_n_archives}{last_n_archives}{glob_archives}",
820                name = name.clone(),
821                first_n_archives = first_n_archives
822                    .map(|first_n| format!(" --first {}", first_n))
823                    .unwrap_or_default(),
824                last_n_archives = last_n_archives
825                    .map(|last_n| format!(" --last {}", last_n))
826                    .unwrap_or_default(),
827                glob_archives = glob_archives
828                    .as_ref()
829                    .map(|glob| format!(" --glob-archives {}", glob))
830                    .unwrap_or_default(),
831            )
832        }
833        MountSource::Archive { archive_name } => archive_name.clone(),
834    };
835    format!(
836        "--log-json {common_options} mount {mount_source} {mountpoint} {select_paths}",
837        common_options = String::from(common_options),
838        mount_source = mount_source_formatted,
839        mountpoint = options.mountpoint,
840        select_paths = options
841            .select_paths
842            .iter()
843            .map(|x| format!("--pattern={}", shell_escape(&x.to_string())))
844            .collect::<Vec<String>>()
845            .join(" "),
846    )
847    .trim()
848    .to_string()
849}
850
851pub(crate) fn mount_parse_output(res: Output) -> Result<(), MountError> {
852    let Some(exit_code) = res.status.code() else {
853        warn!("borg process was terminated by signal");
854        return Err(MountError::TerminatedBySignal);
855    };
856
857    let mut output = String::new();
858
859    for line in BufRead::lines(res.stderr.as_slice()) {
860        let line = line.map_err(MountError::InvalidBorgOutput)?;
861        writeln!(output, "{line}").unwrap();
862
863        trace!("borg output: {line}");
864
865        let log_msg = LoggingMessage::from_str(&line)?;
866
867        if let LoggingMessage::UMountError(message) = log_msg {
868            return Err(MountError::UMountError(message));
869        };
870
871        if let LoggingMessage::LogMessage {
872            name,
873            message,
874            level_name,
875            time,
876            msg_id,
877        } = log_msg
878        {
879            log_message(level_name, time, name, message);
880
881            if let Some(msg_id) = msg_id {
882                if exit_code > 1 {
883                    return Err(MountError::UnexpectedMessageId(msg_id));
884                }
885            }
886        }
887    }
888
889    if exit_code > 1 {
890        return Err(MountError::Unknown(output));
891    }
892    Ok(())
893}
894
895pub(crate) fn list_fmt_args(options: &ListOptions, common_options: &CommonOptions) -> String {
896    format!(
897        "--log-json {common_options} list --json {repository}",
898        common_options = String::from(common_options),
899        repository = shell_escape(&options.repository)
900    )
901}
902
903pub(crate) fn list_parse_output(res: Output) -> Result<ListRepository, ListError> {
904    let Some(exit_code) = res.status.code() else {
905        warn!("borg process was terminated by signal");
906        return Err(ListError::TerminatedBySignal);
907    };
908
909    let mut output = String::new();
910
911    for line in BufRead::lines(res.stderr.as_slice()) {
912        let line = line.map_err(ListError::InvalidBorgOutput)?;
913        writeln!(output, "{line}").unwrap();
914
915        trace!("borg output: {line}");
916
917        let log_msg = LoggingMessage::from_str(&line)?;
918
919        if let LoggingMessage::LogMessage {
920            name,
921            message,
922            level_name,
923            time,
924            msg_id,
925        } = log_msg
926        {
927            log_message(level_name, time, name, message);
928
929            if let Some(msg_id) = msg_id {
930                match msg_id {
931                    MessageId::RepositoryDoesNotExist => {
932                        return Err(ListError::RepositoryDoesNotExist);
933                    }
934                    MessageId::PassphraseWrong => {
935                        return Err(ListError::PassphraseWrong);
936                    }
937                    _ => {
938                        if exit_code > 1 {
939                            return Err(ListError::UnexpectedMessageId(msg_id));
940                        }
941                    }
942                }
943            }
944        }
945    }
946
947    if exit_code > 1 {
948        return Err(ListError::Unknown(output));
949    }
950
951    trace!("Parsing output");
952    let list_repo: ListRepository = serde_json::from_slice(&res.stdout)?;
953
954    Ok(list_repo)
955}
956
957pub(crate) fn create_fmt_args(
958    options: &CreateOptions,
959    common_options: &CommonOptions,
960    progress: bool,
961) -> String {
962    format!(
963        "--log-json{p} {common_options}create --json{comment}{compression}{num_ids}{sparse}{read_special}{no_xattr}{no_acls}{no_flags}{ex_caches}{patterns}{excludes}{pattern_file}{exclude_file} {repo}::{archive} {paths}",
964        common_options = String::from(common_options),
965        p = if progress { " --progress" } else { "" },
966        comment = options.comment.as_ref().map_or("".to_string(), |x| format!(
967            " --comment {}",
968            shell_escape(x)
969        )),
970        compression = options.compression.as_ref().map_or("".to_string(), |x| format!(" --compression {x}")),
971        num_ids = if options.numeric_ids { " --numeric-ids" } else { "" },
972        sparse = if options.sparse { " --sparse" } else { "" },
973        read_special = if options.read_special { " --read-special" } else { "" },
974        no_xattr = if options.no_xattrs { " --noxattrs" } else { "" },
975        no_acls = if options.no_acls { " --noacls" } else { "" },
976        no_flags = if options.no_flags { " --noflags" } else { "" },
977        ex_caches = if options.exclude_caches { " --exclude-caches" } else {""},
978        patterns = options.patterns.iter().map(|x| format!(
979            " --pattern={}",
980            shell_escape(&x.to_string()),
981        )).collect::<Vec<String>>().join(" "),
982        excludes = options.excludes.iter().map(|x| format!(
983            " --exclude={}",
984            shell_escape(&x.to_string()),
985        )).collect::<Vec<String>>().join(" "),
986        pattern_file = options.pattern_file.as_ref().map_or(
987            "".to_string(),
988            |x| format!(" --patterns-from {}", shell_escape(x)),
989        ),
990        exclude_file = options.exclude_file.as_ref().map_or(
991            "".to_string(),
992            |x| format!(" --exclude-from {}", shell_escape(x)),
993        ),
994        repo = shell_escape(&options.repository),
995        archive = shell_escape(&options.archive),
996        paths = options.paths.join(" "),
997    )
998}
999
1000pub(crate) fn create_parse_output(res: Output) -> Result<Create, CreateError> {
1001    let Some(exit_code) = res.status.code() else {
1002        warn!("borg process was terminated by signal");
1003        return Err(CreateError::TerminatedBySignal);
1004    };
1005
1006    let mut output = String::new();
1007
1008    for line in BufRead::lines(res.stderr.as_slice()) {
1009        let line = line.map_err(CreateError::InvalidBorgOutput)?;
1010        writeln!(output, "{line}").unwrap();
1011
1012        trace!("borg output: {line}");
1013
1014        let log_msg = LoggingMessage::from_str(&line)?;
1015
1016        if let LoggingMessage::LogMessage {
1017            name,
1018            message,
1019            level_name,
1020            time,
1021            msg_id,
1022        } = log_msg
1023        {
1024            log_message(level_name, time, name, message);
1025
1026            if let Some(msg_id) = msg_id {
1027                match msg_id {
1028                    MessageId::ArchiveAlreadyExists => {
1029                        return Err(CreateError::ArchiveAlreadyExists)
1030                    }
1031                    MessageId::PassphraseWrong => {
1032                        return Err(CreateError::PassphraseWrong);
1033                    }
1034                    _ => {
1035                        if exit_code > 1 {
1036                            return Err(CreateError::UnexpectedMessageId(msg_id));
1037                        }
1038                    }
1039                }
1040            }
1041        }
1042    }
1043
1044    if exit_code > 1 {
1045        return Err(CreateError::Unknown(output));
1046    }
1047
1048    trace!("Parsing stats");
1049    let stats: Create = serde_json::from_slice(&res.stdout)?;
1050
1051    Ok(stats)
1052}
1053
1054pub(crate) fn compact_fmt_args(options: &CompactOptions, common_options: &CommonOptions) -> String {
1055    format!(
1056        "--log-json {common_options}compact {repository}",
1057        common_options = String::from(common_options),
1058        repository = shell_escape(&options.repository)
1059    )
1060}
1061
1062pub(crate) fn compact_parse_output(res: Output) -> Result<(), CompactError> {
1063    let Some(exit_code) = res.status.code() else {
1064        warn!("borg process was terminated by signal");
1065        return Err(CompactError::TerminatedBySignal);
1066    };
1067
1068    let mut output = String::new();
1069
1070    for line in BufRead::lines(res.stderr.as_slice()) {
1071        let line = line.map_err(CompactError::InvalidBorgOutput)?;
1072        writeln!(output, "{line}").unwrap();
1073
1074        trace!("borg output: {line}");
1075
1076        let log_msg = LoggingMessage::from_str(&line)?;
1077
1078        if let LoggingMessage::LogMessage {
1079            name,
1080            message,
1081            level_name,
1082            time,
1083            msg_id,
1084        } = log_msg
1085        {
1086            log_message(level_name, time, name, message);
1087
1088            if let Some(msg_id) = msg_id {
1089                if exit_code > 1 {
1090                    return Err(CompactError::UnexpectedMessageId(msg_id));
1091                }
1092            }
1093        }
1094    }
1095
1096    if exit_code > 1 {
1097        return Err(CompactError::Unknown(output));
1098    }
1099
1100    Ok(())
1101}
1102
1103#[cfg(test)]
1104mod tests {
1105    use std::num::NonZeroU16;
1106
1107    use crate::common::{
1108        mount_fmt_args, prune_fmt_args, CommonOptions, MountOptions, MountSource, Pattern,
1109        PruneOptions,
1110    };
1111    #[test]
1112    fn test_prune_fmt_args() {
1113        let mut prune_option = PruneOptions::new("prune_option_repo".to_string());
1114        prune_option.keep_secondly = NonZeroU16::new(1);
1115        prune_option.keep_minutely = NonZeroU16::new(2);
1116        prune_option.keep_hourly = NonZeroU16::new(3);
1117        prune_option.keep_daily = NonZeroU16::new(4);
1118        prune_option.keep_weekly = NonZeroU16::new(5);
1119        prune_option.keep_monthly = NonZeroU16::new(6);
1120        prune_option.keep_yearly = NonZeroU16::new(7);
1121        let args = prune_fmt_args(&prune_option, &CommonOptions::default());
1122        assert_eq!("--log-json  prune --keep-secondly 1 --keep-minutely 2 --keep-hourly 3 --keep-daily 4 --keep-weekly 5 --keep-monthly 6 --keep-yearly 7 'prune_option_repo'", args);
1123    }
1124    #[test]
1125    fn test_mount_fmt_args() {
1126        let mount_option = MountOptions::new(
1127            MountSource::Archive {
1128                archive_name: "/tmp/borg-repo::archive".to_string(),
1129            },
1130            String::from("/mnt/borg-mount"),
1131        );
1132        let args = mount_fmt_args(&mount_option, &CommonOptions::default());
1133        assert_eq!(
1134            "--log-json  mount /tmp/borg-repo::archive /mnt/borg-mount",
1135            args
1136        );
1137    }
1138    #[test]
1139    fn test_mount_fmt_args_patterns() {
1140        let mut mount_option = MountOptions::new(
1141            MountSource::Archive {
1142                archive_name: "/my-borg-repo".to_string(),
1143            },
1144            String::from("/borg-mount"),
1145        );
1146        mount_option.select_paths = vec![
1147            Pattern::Shell("**/test/*".to_string()),
1148            Pattern::Regex("^[A-Z]{3}".to_string()),
1149        ];
1150        let args = mount_fmt_args(&mount_option, &CommonOptions::default());
1151        assert_eq!(
1152            "--log-json  mount /my-borg-repo /borg-mount --pattern='sh:**/test/*' --pattern='re:^[A-Z]{3}'",
1153            args
1154        );
1155    }
1156    #[test]
1157    fn test_mount_fmt_args_repo() {
1158        let mut mount_option = MountOptions::new(
1159            MountSource::Repository {
1160                name: "/my-repo".to_string(),
1161                first_n_archives: Some(NonZeroU16::new(10).unwrap()),
1162                last_n_archives: Some(NonZeroU16::new(5).unwrap()),
1163                glob_archives: Some("archive-name*12-2022*".to_string()),
1164            },
1165            String::from("/borg-mount"),
1166        );
1167        mount_option.select_paths = vec![Pattern::Shell("**/foobar/*".to_string())];
1168        let args = mount_fmt_args(&mount_option, &CommonOptions::default());
1169        assert_eq!(
1170            "--log-json  mount /my-repo --first 10 --last 5 --glob-archives archive-name*12-2022* /borg-mount --pattern='sh:**/foobar/*'",
1171            args
1172        );
1173    }
1174}