1use 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#[derive(Serialize, Deserialize, Debug, Clone)]
27pub enum PatternInstruction {
28 Root(String),
30 Include(Pattern),
32 Exclude(Pattern),
34 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#[derive(Serialize, Deserialize, Debug, Clone)]
64pub enum Pattern {
65 FnMatch(String),
84 Shell(String),
91 Regex(String),
103 PathPrefix(String),
108 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#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
142pub enum CompressionMode {
143 None,
145 Lz4,
147 Zstd(u8),
153 Zlib(u8),
159 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#[derive(Serialize, Deserialize, Debug, Clone)]
185pub enum EncryptionMode {
186 None,
190 Authenticated(String),
192 AuthenticatedBlake2(String),
194 Repokey(String),
201 Keyfile(String),
208 RepokeyBlake2(String),
215 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#[derive(Deserialize, Serialize, Debug, Clone, Default)]
254pub struct CommonOptions {
255 pub local_path: Option<String>,
257 pub remote_path: Option<String>,
259 pub upload_ratelimit: Option<u64>,
261 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#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
289pub enum PruneWithinTime {
290 Hour,
292 Day,
294 Week,
296 Month,
298 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#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
317pub struct PruneWithin {
318 pub quantifier: NonZeroU16,
320 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#[derive(Serialize, Deserialize, Debug, Clone)]
345pub struct PruneOptions {
346 pub repository: String,
353 pub passphrase: Option<String>,
358 pub keep_within: Option<PruneWithin>,
361 pub keep_secondly: Option<NonZeroU16>,
363 pub keep_minutely: Option<NonZeroU16>,
365 pub keep_hourly: Option<NonZeroU16>,
367 pub keep_daily: Option<NonZeroU16>,
369 pub keep_weekly: Option<NonZeroU16>,
371 pub keep_monthly: Option<NonZeroU16>,
373 pub keep_yearly: Option<NonZeroU16>,
375 pub checkpoint_interval: Option<NonZeroU16>,
377 pub glob_archives: Option<String>,
381}
382
383impl PruneOptions {
384 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#[derive(Serialize, Deserialize, Debug, Clone)]
408pub enum MountSource {
409 Repository {
411 name: String,
418 first_n_archives: Option<NonZeroU16>,
420 last_n_archives: Option<NonZeroU16>,
422 glob_archives: Option<String>,
424 },
425 Archive {
427 archive_name: String,
434 },
435}
436
437#[derive(Serialize, Deserialize, Debug, Clone)]
442pub struct MountOptions {
443 pub mount_source: MountSource,
445 pub mountpoint: String,
450 pub passphrase: Option<String>,
455 pub select_paths: Vec<Pattern>,
462}
463
464impl MountOptions {
465 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#[derive(Serialize, Deserialize, Clone, Debug)]
478pub struct CompactOptions {
479 pub repository: String,
486}
487
488#[derive(Serialize, Deserialize, Debug, Clone)]
490pub struct CreateOptions {
491 pub repository: String,
498 pub archive: String,
510 pub passphrase: Option<String>,
515 pub comment: Option<String>,
517 pub compression: Option<CompressionMode>,
521 pub paths: Vec<String>,
525 pub exclude_caches: bool,
528 pub patterns: Vec<PatternInstruction>,
542 pub pattern_file: Option<String>,
547 pub excludes: Vec<Pattern>,
551 pub exclude_file: Option<String>,
556 pub numeric_ids: bool,
558 pub sparse: bool,
560 pub read_special: bool,
564 pub no_xattrs: bool,
566 pub no_acls: bool,
568 pub no_flags: bool,
570}
571
572impl CreateOptions {
573 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#[derive(Serialize, Deserialize, Debug, Clone)]
604pub struct InitOptions {
605 pub repository: String,
612 pub encryption_mode: EncryptionMode,
614 pub append_only: bool,
621 pub make_parent_dirs: bool,
625 pub storage_quota: Option<String>,
629}
630
631impl InitOptions {
632 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#[derive(Deserialize, Serialize, Debug, Clone)]
648pub struct ListOptions {
649 pub repository: String,
656 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}