diff --git a/.cargo/audit.toml b/.cargo/audit.toml index aa1db3e6..0b118933 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -3,8 +3,4 @@ ignore = [ # FIXME!: See https://github.com/RustCrypto/RSA/issues/19#issuecomment-1822995643. # There is no workaround available yet. "RUSTSEC-2023-0071", - # FIXME: backoff => used in backend, need to be replaced with backon - "RUSTSEC-2024-0384", - # FIXME: derivative => used for default impls - "RUSTSEC-2024-0388", ] diff --git a/.github/workflows/ci-heavy.yml b/.github/workflows/ci-heavy.yml index 541ae94c..d3260452 100644 --- a/.github/workflows/ci-heavy.yml +++ b/.github/workflows/ci-heavy.yml @@ -174,11 +174,12 @@ jobs: target: x86_64-pc-windows-msvc architecture: x86_64 use-cross: false - - os: windows-latest - os-name: windows - target: x86_64-pc-windows-gnu - architecture: x86_64 - use-cross: false + # FIXME: `aws-lc-sys` doesn't cross compile + # - os: windows-latest + # os-name: windows + # target: x86_64-pc-windows-gnu + # architecture: x86_64 + # use-cross: false - os: macos-13 os-name: macos target: x86_64-apple-darwin @@ -209,16 +210,24 @@ jobs: target: i686-unknown-linux-gnu architecture: i686 use-cross: true - - os: ubuntu-latest - os-name: netbsd - target: x86_64-unknown-netbsd - architecture: x86_64 - use-cross: true + # Check because of Container images for rustic-rs - os: ubuntu-latest os-name: linux - target: armv7-unknown-linux-gnueabihf - architecture: armv7 + target: aarch64-unknown-linux-musl + architecture: arm64 use-cross: true + # FIXME: `aws-lc-sys` doesn't cross compile + # - os: ubuntu-latest + # os-name: netbsd + # target: x86_64-unknown-netbsd + # architecture: x86_64 + # use-cross: true + # FIXME: `aws-lc-sys` doesn't cross compile + # - os: ubuntu-latest + # os-name: linux + # target: armv7-unknown-linux-gnueabihf + # architecture: armv7 + # use-cross: true steps: - name: Checkout repository diff --git a/Cargo.lock b/Cargo.lock index 3a2bf122..83eec76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,17 +349,6 @@ dependencies = [ "paste", ] -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "getrandom", - "instant", - "rand", -] - [[package]] name = "backon" version = "1.2.0" @@ -1107,17 +1096,6 @@ dependencies = [ "serde", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "derive_destructure2" version = "0.1.3" @@ -2083,15 +2061,6 @@ dependencies = [ "similar", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "integer-sqrt" version = "0.1.5" @@ -2381,9 +2350,9 @@ checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" [[package]] name = "mockall" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" dependencies = [ "cfg-if", "downcast", @@ -2395,9 +2364,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" dependencies = [ "cfg-if", "proc-macro2", @@ -3585,7 +3554,7 @@ dependencies = [ "aho-corasick", "anyhow", "aws-lc-sys", - "backoff", + "backon", "bytes", "bytesize", "clap", @@ -3640,7 +3609,6 @@ dependencies = [ "conflate", "crossbeam-channel", "dav-server", - "derivative", "derive_more", "derive_setters", "dirs", diff --git a/build-dependencies.just b/build-dependencies.just index 3701f839..eefb9548 100644 --- a/build-dependencies.just +++ b/build-dependencies.just @@ -4,3 +4,8 @@ install-default-x86_64-unknown-linux-musl: sudo apt-get update sudo apt-get install -y musl-tools + +# Install dependencies for the default feature on aarch64-unknown-linux-musl +install-default-aarch64-unknown-linux-musl: + sudo apt-get update + sudo apt-get install -y musl-tools diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 335c9d27..076f3673 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -42,7 +42,7 @@ opendal = [ "tokio/rt-multi-thread", "dep:typed-path", ] -rest = ["dep:reqwest", "dep:backoff"] +rest = ["dep:reqwest", "dep:backon"] rclone = ["rest", "dep:rand", "dep:semver"] [dependencies] @@ -78,7 +78,7 @@ aho-corasick = { workspace = true } walkdir = "2.5.0" # rest backend -backoff = { version = "0.4.0", optional = true } +backon = { version = "1.2.0", optional = true } reqwest = { version = "0.12.8", default-features = false, features = ["json", "rustls-tls-native-roots", "stream", "blocking"], optional = true } # rclone backend @@ -97,11 +97,11 @@ features = ["prebuilt-nasm"] [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.50.0", features = ["services-b2", "services-sftp", "services-swift", "services-azblob", "services-azdls", "services-cos", "services-fs", "services-ftp", "services-dropbox", "services-gdrive", "services-gcs", "services-ghac", "services-http", "services-ipmfs", "services-memory", "services-obs", "services-onedrive", "services-oss", "services-s3", "services-webdav", "services-webhdfs", "services-azfile", "layers-blocking", "layers-throttle"], optional = true } +opendal = { version = "0.50.2", features = ["services-b2", "services-sftp", "services-swift", "services-azblob", "services-azdls", "services-cos", "services-fs", "services-ftp", "services-dropbox", "services-gdrive", "services-gcs", "services-ghac", "services-http", "services-ipmfs", "services-memory", "services-obs", "services-onedrive", "services-oss", "services-s3", "services-webdav", "services-webhdfs", "services-azfile", "layers-blocking", "layers-throttle", "services-yandex-disk"], optional = true } [target.'cfg(windows)'.dependencies] # opendal backend -opendal = { version = "0.50.0", features = ["services-b2", "services-swift", "services-azblob", "services-azdls", "services-cos", "services-fs", "services-ftp", "services-dropbox", "services-gdrive", "services-gcs", "services-ghac", "services-http", "services-ipmfs", "services-memory", "services-obs", "services-onedrive", "services-oss", "services-s3", "services-webdav", "services-webhdfs", "services-azfile", "layers-blocking", "layers-throttle"], optional = true } +opendal = { version = "0.50.2", features = ["services-b2", "services-swift", "services-azblob", "services-azdls", "services-cos", "services-fs", "services-ftp", "services-dropbox", "services-gdrive", "services-gcs", "services-ghac", "services-http", "services-ipmfs", "services-memory", "services-obs", "services-onedrive", "services-oss", "services-s3", "services-webdav", "services-webhdfs", "services-azfile", "layers-blocking", "layers-throttle", "services-yandex-disk"], optional = true } [dev-dependencies] anyhow = { workspace = true } diff --git a/crates/backend/src/local.rs b/crates/backend/src/local.rs index e43c4c81..b1bc6f03 100644 --- a/crates/backend/src/local.rs +++ b/crates/backend/src/local.rs @@ -169,6 +169,21 @@ impl LocalBackend { } Ok(()) } + + /// Returns the parent path of the given file type and id. + /// + /// # Arguments + /// + /// * `tpe` - The type of the file. + /// * `id` - The id of the file. + /// + /// # Returns + /// + /// The parent path of the file or `None` if the file does not have a parent. + fn parent_path(&self, tpe: FileType, id: &Id) -> Option { + let path = self.path(tpe, id); + path.parent().map(Path::to_path_buf) + } } impl ReadBackend for LocalBackend { @@ -355,13 +370,14 @@ impl ReadBackend for LocalBackend { length: u32, ) -> RusticResult { trace!("reading tpe: {tpe:?}, id: {id}, offset: {offset}, length: {length}"); - let mut file = File::open(self.path(tpe, id)).map_err(|err| { + let filename = self.path(tpe, id); + let mut file = File::open(filename.clone()).map_err(|err| { RusticError::with_source( ErrorKind::Backend, "Failed to open the file `{path}`. Please check the file and try again.", err, ) - .attach_context("path", self.path(tpe, id).to_string_lossy()) + .attach_context("path", filename.to_string_lossy()) })?; _ = file.seek(SeekFrom::Start(offset.into())).map_err(|err| { RusticError::with_source( @@ -466,6 +482,10 @@ impl WriteBackend for LocalBackend { /// * If the length of the file could not be set. /// * If the bytes could not be written to the file. /// * If the OS Metadata could not be synced to disk. + /// * If the file does not have a parent directory. + /// * If the parent directory could not be created. + /// * If the file cannot be opened, due to missing permissions. + /// * If the file cannot be written to, due to lack of space on the disk. fn write_bytes( &self, tpe: FileType, @@ -476,6 +496,28 @@ impl WriteBackend for LocalBackend { trace!("writing tpe: {:?}, id: {}", &tpe, &id); let filename = self.path(tpe, id); + let Some(parent) = self.parent_path(tpe, id) else { + return Err( + RusticError::new( + ErrorKind::Backend, + "The file `{path}` does not have a parent directory. This may be empty or a root path. Please check the file and try again.", + ) + .attach_context("path", filename.display().to_string()) + .ask_report() + ); + }; + + // create parent directory if it does not exist + fs::create_dir_all(parent.clone()).map_err(|err| { + RusticError::with_source( + ErrorKind::InputOutput, + "Failed to create directories `{path}`. Does the directory already exist? Please check the file and try again.", + err, + ) + .attach_context("path", parent.display().to_string()) + .ask_report() + })?; + let mut file = fs::OpenOptions::new() .create(true) .truncate(true) diff --git a/crates/backend/src/rest.rs b/crates/backend/src/rest.rs index b4f07a23..8a7e0223 100644 --- a/crates/backend/src/rest.rs +++ b/crates/backend/src/rest.rs @@ -1,11 +1,11 @@ use std::str::FromStr; use std::time::Duration; -use backoff::{backoff::Backoff, ExponentialBackoff, ExponentialBackoffBuilder}; +use backon::{BlockingRetryable, ExponentialBuilder}; use bytes::Bytes; use log::{trace, warn}; use reqwest::{ - blocking::{Client, ClientBuilder, Response}, + blocking::{Client, ClientBuilder}, header::{HeaderMap, HeaderValue}, Url, }; @@ -28,80 +28,12 @@ pub(super) mod constants { pub(super) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(600); } -// trait CheckError to add user-defined method check_error on Response -pub(crate) trait CheckError { - /// Check reqwest Response for error and treat errors as permanent or transient - fn check_error(self) -> Result>; -} - -impl CheckError for Response { - /// Check reqwest Response for error and treat errors as permanent or transient - /// - /// # Errors - /// - /// If the response is an error, it will return an error of type Error - /// - /// # Returns - /// - /// The response if it is not an error - fn check_error(self) -> Result> { - match self.error_for_status() { - Ok(t) => Ok(t), - // Note: status() always give Some(_) as it is called from a Response - Err(err) if err.status().unwrap().is_client_error() => { - Err(backoff::Error::Permanent(err)) - } - Err(err) => Err(backoff::Error::Transient { - err, - retry_after: None, - }), - } - } -} - -/// A backoff implementation that limits the number of retries -#[derive(Clone, Debug)] -struct LimitRetryBackoff { - /// The maximum number of retries - max_retries: usize, - /// The current number of retries - retries: usize, - /// The exponential backoff - exp: ExponentialBackoff, -} - -impl Default for LimitRetryBackoff { - fn default() -> Self { - Self { - max_retries: constants::DEFAULT_RETRY, - retries: 0, - exp: ExponentialBackoffBuilder::new() - .with_max_elapsed_time(None) // no maximum elapsed time; we count number of retires - .build(), - } - } -} - -impl Backoff for LimitRetryBackoff { - /// Returns the next backoff duration. - /// - /// # Notes - /// - /// If the number of retries exceeds the maximum number of retries, it returns None. - fn next_backoff(&mut self) -> Option { - self.retries += 1; - if self.retries > self.max_retries { - None - } else { - self.exp.next_backoff() - } - } - - /// Resets the backoff to the initial state. - fn reset(&mut self) { - self.retries = 0; - self.exp.reset(); - } +fn construct_backoff_error(err: reqwest::Error) -> Box { + RusticError::with_source( + ErrorKind::Backend, + "Backoff failed, please check the logs for more information.", + err, + ) } /// A backend implementation that uses REST to access the backend. @@ -111,24 +43,37 @@ pub struct RestBackend { url: Url, /// The client to use. client: Client, - /// The backoff implementation to use. - backoff: LimitRetryBackoff, -} - -/// Notify function for backoff in case of error -/// -/// # Arguments -/// -/// * `err` - The error that occurred -/// * `duration` - The duration of the backoff -// We need to pass the error by value to satisfy the signature of the notify function -// for handling errors in backoff -#[allow(clippy::needless_pass_by_value)] -fn notify(err: reqwest::Error, duration: Duration) { - warn!("Error {err} at {duration:?}, retrying"); + /// The ``BackoffBuilder`` we use + backoff: ExponentialBuilder, } impl RestBackend { + /// Call the given operation retrying non-permanent errors and giving warnings for failed operations + /// + /// ## Permanent/non-permanent errors + /// + /// - `client_error` are considered permanent + /// - others are not, and are subject to retry + /// + /// ## Returns + /// + /// The operation result + /// or the last error (permanent or not) that occurred. + fn retry_notify(&self, op: F) -> Result + where + F: FnMut() -> Result, + { + op.retry(self.backoff) + .when(|err| { + err.status().map_or( + true, // retry + |status_code| !status_code.is_client_error(), // do not retry if `is_client_error` + ) + }) + .notify(|err, duration| warn!("Error {err} at {duration:?}, retrying")) + .call() + } + /// Create a new [`RestBackend`] from a given url. /// /// # Arguments @@ -169,7 +114,12 @@ impl RestBackend { .map_err(|err| { RusticError::with_source(ErrorKind::Backend, "Failed to build HTTP client", err) })?; - let mut backoff = LimitRetryBackoff::default(); + + // backon doesn't allow us to specify `None` for `max_delay` + // see + let mut backoff = ExponentialBuilder::default() + .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 { @@ -187,7 +137,7 @@ impl RestBackend { .attach_context("option", "retry") })?, }; - backoff.max_retries = max_retries; + backoff = backoff.with_max_times(max_retries); } else if option == "timeout" { let timeout = humantime::Duration::from_str(&value).map_err(|err| { RusticError::with_source( @@ -299,37 +249,34 @@ impl ReadBackend for RestBackend { .attach_context("tpe_dir", tpe.dirname().to_string()) })?; - backoff::retry_notify( - self.backoff.clone(), - || { - 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()? - .check_error()? - .json::>>()? // use Option to be handle null json value - .unwrap_or_default(); - Ok(list - .into_iter() - .filter_map(|i| match i.name.parse::() { - Ok(id) => Some((id, i.size)), - Err(_) => None, - }) - .collect()) - }, - notify, - ) + 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(|i| match i.name.parse::() { + Ok(id) => Some((id, i.size)), + Err(_) => None, + }) + .collect()) + }) .map_err(construct_backoff_error) } @@ -351,18 +298,13 @@ impl ReadBackend for RestBackend { .url(tpe, id) .map_err(|err| construct_join_url_error(err, tpe, id, &self.url))?; - backoff::retry_notify( - self.backoff.clone(), - || { - Ok(self - .client - .get(url.clone()) - .send()? - .check_error()? - .bytes()?) - }, - notify, - ) + self.retry_notify(|| { + self.client + .get(url.clone()) + .send()? + .error_for_status()? + .bytes() + }) .map_err(construct_backoff_error) } @@ -398,19 +340,14 @@ impl ReadBackend for RestBackend { .attach_context("id", id.to_string()) })?; - backoff::retry_notify( - self.backoff.clone(), - || { - Ok(self - .client - .get(url.clone()) - .header("Range", header_value.clone()) - .send()? - .check_error()? - .bytes()?) - }, - notify, - ) + self.retry_notify(|| { + self.client + .get(url.clone()) + .header("Range", header_value.clone()) + .send()? + .error_for_status()? + .bytes() + }) .map_err(construct_backoff_error) } @@ -425,14 +362,6 @@ impl ReadBackend for RestBackend { } } -fn construct_backoff_error(err: backoff::Error) -> Box { - RusticError::with_source( - ErrorKind::Backend, - "Backoff failed, please check the logs for more information.", - err, - ) -} - fn construct_join_url_error( err: JoiningUrlFailedError, tpe: FileType, @@ -463,14 +392,10 @@ impl WriteBackend for RestBackend { .attach_context("join_input", "?create=true") })?; - backoff::retry_notify( - self.backoff.clone(), - || { - _ = self.client.post(url.clone()).send()?.check_error()?; - Ok(()) - }, - notify, - ) + self.retry_notify(|| { + _ = self.client.post(url.clone()).send()?.error_for_status()?; + Ok(()) + }) .map_err(construct_backoff_error) } @@ -502,15 +427,15 @@ impl WriteBackend for RestBackend { ) .body(buf); - backoff::retry_notify( - self.backoff.clone(), - || { - // Note: try_clone() always gives Some(_) as the body is Bytes which is cloneable - _ = req_builder.try_clone().unwrap().send()?.check_error()?; - Ok(()) - }, - notify, - ) + self.retry_notify(|| { + // Note: try_clone() always gives Some(_) as the body is Bytes which is cloneable + _ = req_builder + .try_clone() + .unwrap() + .send()? + .error_for_status()?; + Ok(()) + }) .map_err(construct_backoff_error) } @@ -531,14 +456,10 @@ impl WriteBackend for RestBackend { .url(tpe, id) .map_err(|err| construct_join_url_error(err, tpe, id, &self.url))?; - backoff::retry_notify( - self.backoff.clone(), - || { - _ = self.client.delete(url.clone()).send()?.check_error()?; - Ok(()) - }, - notify, - ) + self.retry_notify(|| { + _ = self.client.delete(url.clone()).send()?.error_for_status()?; + Ok(()) + }) .map_err(construct_backoff_error) } } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index e7ca9064..7a1535b2 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -45,7 +45,6 @@ displaydoc = { workspace = true } thiserror = { workspace = true } # macros -derivative = "2.2.0" derive_more = { version = "1.0.0", features = ["add", "constructor", "display", "from", "deref", "from_str"] } derive_setters = "0.1.6" diff --git a/crates/core/src/blob/tree.rs b/crates/core/src/blob/tree.rs index e28d079d..2e64a24e 100644 --- a/crates/core/src/blob/tree.rs +++ b/crates/core/src/blob/tree.rs @@ -8,7 +8,6 @@ use std::{ }; use crossbeam_channel::{bounded, unbounded, Receiver, Sender}; -use derivative::Derivative; use derive_setters::Setters; use ignore::overrides::{Override, OverrideBuilder}; use ignore::Match; @@ -455,8 +454,7 @@ impl IntoIterator for Tree { } #[cfg_attr(feature = "clap", derive(clap::Parser))] -#[derive(Derivative, Clone, Debug, Setters)] -#[derivative(Default)] +#[derive(Clone, Debug, Setters)] #[setters(into)] #[non_exhaustive] /// Options for listing the `Nodes` of a `Tree` @@ -488,10 +486,21 @@ pub struct TreeStreamerOptions { /// recursively list the dir #[cfg_attr(feature = "clap", clap(long))] - #[derivative(Default(value = "true"))] pub recursive: bool, } +impl Default for TreeStreamerOptions { + fn default() -> Self { + Self { + glob: Vec::default(), + iglob: Vec::default(), + glob_file: Vec::default(), + iglob_file: Vec::default(), + recursive: true, + } + } +} + /// [`NodeStreamer`] recursively streams all nodes of a given tree including all subtrees in-order #[derive(Debug, Clone)] pub struct NodeStreamer<'a, BE, I> diff --git a/crates/core/src/commands/check.rs b/crates/core/src/commands/check.rs index ca92a12c..7d0668ef 100644 --- a/crates/core/src/commands/check.rs +++ b/crates/core/src/commands/check.rs @@ -306,10 +306,24 @@ pub(crate) fn check_repository( packs.into_par_iter().for_each(|pack| { let id = pack.id; - let data = be.read_full(FileType::Pack, &id).unwrap(); + let data = match be.read_full(FileType::Pack, &id) { + Ok(data) => data, + Err(err) => { + // FIXME: This needs different handling, now it prints a full display of RusticError + // Instead we should actually collect and return a list of errors on the happy path + // for `Check`, as this is a non-critical operation and we want to show all errors + // to the user. + error!("Error reading data for pack {id} : {err}"); + return; + } + }; match check_pack(be, pack, data, &p) { Ok(()) => {} - Err(err) => error!("Error reading pack {id} : {err}",), + // FIXME: This needs different handling, now it prints a full display of RusticError + // Instead we should actually collect and return a list of errors on the happy path + // for `Check`, as this is a non-critical operation and we want to show all errors + // to the user. + Err(err) => error!("Pack {id} is not valid: {err}",), } }); p.finish(); diff --git a/crates/core/src/repofile/snapshotfile.rs b/crates/core/src/repofile/snapshotfile.rs index ec7eecc6..66718986 100644 --- a/crates/core/src/repofile/snapshotfile.rs +++ b/crates/core/src/repofile/snapshotfile.rs @@ -9,7 +9,6 @@ use std::{ use chrono::{DateTime, Duration, Local, OutOfRangeError}; #[cfg(feature = "clap")] use clap::ValueHint; -use derivative::Derivative; use derive_setters::Setters; use dunce::canonicalize; use gethostname::gethostname; @@ -162,9 +161,8 @@ impl SnapshotOptions { /// /// This is an extended version of the summaryOutput structure of restic in /// restic/internal/ui/backup$/json.go -#[derive(Serialize, Deserialize, Debug, Clone, Derivative)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] -#[derivative(Default)] #[non_exhaustive] pub struct SnapshotSummary { /// New files compared to the last (i.e. parent) snapshot @@ -229,11 +227,9 @@ pub struct SnapshotSummary { /// # Note /// /// This may differ from the snapshot `time`. - #[derivative(Default(value = "Local::now()"))] pub backup_start: DateTime, /// The time that the backup has been finished. - #[derivative(Default(value = "Local::now()"))] pub backup_end: DateTime, /// Total duration of the backup in seconds, i.e. the time between `backup_start` and `backup_end` @@ -243,6 +239,36 @@ pub struct SnapshotSummary { pub total_duration: f64, } +impl Default for SnapshotSummary { + fn default() -> Self { + Self { + files_new: Default::default(), + files_changed: Default::default(), + files_unmodified: Default::default(), + total_files_processed: Default::default(), + total_bytes_processed: Default::default(), + dirs_new: Default::default(), + dirs_changed: Default::default(), + dirs_unmodified: Default::default(), + total_dirs_processed: Default::default(), + total_dirsize_processed: Default::default(), + data_blobs: Default::default(), + tree_blobs: Default::default(), + data_added: Default::default(), + data_added_packed: Default::default(), + data_added_files: Default::default(), + data_added_files_packed: Default::default(), + data_added_trees: Default::default(), + data_added_trees_packed: Default::default(), + command: String::default(), + backup_start: Local::now(), + backup_end: Local::now(), + backup_duration: Default::default(), + total_duration: Default::default(), + } + } +} + impl SnapshotSummary { /// Create a new [`SnapshotSummary`]. /// @@ -269,11 +295,10 @@ impl SnapshotSummary { } /// Options for deleting snapshots. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Derivative, Copy)] -#[derivative(Default)] +#[derive(Serialize, Default, Deserialize, Debug, Clone, PartialEq, Eq, Copy)] pub enum DeleteOption { /// No delete option set. - #[derivative(Default)] + #[default] NotSet, /// This snapshot should be never deleted (remove-protection). Never, @@ -291,8 +316,7 @@ impl DeleteOption { impl_repofile!(SnapshotId, FileType::Snapshot, SnapshotFile); #[skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, Derivative)] -#[derivative(Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] /// A [`SnapshotFile`] is the repository representation of the snapshot metadata saved in a repository. /// /// It is usually saved in the repository under `snapshot/` @@ -302,14 +326,10 @@ impl_repofile!(SnapshotId, FileType::Snapshot, SnapshotFile); /// [`SnapshotFile`] implements [`Eq`], [`PartialEq`], [`Ord`], [`PartialOrd`] by comparing only the `time` field. /// If you need another ordering, you have to implement that yourself. pub struct SnapshotFile { - #[derivative(Default(value = "Local::now()"))] /// Timestamp of this snapshot pub time: DateTime, /// Program identifier and its version that have been used to create this snapshot. - #[derivative(Default( - value = "\"rustic \".to_string() + option_env!(\"PROJECT_VERSION\").unwrap_or(env!(\"CARGO_PKG_VERSION\"))" - ))] #[serde(default, skip_serializing_if = "String::is_empty")] pub program_version: String, @@ -364,6 +384,33 @@ pub struct SnapshotFile { pub id: SnapshotId, } +impl Default for SnapshotFile { + fn default() -> Self { + Self { + time: Local::now(), + program_version: { + let project_version = + option_env!("PROJECT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")); + format!("rustic {project_version}") + }, + parent: Option::default(), + tree: TreeId::default(), + label: String::default(), + paths: StringList::default(), + hostname: String::default(), + username: String::default(), + uid: Default::default(), + gid: Default::default(), + tags: StringList::default(), + original: Option::default(), + delete: DeleteOption::default(), + summary: Option::default(), + description: Option::default(), + id: SnapshotId::default(), + } + } +} + impl SnapshotFile { /// Create a [`SnapshotFile`] from [`SnapshotOptions`]. /// diff --git a/crates/core/src/repository.rs b/crates/core/src/repository.rs index ef474e2a..a0299ddd 100644 --- a/crates/core/src/repository.rs +++ b/crates/core/src/repository.rs @@ -418,8 +418,25 @@ impl Repository { /// /// The id of the config file or `None` if no config file is found pub fn config_id(&self) -> RusticResult> { - let config_ids = self.be.list(FileType::Config)?; + self.config_id_with_backend(&self.be) + } + /// Returns the Id of the config file corresponding to a specific backend. + /// + /// # Errors + /// + /// * If listing the repository config file failed + /// * If there is more than one repository config file. + /// + /// # Arguments + /// + /// * `be` - The backend to use + /// + /// # Returns + /// + /// The id of the config file or `None` if no config file is found + fn config_id_with_backend(&self, be: &dyn WriteBackend) -> RusticResult> { + let config_ids = be.list(FileType::Config)?; match config_ids.len() { 1 => Ok(Some(ConfigId::from(config_ids[0]))), 0 => Ok(None), @@ -574,7 +591,12 @@ impl Repository { key_opts: &KeyOptions, config_opts: &ConfigOptions, ) -> RusticResult> { - if self.config_id()?.is_some() { + let config_exists = self.config_id_with_backend(&self.be)?.is_some(); + let hot_config_exists = match self.be_hot { + None => false, + Some(ref be) => self.config_id_with_backend(be)?.is_some(), + }; + if config_exists || hot_config_exists { return Err(RusticError::new( ErrorKind::Configuration, "Config file already exists for `{name}`. Please check the repository.", diff --git a/deny.toml b/deny.toml index 6c630191..d2c10e72 100644 --- a/deny.toml +++ b/deny.toml @@ -74,10 +74,6 @@ ignore = [ # FIXME!: See https://github.com/RustCrypto/RSA/issues/19#issuecomment-1822995643. # There is no workaround available yet. "RUSTSEC-2023-0071", - # FIXME: backoff => used in backend, need to be replaced with backon - "RUSTSEC-2024-0384", - # FIXME: derivative => used for default impls - "RUSTSEC-2024-0388", # { id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, # "a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish # { crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },