diff --git a/Cargo.lock b/Cargo.lock index fa0636986..5f15ee20e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,6 +263,16 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "asyncband" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a214ba60d6231afd0e805e3c27c45a1626d9debaa5a5061c45a1ea1b2f1ed0" +dependencies = [ + "hashbrown 0.17.1", + "slab", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -2564,9 +2574,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "opendal" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f20562cc7447fcc915fc5c23df305a412ea80a733c9f2fd9e2d267e2815be6d" +checksum = "33dbff14cc9bb085224256d6a81289d2f3202e85b06f408d42534b42162a4231" dependencies = [ "ctor", "opendal-core", @@ -2603,11 +2613,12 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec75551ff4cf3e57da98979f6a937aaa9ddb3915bf68cc17d03df733be6646ed" +checksum = "48dbcef97d3eb7591db2c18d5cae95c836bcce07359b98d98dd6f4e861eb77b7" dependencies = [ "anyhow", + "asyncband", "base64 0.23.1", "bytes", "futures", @@ -2615,7 +2626,6 @@ dependencies = [ "jiff", "log", "md-5", - "mea", "percent-encoding", "quick-xml", "reqsign-core", @@ -2629,9 +2639,9 @@ dependencies = [ [[package]] name = "opendal-http-transport-reqwest" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad4d4f19c3ce01126a30611f8e544eaa217104a278c889ac17c9374fe4f9e4ef" +checksum = "85663452ea32bbc17e8f79ab29788c846d116ec7de31451be9c787e462dcb36c" dependencies = [ "bytes", "futures", @@ -2643,21 +2653,21 @@ dependencies = [ [[package]] name = "opendal-layer-concurrent-limit" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "249ac5b0aa5a7a6c3737342d10456067937f9c9a6f3f02544271f7908ab91081" +checksum = "03f9e144b5228d741c3763ade8711d9b72e5fb6d998e779f2d7a09da0b5a3eba" dependencies = [ + "asyncband", "futures", "http", - "mea", "opendal-core", ] [[package]] name = "opendal-layer-logging" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c75411ab00f77851ff086b686c1e9ca8175ac18c15afa2cb75b9036436cb06c" +checksum = "c2de17c61cd32e9d8d7d8efb91795e714dbccbafbc3c4e219e1542f4d6324161" dependencies = [ "log", "opendal-core", @@ -2665,9 +2675,9 @@ dependencies = [ [[package]] name = "opendal-layer-retry" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80b7738bd5f233ad8da39af9b9316b9b7a4eaddd91e8e32a1e19b7030688121d" +checksum = "e94db301964a25366090484d61e6da16d5979cc8faf02c3e12210dc74fafed38" dependencies = [ "backon", "log", @@ -2676,9 +2686,9 @@ dependencies = [ [[package]] name = "opendal-layer-throttle" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885cc8f70e3d25945dec6d7fbd60b08d2129cdc04191f32aa176e3cb2274fe2c" +checksum = "126e7e819f2530a14c3082a0dbe93ea35459532f4c8d46b0c345acd1810d3400" dependencies = [ "governor", "opendal-core", @@ -2686,9 +2696,9 @@ dependencies = [ [[package]] name = "opendal-layer-timeout" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a704141924500f3803c05ed871b53305d2a2f11cb5ef20160c3ee688a1857f66" +checksum = "08956ddda07465449bfd48825f4f0f25e0351278ac974eaa659895d9d74f2c80" dependencies = [ "opendal-core", "tokio", @@ -2696,9 +2706,9 @@ dependencies = [ [[package]] name = "opendal-service-azblob" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3310fbbb48f111c6f590473c2cd15e1b7f8e384444b0d4e328f0464c864d767" +checksum = "6d278d2fb57661947d1c9fb44047e432b2782c48dc085b7abc99fe6e18c26cf8" dependencies = [ "base64 0.23.1", "bytes", @@ -2717,15 +2727,15 @@ dependencies = [ [[package]] name = "opendal-service-azdls" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e3c406729935fe214ce574d68681a1ff7e0b322548f14094912bdbfe50e5c53" +checksum = "2d564484a8f7d091827e825cfc91ed45bd48e64d262451ee041fe843db81bd8a" dependencies = [ + "asyncband", "base64 0.23.1", "bytes", "http", "log", - "mea", "opendal-core", "opendal-service-azure-common", "quick-xml", @@ -2738,9 +2748,9 @@ dependencies = [ [[package]] name = "opendal-service-azfile" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cd228c87087e4486c5a4d7b978b1db3ebed7f98351e6ed02e9f9bde24f3ce30" +checksum = "c85f0fbe58e733b6473138aa67cf616127a1ca4f4147567a913e9acb92f2b96a" dependencies = [ "bytes", "http", @@ -2756,9 +2766,9 @@ dependencies = [ [[package]] name = "opendal-service-azure-common" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7348c88edf15af435b7be930077746b569fac5e738c1bf6a363b675e7317c9df" +checksum = "cfcc1bfdac4f54d9018c462dd32ad9e2f68fdf584f825c811550c8ffc87894cd" dependencies = [ "http", "opendal-core", @@ -2766,14 +2776,14 @@ dependencies = [ [[package]] name = "opendal-service-b2" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f5d1fbf2a0ba77768270ec5bde55a7f5c496655e93a2d2ee85ff56a5325f2ac" +checksum = "d606353dd192c46db1a146d0085dadd4dba85a97d63a233851de0f5a4697ddb8" dependencies = [ + "asyncband", "bytes", "http", "log", - "mea", "opendal-core", "serde", "serde_json", @@ -2781,9 +2791,9 @@ dependencies = [ [[package]] name = "opendal-service-cos" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d533d4582105d269c8aebeee5f0e8bcf960f41b8aab6197df7012254d9f39bf0" +checksum = "bb021c128ebde42e6f3e719d4cfed27e994017aa503a7a1e5818bdb61fd67dc9" dependencies = [ "bytes", "http", @@ -2798,13 +2808,13 @@ dependencies = [ [[package]] name = "opendal-service-dropbox" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668e32a4c68d65dc21b646d96279e1a32d78a295e3ac3fe124b06c7f27fa061f" +checksum = "09a7f0c1f99885543e2647f5fd148b81fdeb21d2f5fd45e59090fad15fad9105" dependencies = [ + "asyncband", "bytes", "http", - "mea", "opendal-core", "serde", "serde_json", @@ -2812,9 +2822,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826c4e17a30643b888fe983897f9a4b23b07066e1d069727a923cc8fb419a702" +checksum = "c7ef1e1c45f3f89282a59073897e0d685e51385fed0aea771714789525cff996" dependencies = [ "bytes", "log", @@ -2826,9 +2836,9 @@ dependencies = [ [[package]] name = "opendal-service-ftp" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8226225287abcc9eb620b7924e5852963c51d017b0d56da70b088d68b640e207" +checksum = "f992e484df00e78880e7d066ecfb06170865e25cc064c5069bf81011c3dd56e3" dependencies = [ "bytes", "fastpool", @@ -2845,9 +2855,9 @@ dependencies = [ [[package]] name = "opendal-service-gcs" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007f3fba63c21e516c956b891e96ff9892d8175662bfb781cdada9d3766a11e6" +checksum = "da1f2a8c975fd22fea01f0409bcb8774f1ac02ad79863a3490098203121555d3" dependencies = [ "async-trait", "bytes", @@ -2866,14 +2876,14 @@ dependencies = [ [[package]] name = "opendal-service-gdrive" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856c93abe1eab4a8b119286a8260d468aa9a07f3951c2d5ace16832dfba9b79" +checksum = "bf817a82a81423bdda3c980e5d5b076f07364a71398dbf038d6220bb4f55b529" dependencies = [ + "asyncband", "bytes", "http", "log", - "mea", "opendal-core", "serde", "serde_json", @@ -2881,9 +2891,9 @@ dependencies = [ [[package]] name = "opendal-service-ghac" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dad7db6252879b8f864383027d8b91c57d5cc3c3f6269df5f61cea4a09d1079" +checksum = "21cbd6ce78d2d3687bc16f353b008bb160a9312d8614015b5646ccd55af19eb7" dependencies = [ "bytes", "ghac", @@ -2901,9 +2911,9 @@ dependencies = [ [[package]] name = "opendal-service-http" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fd1f0f28bd052e068d9bb4bc5d30a09de22a4f7cea24558dbeef2b91090cc3" +checksum = "a2ebcbdfad4b81a1e6dde70e898396e011dfd52b5f8639b2628656f7915d6f4c" dependencies = [ "http", "log", @@ -2913,9 +2923,9 @@ dependencies = [ [[package]] name = "opendal-service-ipmfs" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a3a82f09ace60a740033b112169259d07315ee35caf26c8f82ad3480b5c02b" +checksum = "f5ad5ec703ad8fee5e81a7a3c207952fcafec9b2c97a3b9a1d655b864f0f9600" dependencies = [ "bytes", "http", @@ -2927,9 +2937,9 @@ dependencies = [ [[package]] name = "opendal-service-obs" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a39f4a4e705d9fb375d53a0dbbdb6fc435a04b94e7697b86a365a6b4ea4b5639" +checksum = "9e03c635ea35bdb07c1168612d42386635d16a1583c753efaf70bc7c8ac68c83" dependencies = [ "bytes", "http", @@ -2944,14 +2954,14 @@ dependencies = [ [[package]] name = "opendal-service-onedrive" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb96d5fb183104a405b010d2131031438cb29227785a2a67187c7d27731a2ea" +checksum = "d2d251202faa1d3d7e05a02f4fcb7e56a8770dfcd69a5e5fdebc567dabd5133c" dependencies = [ + "asyncband", "bytes", "http", "log", - "mea", "opendal-core", "serde", "serde_json", @@ -2960,9 +2970,9 @@ dependencies = [ [[package]] name = "opendal-service-oss" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd528ec2d49c5ca69e674ffed7b3e0686fb9cfcfea0596870de381467fda4f1b" +checksum = "8e3ce7a2ceb925e0f28f545b169eb57d04cec6116aae94b8584d2bdbe7d456c6" dependencies = [ "bytes", "http", @@ -2977,9 +2987,9 @@ dependencies = [ [[package]] name = "opendal-service-pcloud" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df6dba66e62b66d1ebd1df8cb74505d60d2a9ed06688bdbcd9a814a9c3e81141" +checksum = "3939c52070fbda08691d25319e4af8965c1c52507a3e868915be5bc4e1303f9e" dependencies = [ "bytes", "http", @@ -2991,9 +3001,9 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58e80cdf192d7eff05feed747894d64f81905ac4eaf132edf7ea270abdd2d663" +checksum = "c64335f9f24ccb62ac36f1d976342b48611a75ba61979813a4f78a4ebd94de42" dependencies = [ "base64 0.23.1", "bytes", @@ -3012,9 +3022,9 @@ dependencies = [ [[package]] name = "opendal-service-sftp" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79feb943e8e0f2c8e57c98773e6c9e1006abf486710af78a9929fe22aab1684f" +checksum = "baf159e1edfbbdab07724396681862c5ecacbe75003495aeb0f3f58910638a50" dependencies = [ "bytes", "fastpool", @@ -3029,9 +3039,9 @@ dependencies = [ [[package]] name = "opendal-service-swift" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44595b08eb013c0003377f501c52823ea206b651e7f2cae0da33c7fcb2fbc928" +checksum = "63f9552485e97289d44f13577ca15fb62f95965e433a0ce1b5f613f8f52cc52b" dependencies = [ "base64 0.23.1", "bytes", @@ -3050,15 +3060,15 @@ dependencies = [ [[package]] name = "opendal-service-webdav" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743e25172ee8de7a4782eabd0d6030095b60558e8354e7422cd960c59adf2131" +checksum = "1fa9aec39b39efa0367020732991260373492ffb0273259591ec362b664f455f" dependencies = [ "anyhow", + "asyncband", "bytes", "http", "log", - "mea", "opendal-core", "quick-xml", "serde", @@ -3066,15 +3076,15 @@ dependencies = [ [[package]] name = "opendal-service-webhdfs" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8703b22fe6e6b8aef40a8fc884aba39c78405c657ec545134552f785c2f193a" +checksum = "3a0f7466fde4e0574935079401198c59980fe3dceb820c23a239e2fe513b1126" dependencies = [ "anyhow", + "asyncband", "bytes", "http", "log", - "mea", "opendal-core", "serde", "serde_json", @@ -3083,9 +3093,9 @@ dependencies = [ [[package]] name = "opendal-service-yandex-disk" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34926d9c5014176cee60177382bf99af378fa1084e135556b2bf0612c69ce695" +checksum = "8aec0fda74736af2b5b967e44b12a4a8a6d34a51ba3043681502424afb59ccf8" dependencies = [ "bytes", "http", @@ -4106,7 +4116,6 @@ checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", - "futures-channel", "futures-core", "futures-util", "http", @@ -4273,10 +4282,12 @@ dependencies = [ "conflate", "derive_setters", "displaydoc", + "futures-util", "hex", "jiff", "log", "opendal", + "opendal-http-transport-reqwest", "rand 0.10.2", "rayon", "reqwest", diff --git a/README.md b/README.md index 2b3ad3224..b93466a39 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ This crate exposes a few features for controlling dependency usage: ```rust use rustic_backend::BackendOptions; -use rustic_core::{ConfigOptions, KeyOptions, Repository, RepositoryOptions}; +use rustic_core::{Credentials, ConfigOptions, KeyOptions, Repository, RepositoryOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; @@ -78,10 +78,11 @@ fn main() -> Result<(), Box> { .to_backends()?; // Init repository - let repo_opts = RepositoryOptions::default().password("test"); + let creds = Credentials::Password("test".into()); + let repo_opts = RepositoryOptions::default(); let key_opts = KeyOptions::default(); let config_opts = ConfigOptions::default(); - let _repo = Repository::new(&repo_opts, &backends)?.init(&key_opts, &config_opts)?; + let _repo = Repository::new(&repo_opts, &backends)?.init(&creds, &key_opts, &config_opts)?; // -> use _repo for any operation on an open repository Ok(()) @@ -92,7 +93,7 @@ fn main() -> Result<(), Box> { ```rust use rustic_backend::BackendOptions; -use rustic_core::{BackupOptions, PathList, Repository, RepositoryOptions, SnapshotOptions}; +use rustic_core::{BackupOptions, Credentials, PathList, Repository, RepositoryOptions, SnapshotOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; @@ -107,10 +108,11 @@ fn main() -> Result<(), Box> { .to_backends()?; // Open repository - let repo_opts = RepositoryOptions::default().password("test"); + let creds = Credentials::Password("test".into()); + let repo_opts = RepositoryOptions::default(); let repo = Repository::new(&repo_opts, &backends)? - .open()? + .open(&creds)? .to_indexed_ids()?; let backup_opts = BackupOptions::default(); @@ -131,7 +133,7 @@ fn main() -> Result<(), Box> { ```rust use rustic_backend::BackendOptions; -use rustic_core::{LocalDestination, LsOptions, Repository, RepositoryOptions, RestoreOptions}; +use rustic_core::{Credentials, LocalDestination, LsOptions, Repository, RepositoryOptions, RestoreOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; @@ -145,9 +147,10 @@ fn main() -> Result<(), Box> { .to_backends()?; // Open repository - let repo_opts = RepositoryOptions::default().password("test"); + let creds = Credentials::Password("test".into()); + let repo_opts = RepositoryOptions::default(); let repo = Repository::new(&repo_opts, &backends)? - .open()? + .open(&creds)? .to_indexed()?; // use latest snapshot without filtering snapshots @@ -175,7 +178,7 @@ fn main() -> Result<(), Box> { ```rust use rustic_backend::BackendOptions; -use rustic_core::{CheckOptions, Repository, RepositoryOptions}; +use rustic_core::{CheckOptions, Credentials, Repository, RepositoryOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; @@ -189,8 +192,9 @@ fn main() -> Result<(), Box> { .to_backends()?; // Open repository - let repo_opts = RepositoryOptions::default().password("test"); - let repo = Repository::new(&repo_opts, &backends)?.open()?; + let creds = Credentials::Password("test".into()); + let repo_opts = RepositoryOptions::default(); + let repo = Repository::new(&repo_opts, &backends)?.open(&creds)?; // Check repository with standard options but omitting cache checks let opts = CheckOptions::default().trust_cache(true); diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 092c05474..ec44ead3a 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -35,13 +35,22 @@ cli = ["merge", "clap"] merge = ["dep:conflate"] clap = ["dep:clap"] opendal = [ + "reqwest", "dep:opendal", + "dep:opendal-http-transport-reqwest", "dep:rayon", "dep:tokio", - "tokio/rt-multi-thread", "dep:typed-path", + "tokio/rt-multi-thread", +] +reqwest = ["dep:reqwest"] +rest = [ + "reqwest", + "dep:backon", + "dep:futures-util", + "dep:tokio", + "tokio/rt-multi-thread", ] -rest = ["dep:reqwest", "dep:backon"] rclone = ["rest", "dep:rand", "dep:semver"] [dependencies] @@ -63,7 +72,10 @@ strum = { version = "0.28.0", features = ["derive"] } # general / backend choosing hex = { version = "0.4.3", features = ["serde"] } +rayon = { version = "1.11.0", optional = true } serde = { version = "1.0.228" } +tokio = { version = "1.49.0", optional = true, default-features = false } +typed-path = { version = "0.12.2", optional = true } url = "2.5.8" # cli support @@ -74,9 +86,12 @@ conflate = { version = "0.3.3", optional = true } aho-corasick = { workspace = true } walkdir = "2.5.0" +# reqwest +reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls", "stream"], optional = true } + # rest backend backon = { version = "1.6.0", optional = true } -reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls", "stream", "blocking"], optional = true } +futures-util = { version = "0.3.34", default-features = false, optional = true } # rclone backend rand = { version = "0.10.0", optional = true } @@ -84,18 +99,16 @@ semver = { version = "1.0.27", optional = true } # opendal backend bytesize = "2.3.1" -rayon = { version = "1.11.0", optional = true } -tokio = { version = "1.49.0", optional = true, default-features = false } -typed-path = { version = "0.12.2", optional = true } +opendal-http-transport-reqwest = { version = "0.58.2", optional = true } [target.'cfg(not(windows))'.dependencies] # opendal backend # - sftp is not supported on windows, see https://github.com/apache/incubator-opendal/issues/2963 -opendal = { version = "0.58.1", features = ["blocking", "services-b2", "services-ftp", "services-sftp", "services-swift", "services-azblob", "services-azdls", "services-cos", "services-fs", "services-dropbox", "services-gdrive", "services-gcs", "services-ghac", "services-http", "services-ipmfs", "services-memory", "services-obs", "services-onedrive", "services-oss", "services-pcloud", "services-s3", "services-webdav", "services-webhdfs", "services-azfile", "layers-throttle", "services-yandex-disk"], optional = true } +opendal = { version = "0.58.2", features = ["blocking", "services-b2", "services-ftp", "services-sftp", "services-swift", "services-azblob", "services-azdls", "services-cos", "services-fs", "services-dropbox", "services-gdrive", "services-gcs", "services-ghac", "services-http", "services-ipmfs", "services-memory", "services-obs", "services-onedrive", "services-oss", "services-pcloud", "services-s3", "services-webdav", "services-webhdfs", "services-azfile", "layers-throttle", "services-yandex-disk"], optional = true } [target.'cfg(windows)'.dependencies] # opendal backend -opendal = { version = "0.58.1", features = ["blocking", "services-b2", "services-ftp", "services-swift", "services-azblob", "services-azdls", "services-cos", "services-fs", "services-dropbox", "services-gdrive", "services-gcs", "services-ghac", "services-http", "services-ipmfs", "services-memory", "services-obs", "services-onedrive", "services-oss", "services-pcloud", "services-s3", "services-webdav", "services-webhdfs", "services-azfile", "layers-throttle", "services-yandex-disk"], optional = true } +opendal = { version = "0.58.2", features = ["blocking", "services-b2", "services-ftp", "services-swift", "services-azblob", "services-azdls", "services-cos", "services-fs", "services-dropbox", "services-gdrive", "services-gcs", "services-ghac", "services-http", "services-ipmfs", "services-memory", "services-obs", "services-onedrive", "services-oss", "services-pcloud", "services-s3", "services-webdav", "services-webhdfs", "services-azfile", "layers-throttle", "services-yandex-disk"], optional = true } [dev-dependencies] anyhow = { workspace = true } diff --git a/crates/backend/README.md b/crates/backend/README.md index 1064b829e..8c48bb7e2 100644 --- a/crates/backend/README.md +++ b/crates/backend/README.md @@ -10,6 +10,16 @@

+

+Most backends featured by
+ + + +Powered by Apache OpenDAL + + +

+ ## News We temporarily disabled the `service-ftp` feature on the `opendal` backend due diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index 7ce3da126..75ba455d9 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -61,6 +61,9 @@ pub mod local; /// Utility functions for the backend. pub mod util; +#[cfg(feature = "reqwest")] +pub(crate) mod reqwest; + /// `OpenDAL` backend for Rustic. #[cfg(feature = "opendal")] pub mod opendal; diff --git a/crates/backend/src/local.rs b/crates/backend/src/local.rs index 585e629e8..1a89a7e77 100644 --- a/crates/backend/src/local.rs +++ b/crates/backend/src/local.rs @@ -522,6 +522,8 @@ impl WriteBackend for LocalBackend { trace!("writing tpe: {tpe:?}, id: {id}"); let filename = self.path(tpe, id); + let replaces_existing_config = + tpe == FileType::Config && self.post_delete_command.is_some() && filename.exists(); let parent = self.base_path(tpe, id); @@ -565,6 +567,14 @@ impl WriteBackend for LocalBackend { .ask_report() })?; + // Replacing the config bypasses `remove`, so invoke its hook explicitly. + if replaces_existing_config + && let Some(command) = &self.post_delete_command + && let Err(err) = Self::call_command(tpe, id, &filename, command) + { + warn!("post-delete: {}", err.display_log()); + } + if let Some(command) = &self.post_create_command && let Err(err) = Self::call_command(tpe, id, &filename, command) { @@ -603,3 +613,63 @@ impl WriteBackend for LocalBackend { Ok(()) } } + +#[cfg(all(test, unix))] +mod tests { + use std::{ + env, fs, process, + time::{SystemTime, UNIX_EPOCH}, + }; + + use bytes::Bytes; + use rustic_core::{FileType, Id, WriteBackend}; + + use super::LocalBackend; + + #[test] + fn replacing_config_runs_delete_hook_before_create_hook() { + let repo = env::temp_dir().join(format!( + "rustic-local-backend-test-{}-{}", + process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir(&repo).unwrap(); + let marker = repo.join("hooks-ran"); + let options = [ + ( + "post-create-command".to_string(), + format!("sh -c 'printf create >> {}'", marker.display()), + ), + ( + "post-delete-command".to_string(), + format!("sh -c 'printf delete >> {}'", marker.display()), + ), + ]; + let backend = LocalBackend::new(repo.to_string_lossy(), options).unwrap(); + backend.create().unwrap(); + + backend + .write_bytes( + FileType::Config, + &Id::default(), + false, + Bytes::from_static(b"first").into(), + ) + .unwrap(); + backend + .write_bytes( + FileType::Config, + &Id::default(), + false, + Bytes::from_static(b"second").into(), + ) + .unwrap(); + + let hooks_ran = fs::read_to_string(marker).unwrap(); + fs::remove_dir_all(repo).unwrap(); + assert_eq!(hooks_ran, "createdeletecreate"); + } +} diff --git a/crates/backend/src/opendal.rs b/crates/backend/src/opendal.rs index b82d59f21..c830c8cea 100644 --- a/crates/backend/src/opendal.rs +++ b/crates/backend/src/opendal.rs @@ -11,11 +11,14 @@ use bytes::Bytes; use bytesize::ByteSize; use log::{error, trace, warn}; use opendal::{ - Entry, Metadata, + Entry, HttpTransporter, Metadata, OperationContext, blocking::{Operator, StdReader}, - layers::{ConcurrentLimitLayer, LoggingLayer, RetryLayer, ThrottleLayer}, + layers::{ + ConcurrentLimitLayer, LoggingLayer, RetryEvent, RetryInterceptor, RetryLayer, ThrottleLayer, + }, options::{ListOptions, ReadOptions}, }; +use opendal_http_transport_reqwest::ReqwestTransport; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use tokio::runtime::Runtime; use typed_path::UnixPathBuf; @@ -26,6 +29,8 @@ use rustic_core::{ repofile::{Node, NodeType}, }; +use crate::reqwest::reqwest_client; + mod constants { /// Default number of retries pub(super) const DEFAULT_RETRY: usize = 5; @@ -47,6 +52,26 @@ fn runtime() -> &'static Runtime { }) } +/// Log `OpenDAL` retries as a single line using `Display`, not `Debug`. +/// +/// `OpenDAL`'s default interceptor formats the error with `{:?}`, which dumps +/// the full context/source tree across many lines and fights progress/TUI +/// output. +#[derive(Clone, Copy, Debug)] +struct CompactRetryInterceptor; + +impl RetryInterceptor for CompactRetryInterceptor { + fn intercept(&self, event: RetryEvent<'_>) { + warn!( + "Error {err} at {duration:?}, retrying {op:?} (attempt {attempt})", + err = event.err, + duration = event.retry_after, + op = event.op, + attempt = event.attempt, + ); + } +} + /// Throttling parameters /// /// Note: Throttle implements [`FromStr`] to read it from something like "10kiB,10MB" @@ -144,6 +169,10 @@ impl OpenDALBackend { .map(|t| Throttle::from_str(t)) .transpose()?; + let client = reqwest_client(&options)?; + let http_transport = HttpTransporter::new(ReqwestTransport::new(client)); + let operation_context = OperationContext::new().with_http_transport(http_transport); + let scheme = path .as_ref() .split(':') @@ -159,8 +188,13 @@ impl OpenDALBackend { ) .attach_context("path", path.as_ref().to_string()) .attach_context("schema", scheme.to_string()) - })? - .layer(RetryLayer::new().with_max_times(max_retries).with_jitter()); + })?.with_context(operation_context) + .layer( + RetryLayer::new() + .with_max_times(max_retries) + .with_jitter() + .with_notify(CompactRetryInterceptor), + ); if let Some(Throttle { bandwidth, burst }) = throttle { operator = operator.layer(ThrottleLayer::new(bandwidth, burst)); @@ -647,6 +681,27 @@ mod tests { Ok(()) } + + #[test] + fn opendal_error_display_is_single_line() { + let err = opendal::Error::new(opendal::ErrorKind::Unexpected, "send http request") + .with_operation("read") + .with_context("url", "https://example.com/index/abc") + .set_source(std::io::Error::other( + "connection closed before message completed", + )); + + let display = err.to_string(); + let debug = format!("{err:?}"); + assert!( + !display.contains('\n'), + "Display should be one line, got: {display:?}" + ); + assert!( + debug.contains("Context:"), + "Debug should dump context, got: {debug}" + ); + } } #[derive(Debug)] diff --git a/crates/backend/src/reqwest.rs b/crates/backend/src/reqwest.rs new file mode 100644 index 000000000..cc40f4e42 --- /dev/null +++ b/crates/backend/src/reqwest.rs @@ -0,0 +1,119 @@ +use std::str::FromStr; +use std::{collections::BTreeMap, io::Read}; + +use jiff::SignedDuration; +use log::warn; +use reqwest::{ + Client, ClientBuilder, + header::{HeaderMap, HeaderValue}, +}; + +use rustic_core::{ErrorKind, RusticError, RusticResult}; + +pub(super) mod constants { + use std::time::Duration; + + /// Default timeout for the client + /// This is set to 10 minutes + pub(super) const DEFAULT_TIMEOUT: Duration = Duration::from_mins(10); +} + +fn read_file_contents(log_name: &str, path: &str) -> RusticResult> { + let mut buf = Vec::new(); + let _ = std::fs::File::open(path) + .map_err(|err| { + RusticError::with_source( + ErrorKind::InvalidInput, + "Cannot open {log_name} `{path}`", + err, + ) + .attach_context("path", path) + .attach_context("log_name", log_name) + })? + .read_to_end(&mut buf) + .map_err(|err| { + RusticError::with_source( + ErrorKind::InvalidInput, + "Cannot read {log_name} `{path}`", + err, + ) + .attach_context("path", path) + .attach_context("log_name", log_name) + })?; + Ok(buf) +} + +fn get_cacert(value: &str) -> RusticResult { + let buf = read_file_contents("cacert", value)?; + reqwest::Certificate::from_pem(&buf).map_err(|err| { + RusticError::with_source( + ErrorKind::InvalidInput, + "Cannot parse cacert `{value}`", + err, + ) + .attach_context("value", value) + }) +} + +fn get_tls_client_cert(value: &str) -> RusticResult { + let buf = read_file_contents("tls-client-cert", value)?; + reqwest::Identity::from_pem(&buf).map_err(|err| { + RusticError::with_source( + ErrorKind::InvalidInput, + "Cannot parse tls-client-cert `{value}`", + err, + ) + .attach_context("value", value) + }) +} + +pub fn reqwest_client(options: &BTreeMap) -> RusticResult { + let mut headers = HeaderMap::new(); + _ = headers.insert("User-Agent", HeaderValue::from_static("rustic")); + + // set default timeout to 10 minutes (we can have *large* packfiles) + let mut timeout = constants::DEFAULT_TIMEOUT; + + let mut client_builder = ClientBuilder::new().default_headers(headers); + + for (option, value) in options { + if option == "timeout" { + timeout = SignedDuration::from_str(value).map_err(|err| { + RusticError::with_source( + ErrorKind::InvalidInput, + "Could not parse value `{value}` as duration. Invalid value for option `{option}`.", + err, + ) + .attach_context("value", value) + .attach_context("option", "timeout") + })?.try_into() + // ignore conversation errors, but print out warning + .inspect_err(|err| warn!("cannot use timeout: {err}")) + .unwrap_or_default(); + } else if option == "cacert" { + client_builder = client_builder.add_root_certificate(get_cacert(value)?); + } else if option == "tls-client-cert" { + client_builder = client_builder.identity(get_tls_client_cert(value)?); + } else if option == "http-insecure-tls" { + match value.parse() { + Err(err) => warn!("cannot use value for `http-insecure-tls`: {err}"), + Ok(insecure_tls) => { + client_builder = client_builder.danger_accept_invalid_certs(insecure_tls); + } + } + } else if option == "http-referer" { + match value.parse() { + Err(err) => warn!("cannot use value for `http-referer`: {err}"), + Ok(referer) => { + client_builder = client_builder.referer(referer); + } + } + } + } + + let client = client_builder.timeout(timeout).build().map_err(|err| { + RusticError::with_source(ErrorKind::Backend, "Failed to build HTTP client", err) + })?; + + Ok(client) +} diff --git a/crates/backend/src/rest.rs b/crates/backend/src/rest.rs index 63133f52f..343b4c0a0 100644 --- a/crates/backend/src/rest.rs +++ b/crates/backend/src/rest.rs @@ -1,35 +1,32 @@ -use std::io::Read; +use std::collections::BTreeMap; use std::str::FromStr; +use std::sync::OnceLock; use std::time::Duration; use backon::{BlockingRetryable, ExponentialBuilder}; use bytes::Bytes; -use jiff::SignedDuration; +use futures_util::stream; use log::{trace, warn}; use reqwest::{ - Url, - blocking::{Body, Client, ClientBuilder}, + Body, Client, Url, header::{HeaderMap, HeaderValue}, }; use serde::Deserialize; +use tokio::runtime::Runtime; use rustic_core::{ BytesList, ErrorKind, FileType, Id, ReadBackend, RusticError, RusticResult, WriteBackend, }; +use crate::reqwest::reqwest_client; + /// joining URL failed on: `{0}` #[derive(thiserror::Error, Clone, Copy, Debug, displaydoc::Display)] pub struct JoiningUrlFailedError(url::ParseError); pub(super) mod constants { - use std::time::Duration; - /// Default number of retries pub(super) const DEFAULT_RETRY: usize = 5; - - /// Default timeout for the client - /// This is set to 10 minutes - pub(super) const DEFAULT_TIMEOUT: Duration = Duration::from_mins(10); } fn construct_backoff_error(err: reqwest::Error) -> Box { @@ -40,55 +37,6 @@ fn construct_backoff_error(err: reqwest::Error) -> Box { ) } -fn read_file_contents(log_name: &str, path: &str) -> RusticResult> { - let mut buf = Vec::new(); - let _ = std::fs::File::open(path) - .map_err(|err| { - RusticError::with_source( - ErrorKind::InvalidInput, - "Cannot open {log_name} `{path}`", - err, - ) - .attach_context("path", path) - .attach_context("log_name", log_name) - })? - .read_to_end(&mut buf) - .map_err(|err| { - RusticError::with_source( - ErrorKind::InvalidInput, - "Cannot read {log_name} `{path}`", - err, - ) - .attach_context("path", path) - .attach_context("log_name", log_name) - })?; - Ok(buf) -} - -fn get_cacert(value: &str) -> RusticResult { - let buf = read_file_contents("cacert", value)?; - reqwest::Certificate::from_pem(&buf).map_err(|err| { - RusticError::with_source( - ErrorKind::InvalidInput, - "Cannot parse cacert `{value}`", - err, - ) - .attach_context("value", value) - }) -} - -fn get_tls_client_cert(value: &str) -> RusticResult { - let buf = read_file_contents("tls-client-cert", value)?; - reqwest::Identity::from_pem(&buf).map_err(|err| { - RusticError::with_source( - ErrorKind::InvalidInput, - "Cannot parse tls-client-cert `{value}`", - err, - ) - .attach_context("value", value) - }) -} - /// A backend implementation that uses REST to access the backend. #[derive(Clone, Debug)] pub struct RestBackend { @@ -100,6 +48,16 @@ pub struct RestBackend { backoff: ExponentialBuilder, } +fn runtime() -> &'static Runtime { + static RUNTIME: OnceLock = OnceLock::new(); + RUNTIME.get_or_init(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + }) +} + impl RestBackend { /// Call the given operation retrying non-permanent errors and giving warnings for failed operations /// @@ -137,10 +95,7 @@ impl RestBackend { /// /// * If the url could not be parsed. /// * If the client could not be built. - pub fn new( - url: impl AsRef, - options: impl IntoIterator, - ) -> RusticResult { + pub fn new(url: impl AsRef, options: BTreeMap) -> RusticResult { let url = url.as_ref().to_string(); let url = if url.ends_with('/') { @@ -160,10 +115,7 @@ impl RestBackend { let mut headers = HeaderMap::new(); _ = headers.insert("User-Agent", HeaderValue::from_static("rustic")); - // set default timeout to 10 minutes (we can have *large* packfiles) - let mut timeout = constants::DEFAULT_TIMEOUT; - - let mut client_builder = ClientBuilder::new().default_headers(headers); + let client = reqwest_client(&options)?; // backon doesn't allow us to specify `None` for `max_delay` // see @@ -171,7 +123,6 @@ impl RestBackend { .with_max_delay(Duration::MAX) // no maximum elapsed time; we count number of retries .with_max_times(constants::DEFAULT_RETRY); - // FIXME: If we have multiple times the same option, this could lead to unexpected behavior for (option, value) in options { if option == "retry" { let max_retries = match value.as_str() { @@ -188,30 +139,9 @@ impl RestBackend { })?, }; backoff = backoff.with_max_times(max_retries); - } else if option == "timeout" { - timeout = SignedDuration::from_str(&value).map_err(|err| { - RusticError::with_source( - ErrorKind::InvalidInput, - "Could not parse value `{value}` as duration. Invalid value for option `{option}`.", - err, - ) - .attach_context("value", value) - .attach_context("option", "timeout") - })?.try_into() - // ignore conversation errors, but print out warning - .inspect_err(|err| warn!("cannot use timeout: {err}")) - .unwrap_or_default(); - } else if option == "cacert" { - client_builder = client_builder.add_root_certificate(get_cacert(&value)?); - } else if option == "tls-client-cert" { - client_builder = client_builder.identity(get_tls_client_cert(&value)?); } } - let client = client_builder.timeout(timeout).build().map_err(|err| { - RusticError::with_source(ErrorKind::Backend, "Failed to build HTTP client", err) - })?; - Ok(Self { url, client, @@ -300,32 +230,43 @@ impl ReadBackend for RestBackend { })?; self.retry_notify(|| { - if tpe == FileType::Config { - return Ok( - if self.client.head(url.clone()).send()?.status().is_success() { - vec![(Id::default(), 0)] - } else { - Vec::new() - }, - ); - } - - let list = self - .client - .get(url.clone()) - .header("Accept", "application/vnd.x.restic.rest.v2") - .send()? - .error_for_status()? - .json::>>()? // use Option to be handle null json value - .unwrap_or_default(); - - Ok(list - .into_iter() - .filter_map(|entry| { - let id = Id::parse_some(&entry.name, tpe)?; - Some((id, entry.size)) - }) - .collect()) + runtime().block_on(async { + if tpe == FileType::Config { + return Ok( + if self + .client + .head(url.clone()) + .send() + .await? + .status() + .is_success() + { + vec![(Id::default(), 0)] + } else { + Vec::new() + }, + ); + } + + let list = self + .client + .get(url.clone()) + .header("Accept", "application/vnd.x.restic.rest.v2") + .send() + .await? + .error_for_status()? + .json::>>() // use Option to handle null json value + .await? + .unwrap_or_default(); + + Ok(list + .into_iter() + .filter_map(|entry| { + let id = Id::parse_some(&entry.name, tpe)?; + Some((id, entry.size)) + }) + .collect()) + }) }) .map_err(construct_backoff_error) } @@ -349,11 +290,15 @@ impl ReadBackend for RestBackend { .map_err(|err| construct_join_url_error(err, tpe, id, &self.url))?; self.retry_notify(|| { - self.client - .get(url.clone()) - .send()? - .error_for_status()? - .bytes() + runtime().block_on(async { + self.client + .get(url.clone()) + .send() + .await? + .error_for_status()? + .bytes() + .await + }) }) .map_err(construct_backoff_error) } @@ -391,12 +336,16 @@ impl ReadBackend for RestBackend { })?; self.retry_notify(|| { - self.client - .get(url.clone()) - .header("Range", header_value.clone()) - .send()? - .error_for_status()? - .bytes() + runtime().block_on(async { + self.client + .get(url.clone()) + .header("Range", header_value.clone()) + .send() + .await? + .error_for_status()? + .bytes() + .await + }) }) .map_err(construct_backoff_error) } @@ -442,8 +391,15 @@ impl WriteBackend for RestBackend { })?; self.retry_notify(|| { - _ = self.client.post(url.clone()).send()?.error_for_status()?; - Ok(()) + runtime().block_on(async { + _ = self + .client + .post(url.clone()) + .send() + .await? + .error_for_status()?; + Ok(()) + }) }) .map_err(construct_backoff_error) } @@ -473,14 +429,24 @@ impl WriteBackend for RestBackend { .map_err(|err| construct_join_url_error(err, tpe, id, &self.url))?; self.retry_notify(|| { - let body = Body::new(content.clone().reader()); - _ = self - .client - .post(url.clone()) - .body(body) - .send()? - .error_for_status()?; - Ok(()) + let stream = stream::iter( + content + .clone() + .into_vec() + .into_iter() + .map(|b| -> RusticResult<_> { Ok(b) }), + ); + let body = Body::wrap_stream(stream); + runtime().block_on(async { + _ = self + .client + .post(url.clone()) + .body(body) + .send() + .await? + .error_for_status()?; + Ok(()) + }) }) .map_err(construct_backoff_error) } @@ -503,8 +469,15 @@ impl WriteBackend for RestBackend { .map_err(|err| construct_join_url_error(err, tpe, id, &self.url))?; self.retry_notify(|| { - _ = self.client.delete(url.clone()).send()?.error_for_status()?; - Ok(()) + runtime().block_on(async { + _ = self + .client + .delete(url.clone()) + .send() + .await? + .error_for_status()?; + Ok(()) + }) }) .map_err(construct_backoff_error) } diff --git a/crates/core/README.md b/crates/core/README.md index 3983ae10b..9b5ea6ff0 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -67,7 +67,7 @@ This crate exposes a few features for controlling dependency usage: ```rust use rustic_backend::BackendOptions; -use rustic_core::{ConfigOptions, KeyOptions, Repository, RepositoryOptions}; +use rustic_core::{Credentials, ConfigOptions, KeyOptions, Repository, RepositoryOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; @@ -81,10 +81,11 @@ fn main() -> Result<(), Box> { .to_backends()?; // Init repository - let repo_opts = RepositoryOptions::default().password("test"); + let creds = Credentials::Password("test".into()); + let repo_opts = RepositoryOptions::default(); let key_opts = KeyOptions::default(); let config_opts = ConfigOptions::default(); - let _repo = Repository::new(&repo_opts, backends)?.init(&key_opts, &config_opts)?; + let _repo = Repository::new(&repo_opts, &backends)?.init(&creds, &key_opts, &config_opts)?; // -> use _repo for any operation on an open repository Ok(()) @@ -95,7 +96,7 @@ fn main() -> Result<(), Box> { ```rust use rustic_backend::BackendOptions; -use rustic_core::{BackupOptions, PathList, Repository, RepositoryOptions, SnapshotOptions}; +use rustic_core::{BackupOptions, Credentials, PathList, Repository, RepositoryOptions, SnapshotOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; @@ -110,10 +111,11 @@ fn main() -> Result<(), Box> { .to_backends()?; // Open repository - let repo_opts = RepositoryOptions::default().password("test"); + let creds = Credentials::Password("test".into()); + let repo_opts = RepositoryOptions::default(); - let repo = Repository::new(&repo_opts, backends)? - .open()? + let repo = Repository::new(&repo_opts, &backends)? + .open(&creds)? .to_indexed_ids()?; let backup_opts = BackupOptions::default(); @@ -127,13 +129,14 @@ fn main() -> Result<(), Box> { println!("successfully created snapshot:\n{snap:#?}"); Ok(()) +} ``` ### Example: Restoring a snapshot ```rust use rustic_backend::BackendOptions; -use rustic_core::{LocalDestination, LsOptions, Repository, RepositoryOptions, RestoreOptions}; +use rustic_core::{Credentials, LocalDestination, LsOptions, Repository, RepositoryOptions, RestoreOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; @@ -147,9 +150,10 @@ fn main() -> Result<(), Box> { .to_backends()?; // Open repository - let repo_opts = RepositoryOptions::default().password("test"); - let repo = Repository::new(&repo_opts, backends)? - .open()? + let creds = Credentials::Password("test".into()); + let repo_opts = RepositoryOptions::default(); + let repo = Repository::new(&repo_opts, &backends)? + .open(&creds)? .to_indexed()?; // use latest snapshot without filtering snapshots @@ -177,7 +181,7 @@ fn main() -> Result<(), Box> { ```rust use rustic_backend::BackendOptions; -use rustic_core::{CheckOptions, Repository, RepositoryOptions}; +use rustic_core::{CheckOptions, Credentials, Repository, RepositoryOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; use std::error::Error; @@ -191,8 +195,9 @@ fn main() -> Result<(), Box> { .to_backends()?; // Open repository - let repo_opts = RepositoryOptions::default().password("test"); - let repo = Repository::new(&repo_opts, backends)?.open()?; + let creds = Credentials::Password("test".into()); + let repo_opts = RepositoryOptions::default(); + let repo = Repository::new(&repo_opts, &backends)?.open(&creds)?; // Check repository with standard options but omitting cache checks let opts = CheckOptions::default().trust_cache(true); diff --git a/crates/core/src/archiver.rs b/crates/core/src/archiver.rs index 3fb6d5979..90f627bcf 100644 --- a/crates/core/src/archiver.rs +++ b/crates/core/src/archiver.rs @@ -4,6 +4,7 @@ pub(crate) mod tree; pub(crate) mod tree_archiver; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::thread::scope; use jiff::Zoned; @@ -18,7 +19,7 @@ use crate::{ }, backend::{ReadSource, ReadSourceEntry, decrypt::DecryptFullBackend}, blob::BlobType, - error::RusticResult, + error::{RusticError, RusticResult}, index::{ ReadGlobalIndex, indexer::{Indexer, SharedIndexer}, @@ -26,6 +27,12 @@ use crate::{ repofile::{configfile::ConfigFile, snapshotfile::SnapshotFile}, }; +/// Report a source-file error, count it, and continue the backup. +fn report_source_error(p: &Progress, errors: &AtomicU64, during: &'static str, err: &RusticError) { + _ = errors.fetch_add(1, Ordering::Relaxed); + p.error(err.context_value("path"), during, &err.display_log()); +} + #[derive(thiserror::Error, Debug, displaydoc::Display)] /// Tree stack empty pub struct TreeStackEmptyError; @@ -138,6 +145,8 @@ impl<'a, BE: DecryptFullBackend, I: ReadGlobalIndex> Archiver<'a, BE, I> { ::Open: Send, ::Iter: Send, { + let error_count = AtomicU64::new(0); + scope(|s| -> RusticResult<_> { // determine backup size in parallel to running backup let src_size_handle = s.spawn(|| { @@ -153,7 +162,7 @@ impl<'a, BE: DecryptFullBackend, I: ReadGlobalIndex> Archiver<'a, BE, I> { // filter out errors and handle as_path let iter = src.entries().filter_map(|item| match item { Err(err) => { - warn!("ignoring error: {}", err.display_log()); + report_source_error(p, &error_count, "scan", &err); None } Ok(ReadSourceEntry { path, node, open }) => { @@ -197,7 +206,7 @@ impl<'a, BE: DecryptFullBackend, I: ReadGlobalIndex> Archiver<'a, BE, I> { .filter_map(|item| match item { Ok(item) => Some(item), Err(err) => { - warn!("ignoring error: {}", err.display_log()); + report_source_error(p, &error_count, "archival", &err); None } }) @@ -213,6 +222,7 @@ impl<'a, BE: DecryptFullBackend, I: ReadGlobalIndex> Archiver<'a, BE, I> { let stats = self.file_archiver.finalize()?; let (id, mut summary) = self.tree_archiver.finalize(self.parent.tree_id())?; stats.apply(&mut summary, BlobType::Data); + summary.error_count = error_count.load(Ordering::Relaxed); self.snap.tree = id; self.indexer.write().unwrap().finalize()?; diff --git a/crates/core/src/backend/ignore.rs b/crates/core/src/backend/ignore.rs index de5ad8a47..a36875453 100644 --- a/crates/core/src/backend/ignore.rs +++ b/crates/core/src/backend/ignore.rs @@ -283,6 +283,17 @@ impl ReadSource for LocalSource { } } +fn ignore_error_path(err: &ignore::Error) -> Option { + match err { + ignore::Error::WithPath { path, .. } => Some(path.display().to_string()), + ignore::Error::WithDepth { err, .. } | ignore::Error::WithLineNumber { err, .. } => { + ignore_error_path(err) + } + ignore::Error::Loop { child, .. } => Some(child.display().to_string()), + _ => None, + } +} + // Walk doesn't implement Debug #[allow(missing_debug_implementations)] pub struct LocalSourceWalker { @@ -304,23 +315,35 @@ impl Iterator for LocalSourceWalker { item => item, } .map(|e| { - self.save_opts - .map_entry(e.map_err(|err| { + let entry = e.map_err(|err| { + let path = ignore_error_path(&err); + let rustic_err = if path.is_some() { RusticError::with_source( - ErrorKind::Internal, - "Failed to get next entry from walk iterator.", + ErrorKind::InputOutput, + "Failed to read source path `{path}`.", err, ) - .ask_report() - })?) - .map_err(|err| { + } else { RusticError::with_source( - ErrorKind::Internal, - "Failed to map Directory entry to ReadSourceEntry.", + ErrorKind::InputOutput, + "Failed to read source path.", err, ) - .ask_report() - }) + }; + match path { + Some(path) => rustic_err.attach_context("path", path), + None => rustic_err, + } + })?; + let path = entry.path().display().to_string(); + self.save_opts.map_entry(entry).map_err(|err| { + RusticError::with_source( + ErrorKind::InputOutput, + "Failed to map directory entry `{path}` to a backup source entry.", + err, + ) + .attach_context("path", path) + }) }) } } diff --git a/crates/core/src/blob/tree.rs b/crates/core/src/blob/tree.rs index be55ac07f..d2fbe4083 100644 --- a/crates/core/src/blob/tree.rs +++ b/crates/core/src/blob/tree.rs @@ -671,9 +671,12 @@ impl TreeStreamerOnce { let out_tx = out_tx.clone(); let _join_handle = std::thread::spawn(move || { for (path, id, count) in in_rx { - out_tx + if out_tx .send(Tree::from_backend(&be, &index, id).map(|tree| (path, tree, count))) - .unwrap(); + .is_err() + { + break; + } } }); } diff --git a/crates/core/src/commands/config.rs b/crates/core/src/commands/config.rs index b2ecf20ce..a3a396374 100644 --- a/crates/core/src/commands/config.rs +++ b/crates/core/src/commands/config.rs @@ -1,6 +1,8 @@ //! `config` subcommand use bytesize::ByteSize; use derive_setters::Setters; +use serde::{Deserialize, Serialize}; +use serde_with::{DisplayFromStr, serde_as}; use crate::{ backend::decrypt::{DecryptBackend, DecryptWriteBackend}, @@ -116,56 +118,74 @@ pub(crate) fn save_config_hot( Ok(()) } +#[serde_as] #[cfg_attr(feature = "clap", derive(clap::Parser))] -#[derive(Debug, Clone, Copy, Default, Setters)] +#[cfg_attr(feature = "merge", derive(conflate::Merge))] +#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, Setters)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[setters(into)] #[non_exhaustive] /// Options for the `config` command, used to set repository-wide options pub struct ConfigOptions { /// Set repository version. Allowed versions: 1,2 #[cfg_attr(feature = "clap", clap(long, value_name = "VERSION"))] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_version: Option, /// Set chunker to use. Allowed chunkers: ``rabin``, ``fixed_size``. /// Defaults to ``rabin`` if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "CHUNKER"))] + #[serde_as(as = "Option")] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_chunker: Option, /// Set the chunk size. For the rabin chunker this is the average chunk size. /// Defaults to `1 MiB` if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + #[serde_as(as = "Option")] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_chunk_size: Option, /// Set the minimum chunk size. Only used for the rabin chunker. /// Defaults to `512 kiB` if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + #[serde_as(as = "Option")] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_chunk_min_size: Option, /// Set the maximum chunk size. Only used for the rabin chunker. /// Defaults to `8 MiB` if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + #[serde_as(as = "Option")] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_chunk_max_size: Option, /// Set compression level. Allowed levels are 1 to 22 and -1 to -7, see . /// Note that 0 equals to no compression #[cfg_attr(feature = "clap", clap(long, value_name = "LEVEL"))] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_compression: Option, /// Set append-only mode. /// Note that only append-only commands work once this is set. `forget`, `prune` or `config` won't work any longer. #[cfg_attr(feature = "clap", clap(long))] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_append_only: Option, /// Set default packsize for tree packs. rustic tries to always produce packs greater than this value. /// Note that for large repos, this value is grown by the grown factor. /// Defaults to `4 MiB` if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + #[serde_as(as = "Option")] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_treepack_size: Option, /// Set upper limit for default packsize for tree packs. /// Note that packs actually can get a bit larger. /// If not set, pack sizes can grow up to approximately `4 GiB`. #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + #[serde_as(as = "Option")] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_treepack_size_limit: Option, /// Set grow factor for tree packs. The default packsize grows by the square root of the total size of all @@ -173,12 +193,15 @@ pub struct ConfigOptions { /// treesize in GiB. /// Defaults to `32` (= 1MB per square root of total treesize in GiB) if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "FACTOR"))] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_treepack_growfactor: Option, /// Set default packsize for data packs. rustic tries to always produce packs greater than this value. /// Note that for large repos, this value is grown by the grown factor. /// Defaults to `32 MiB` if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + #[serde_as(as = "Option")] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_datapack_size: Option, /// Set grow factor for data packs. The default packsize grows by the square root of the total size of all @@ -186,28 +209,34 @@ pub struct ConfigOptions { /// datasize in GiB. /// Defaults to `32` (= 1MB per square root of total datasize in GiB) if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "FACTOR"))] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_datapack_growfactor: Option, /// Set upper limit for default packsize for tree packs. /// Note that packs actually can get a bit larger. /// If not set, pack sizes can grow up to approximately `4 GiB`. #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + #[serde_as(as = "Option")] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_datapack_size_limit: Option, /// Set minimum tolerated packsize in percent of the targeted packsize. /// Defaults to `30` if not set. #[cfg_attr(feature = "clap", clap(long, value_name = "PERCENT"))] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_min_packsize_tolerate_percent: Option, /// Set maximum tolerated packsize in percent of the targeted packsize /// A value of `0` means packs larger than the targeted packsize are always /// tolerated. Default if not set: larger packfiles are always tolerated. #[cfg_attr(feature = "clap", clap(long, value_name = "PERCENT"))] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_max_packsize_tolerate_percent: Option, /// Do an extra verification by decompressing/decrypting all data before uploading to the repository. /// Default: true #[cfg_attr(feature = "clap", clap(long))] + #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))] pub set_extra_verify: Option, } diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 0e72a7837..e223d01a7 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -369,6 +369,14 @@ impl RusticError { Self::with_source(kind, error.to_string(), error) } + /// Get a context value by key, if present. + #[must_use] + pub fn context_value(&self, key: &str) -> Option<&str> { + self.context + .iter() + .find_map(|(k, v)| (k == key).then_some(v.as_str())) + } + /// Returns a String representation for logging purposes. /// /// This is a more concise version of the error message. diff --git a/crates/core/src/progress.rs b/crates/core/src/progress.rs index bb413dffa..88d3899fc 100644 --- a/crates/core/src/progress.rs +++ b/crates/core/src/progress.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use log::info; +use log::{info, warn}; /// A progress used to indicate/update the status of something which is being processed #[derive(Debug, Clone)] @@ -55,6 +55,17 @@ impl Progress { pub fn finish(&self) { self.0.finish(); } + + /// Report an error encountered while processing an item. + /// + /// # Arguments + /// + /// * `item` - Usually the path of the problematic file or directory + /// * `during` - What was being attempted, e.g. `"scan"` or `"archival"` + /// * `message` - Error message + pub fn error(&self, item: Option<&str>, during: &str, message: &str) { + self.0.error(item, during, message); + } } /// Trait to report progress information for any rustic action which supports that. @@ -87,6 +98,23 @@ pub trait RusticProgress: Send + Sync + 'static + std::fmt::Debug { /// Finish the progress fn finish(&self); + + /// Report an error encountered while processing an item. + /// + /// The default implementation logs a warning. JSON progress implementations + /// emit a restic-compatible error message instead. + /// + /// # Arguments + /// + /// * `item` - Usually the path of the problematic file or directory + /// * `during` - What was being attempted, e.g. `"scan"` or `"archival"` + /// * `message` - Error message + fn error(&self, item: Option<&str>, during: &str, message: &str) { + match item { + Some(item) => warn!("error: {during} {item}: {message}"), + None => warn!("{during}: {message}"), + } + } } /// Type of progress diff --git a/crates/core/src/repofile/configfile.rs b/crates/core/src/repofile/configfile.rs index f15f517e0..13cb8966c 100644 --- a/crates/core/src/repofile/configfile.rs +++ b/crates/core/src/repofile/configfile.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use serde_derive::{Deserialize, Serialize}; use serde_with::skip_serializing_none; +use strum::Display; use crate::{ backend::FileType, @@ -285,13 +286,15 @@ impl ConfigFile { } } -#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Display, Default, Clone, Copy, PartialEq, Eq)] /// Supported chunkers used to cut large files into Blobs. pub enum Chunker { #[default] /// Rabin chunker - a content defined chunker (CDC) based on Rabin fingerprints + #[strum(to_string = "rabin")] Rabin, /// Fixed size chunker - makes chunks of a given fixed size + #[strum(to_string = "fixed_size")] FixedSize, } diff --git a/crates/core/src/repofile/snapshotfile.rs b/crates/core/src/repofile/snapshotfile.rs index ac4016daa..86e28b1f0 100644 --- a/crates/core/src/repofile/snapshotfile.rs +++ b/crates/core/src/repofile/snapshotfile.rs @@ -46,8 +46,8 @@ pub enum SnapshotFileErrorKind { ValueNotAllowed(String), /// removing dots from paths failed: `{0:?}` RemovingDotsFromPathFailed(std::io::Error), - /// canonicalizing path failed: `{0:?}` - CanonicalizingPathFailed(std::io::Error), + /// canonicalizing path `{0:?}` failed: `{1:?}` + CanonicalizingPathFailed(PathBuf, std::io::Error), } pub(crate) type SnapshotFileResult = Result; @@ -247,6 +247,18 @@ pub struct SnapshotSummary { /// Total duration that the rustic command ran in seconds pub total_duration: f64, + + /// Number of source files/directories that could not be read during this backup run. + /// + /// Serialized only when non-zero so existing restic/rustic snapshots stay unchanged. + #[serde(default, skip_serializing_if = "u64_is_zero")] + pub error_count: u64, +} + +// serde `skip_serializing_if` always passes a reference. +#[allow(clippy::trivially_copy_pass_by_ref)] +const fn u64_is_zero(value: &u64) -> bool { + *value == 0 } impl Default for SnapshotSummary { @@ -275,6 +287,7 @@ impl Default for SnapshotSummary { backup_end: Zoned::now(), backup_duration: Default::default(), total_duration: Default::default(), + error_count: Default::default(), } } } @@ -1255,30 +1268,23 @@ impl PathList { self.0 = self .0 .into_iter() - .map(|p| canonicalize(p).map_err(SnapshotFileErrorKind::CanonicalizingPathFailed)) + .map(|p| { + canonicalize(&p) + .map_err(|err| SnapshotFileErrorKind::CanonicalizingPathFailed(p, err)) + }) .collect::>()?; } Ok(self.merge()) } - /// Sort paths and filters out subpaths of already existing paths. + /// Merge paths: Sort and remove duplicates #[must_use] pub fn merge(self) -> Self { let mut paths = self.0; // sort paths paths.sort_unstable(); - - let mut root_path = None; - - // filter out subpaths - paths.retain(|path| match &root_path { - Some(root_path) if path.starts_with(root_path) => false, - _ => { - root_path = Some(path.clone()); - true - } - }); - + // remove duplicates + paths.dedup(); Self(paths) } } @@ -1369,6 +1375,25 @@ mod tests { assert_eq!(expected, &result); } + #[test] + fn path_list_sanitize_error_includes_path() { + let tempdir = tempfile::tempdir().unwrap(); + let missing_path = tempdir.path().join("missing"); + + let err = PathList::from_iter([missing_path.clone()]) + .sanitize() + .unwrap_err(); + + assert!( + err.to_string() + .contains(&missing_path.display().to_string()) + ); + assert!(matches!( + err, + SnapshotFileErrorKind::CanonicalizingPathFailed(path, _) if path == missing_path + )); + } + fn fake_snapshot_file_with_id_time( id_time_vec: Vec<(Id, Zoned)>, key: &Key, @@ -1588,4 +1613,24 @@ mod tests { let ids: Vec<_> = snaps.iter().map(|sn| *sn.id).collect(); assert_eq!(ids, vec![id3, id1, id3]); } + + #[test] + fn error_count_omitted_from_json_when_zero() { + let summary = SnapshotSummary::default(); + let json = serde_json::to_value(&summary).unwrap(); + assert!(json.get("error_count").is_none()); + } + + #[test] + fn error_count_serialized_when_nonzero() { + let summary = SnapshotSummary { + error_count: 3, + ..Default::default() + }; + let json = serde_json::to_value(&summary).unwrap(); + assert_eq!(json["error_count"], 3); + + let decoded: SnapshotSummary = serde_json::from_value(json).unwrap(); + assert_eq!(decoded.error_count, 3); + } } diff --git a/crates/core/tests/integration/backup.rs b/crates/core/tests/integration/backup.rs index c7d93d498..1b97c53c7 100644 --- a/crates/core/tests/integration/backup.rs +++ b/crates/core/tests/integration/backup.rs @@ -307,3 +307,45 @@ fn test_backup_excludes_xattr_entries(set_up_repo: Result) -> Result<( Ok(()) } + +#[cfg(unix)] +#[rstest] +fn test_backup_unreadable_file_sets_error_count(set_up_repo: Result) -> Result<()> { + use std::fs::{self, File, Permissions}; + use std::os::unix::fs::PermissionsExt; + + let tmp = tempfile::tempdir()?; + let base = tmp.path(); + fs::write(base.join("ok.txt"), "ok")?; + let secret = base.join("secret.txt"); + fs::write(&secret, "secret")?; + fs::set_permissions(&secret, Permissions::from_mode(0o000))?; + + if File::open(&secret).is_ok() { + // e.g. running as root; mode bits are not enforced + return Ok(()); + } + + let repo = set_up_repo?.to_indexed_ids()?; + let paths = PathList::from_iter(Some(base.to_path_buf())); + let opts = BackupOptions::default().as_path(PathBuf::from("test")); + let snapshot = repo.backup(&opts, &paths, SnapshotFile::default())?; + + let summary = snapshot.summary.as_ref().expect("backup sets a summary"); + assert!( + summary.error_count >= 1, + "expected at least one source-file error, got {}", + summary.error_count + ); + assert_ne!(snapshot.id, SnapshotFile::default().id); + assert!(summary.total_files_processed >= 1); + + let loaded = repo.get_snapshot_from_str(&snapshot.id.to_string(), |_| true)?; + assert_eq!( + loaded.summary.as_ref().map(|s| s.error_count), + Some(summary.error_count), + "error_count should be persisted in the snapshot file when non-zero" + ); + + Ok(()) +} diff --git a/crates/core/tests/integration/snapshots/integration__integration__chunker__chunker-fixedsize.snap b/crates/core/tests/integration/snapshots/integration__integration__chunker__chunker-fixedsize.snap index 7aaf8a703..38ddf3557 100644 --- a/crates/core/tests/integration/snapshots/integration__integration__chunker__chunker-fixedsize.snap +++ b/crates/core/tests/integration/snapshots/integration__integration__chunker__chunker-fixedsize.snap @@ -38,6 +38,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ) diff --git a/crates/core/tests/snapshots/integration__backup-tar-groups-nix.snap b/crates/core/tests/snapshots/integration__backup-tar-groups-nix.snap index 0ebeb396e..1ab80205e 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-groups-nix.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-groups-nix.snap @@ -45,6 +45,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ), @@ -87,6 +88,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ), @@ -142,6 +144,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ), diff --git a/crates/core/tests/snapshots/integration__backup-tar-groups-windows.snap b/crates/core/tests/snapshots/integration__backup-tar-groups-windows.snap index 06a0d2631..774fd680e 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-groups-windows.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-groups-windows.snap @@ -45,6 +45,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ), @@ -87,6 +88,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ), @@ -142,6 +144,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ), diff --git a/crates/core/tests/snapshots/integration__backup-tar-matching-snaps-nix.snap b/crates/core/tests/snapshots/integration__backup-tar-matching-snaps-nix.snap index 620e33d75..e62aca972 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-matching-snaps-nix.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-matching-snaps-nix.snap @@ -45,6 +45,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ), diff --git a/crates/core/tests/snapshots/integration__backup-tar-matching-snaps-windows.snap b/crates/core/tests/snapshots/integration__backup-tar-matching-snaps-windows.snap index 2294a3b4f..56c60fe43 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-matching-snaps-windows.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-matching-snaps-windows.snap @@ -45,6 +45,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ), diff --git a/crates/core/tests/snapshots/integration__backup-tar-summary-first-nix.snap b/crates/core/tests/snapshots/integration__backup-tar-summary-first-nix.snap index 4280c5511..ad66f19dc 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-summary-first-nix.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-summary-first-nix.snap @@ -38,6 +38,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ) diff --git a/crates/core/tests/snapshots/integration__backup-tar-summary-first-windows.snap b/crates/core/tests/snapshots/integration__backup-tar-summary-first-windows.snap index 4280c5511..ad66f19dc 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-summary-first-windows.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-summary-first-windows.snap @@ -38,6 +38,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ) diff --git a/crates/core/tests/snapshots/integration__backup-tar-summary-second-nix.snap b/crates/core/tests/snapshots/integration__backup-tar-summary-second-nix.snap index dcea654db..8cf88b39d 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-summary-second-nix.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-summary-second-nix.snap @@ -40,6 +40,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ) diff --git a/crates/core/tests/snapshots/integration__backup-tar-summary-second-windows.snap b/crates/core/tests/snapshots/integration__backup-tar-summary-second-windows.snap index cb86c8030..043253171 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-summary-second-windows.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-summary-second-windows.snap @@ -40,6 +40,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ) diff --git a/crates/core/tests/snapshots/integration__backup-tar-summary-third-nix.snap b/crates/core/tests/snapshots/integration__backup-tar-summary-third-nix.snap index 54fb3b619..7e1181912 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-summary-third-nix.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-summary-third-nix.snap @@ -43,6 +43,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ) diff --git a/crates/core/tests/snapshots/integration__backup-tar-summary-third-windows.snap b/crates/core/tests/snapshots/integration__backup-tar-summary-third-windows.snap index 47d029f0e..b78e3936d 100644 --- a/crates/core/tests/snapshots/integration__backup-tar-summary-third-windows.snap +++ b/crates/core/tests/snapshots/integration__backup-tar-summary-third-windows.snap @@ -43,6 +43,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ) diff --git a/crates/core/tests/snapshots/integration__dryrun-tar-summary-first-nix.snap b/crates/core/tests/snapshots/integration__dryrun-tar-summary-first-nix.snap index 8abb966ad..585014011 100644 --- a/crates/core/tests/snapshots/integration__dryrun-tar-summary-first-nix.snap +++ b/crates/core/tests/snapshots/integration__dryrun-tar-summary-first-nix.snap @@ -38,5 +38,6 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), ) diff --git a/crates/core/tests/snapshots/integration__dryrun-tar-summary-first-windows.snap b/crates/core/tests/snapshots/integration__dryrun-tar-summary-first-windows.snap index 8abb966ad..585014011 100644 --- a/crates/core/tests/snapshots/integration__dryrun-tar-summary-first-windows.snap +++ b/crates/core/tests/snapshots/integration__dryrun-tar-summary-first-windows.snap @@ -38,5 +38,6 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), ) diff --git a/crates/core/tests/snapshots/integration__dryrun-tar-summary-second-nix.snap b/crates/core/tests/snapshots/integration__dryrun-tar-summary-second-nix.snap index 08c999930..9e3d61ad9 100644 --- a/crates/core/tests/snapshots/integration__dryrun-tar-summary-second-nix.snap +++ b/crates/core/tests/snapshots/integration__dryrun-tar-summary-second-nix.snap @@ -40,5 +40,6 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), ) diff --git a/crates/core/tests/snapshots/integration__dryrun-tar-summary-second-windows.snap b/crates/core/tests/snapshots/integration__dryrun-tar-summary-second-windows.snap index a52216975..b88bbfe67 100644 --- a/crates/core/tests/snapshots/integration__dryrun-tar-summary-second-windows.snap +++ b/crates/core/tests/snapshots/integration__dryrun-tar-summary-second-windows.snap @@ -40,5 +40,6 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), ) diff --git a/crates/core/tests/snapshots/integration__rewrite-snapshots-first-nix.snap b/crates/core/tests/snapshots/integration__rewrite-snapshots-first-nix.snap index baad8dc79..8812a5556 100644 --- a/crates/core/tests/snapshots/integration__rewrite-snapshots-first-nix.snap +++ b/crates/core/tests/snapshots/integration__rewrite-snapshots-first-nix.snap @@ -44,6 +44,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), description: Some("description"), id: "[id]", diff --git a/crates/core/tests/snapshots/integration__rewrite-snapshots-first-windows.snap b/crates/core/tests/snapshots/integration__rewrite-snapshots-first-windows.snap index baad8dc79..8812a5556 100644 --- a/crates/core/tests/snapshots/integration__rewrite-snapshots-first-windows.snap +++ b/crates/core/tests/snapshots/integration__rewrite-snapshots-first-windows.snap @@ -44,6 +44,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), description: Some("description"), id: "[id]", diff --git a/crates/core/tests/snapshots/integration__rewrite-snapshots-second-nix.snap b/crates/core/tests/snapshots/integration__rewrite-snapshots-second-nix.snap index 425bdf4a6..9bab75634 100644 --- a/crates/core/tests/snapshots/integration__rewrite-snapshots-second-nix.snap +++ b/crates/core/tests/snapshots/integration__rewrite-snapshots-second-nix.snap @@ -45,6 +45,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), description: Some("description"), id: "[id]", diff --git a/crates/core/tests/snapshots/integration__rewrite-snapshots-second-windows.snap b/crates/core/tests/snapshots/integration__rewrite-snapshots-second-windows.snap index 425bdf4a6..9bab75634 100644 --- a/crates/core/tests/snapshots/integration__rewrite-snapshots-second-windows.snap +++ b/crates/core/tests/snapshots/integration__rewrite-snapshots-second-windows.snap @@ -45,6 +45,7 @@ expression: snap backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), description: Some("description"), id: "[id]", diff --git a/crates/core/tests/snapshots/integration__stdin-command-summary-nix.snap b/crates/core/tests/snapshots/integration__stdin-command-summary-nix.snap index e837cecb6..d937d9ebd 100644 --- a/crates/core/tests/snapshots/integration__stdin-command-summary-nix.snap +++ b/crates/core/tests/snapshots/integration__stdin-command-summary-nix.snap @@ -38,6 +38,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", ) diff --git a/crates/core/tests/snapshots/integration__stdin-command-summary-windows.snap b/crates/core/tests/snapshots/integration__stdin-command-summary-windows.snap index e837cecb6..d937d9ebd 100644 --- a/crates/core/tests/snapshots/integration__stdin-command-summary-windows.snap +++ b/crates/core/tests/snapshots/integration__stdin-command-summary-windows.snap @@ -38,6 +38,7 @@ SnapshotFile( backup_end: "[backup_end]", backup_duration: "[backup_duration]", total_duration: "[total_duration]", + error_count: 0, )), id: "[id]", )