use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use time::OffsetDateTime;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportCategory {
pub id: String,
pub name: String,
pub category_type: ExportCategoryType,
#[serde(default)]
pub optional: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExportCategoryType {
Settings,
SubSettings,
External,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ExportType {
#[default]
Full,
SettingsOnly,
Single {
settings_type: String,
name: String,
},
}
#[derive(Debug, Clone)]
pub struct BackupOptions {
pub output_dir: PathBuf,
pub export_type: ExportType,
pub password: Option<String>,
pub user_note: Option<String>,
pub include_settings: bool,
pub include_sub_settings: Vec<String>,
pub include_sub_settings_items: std::collections::HashMap<String, Vec<String>>,
pub include_external_configs: Vec<String>,
pub filename_suffix: Option<String>,
pub on_progress: Option<ProgressCallback>,
#[cfg(feature = "profiles")]
pub include_profiles: Vec<String>,
pub secret_policy: crate::SecretBackupPolicy,
}
#[derive(Clone)]
pub struct ProgressCallback(pub std::sync::Arc<dyn Fn(u64, u64) + Send + Sync>);
impl std::fmt::Debug for ProgressCallback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ProgressCallback")
}
}
impl std::ops::Deref for ProgressCallback {
type Target = dyn Fn(u64, u64) + Send + Sync;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl Default for BackupOptions {
fn default() -> Self {
Self {
output_dir: PathBuf::from("."),
export_type: ExportType::Full,
password: None,
user_note: None,
include_settings: true,
include_sub_settings: Vec::new(),
include_sub_settings_items: std::collections::HashMap::new(),
include_external_configs: Vec::new(),
filename_suffix: None,
on_progress: None,
#[cfg(feature = "profiles")]
include_profiles: Vec::new(),
secret_policy: crate::SecretBackupPolicy::default(),
}
}
}
impl BackupOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn secret_policy(mut self, policy: crate::SecretBackupPolicy) -> Self {
self.secret_policy = policy;
self
}
#[must_use]
pub fn output_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.output_dir = path.into();
self
}
#[must_use]
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
#[must_use]
pub fn note(mut self, note: impl Into<String>) -> Self {
self.user_note = Some(note.into());
self
}
#[must_use]
pub fn export_type(mut self, export_type: ExportType) -> Self {
self.export_type = export_type;
self
}
#[must_use]
pub fn include_settings(mut self, include: bool) -> Self {
self.include_settings = include;
self
}
#[must_use]
pub fn include_sub_settings(mut self, category: impl Into<String>) -> Self {
self.include_sub_settings.push(category.into());
self
}
#[must_use]
pub fn include_sub_settings_items(
mut self,
category: impl Into<String>,
items: &[impl AsRef<str>],
) -> Self {
let items: Vec<String> = items.iter().map(|s| s.as_ref().to_string()).collect();
self.include_sub_settings_items
.insert(category.into(), items);
self
}
#[must_use]
pub fn include_external(mut self, id: impl Into<String>) -> Self {
self.include_external_configs.push(id.into());
self
}
#[must_use]
pub fn filename_suffix(mut self, suffix: impl Into<String>) -> Self {
self.filename_suffix = Some(suffix.into());
self
}
#[must_use]
pub fn on_progress<F>(mut self, callback: F) -> Self
where
F: Fn(u64, u64) + Send + Sync + 'static,
{
self.on_progress = Some(ProgressCallback(std::sync::Arc::new(callback)));
self
}
#[cfg(feature = "profiles")]
#[must_use]
pub fn include_profile(mut self, profile: impl Into<String>) -> Self {
self.include_profiles.push(profile.into());
self
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RestoreControl {
pub overwrite_existing: bool,
pub dry_run: bool,
pub verify_checksum: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct RestoreScope {
pub restore_settings: bool,
}
impl Default for RestoreScope {
fn default() -> Self {
Self {
restore_settings: true,
}
}
}
#[derive(Debug, Clone, Default, Copy)]
pub struct RestoreFlags {
pub control: RestoreControl,
pub scope: RestoreScope,
}
#[derive(Debug, Clone, Default)]
pub struct RestoreOptions {
pub backup_path: PathBuf,
pub password: Option<String>,
pub flags: RestoreFlags,
pub restore_sub_settings: std::collections::HashMap<String, Vec<String>>,
pub restore_external_configs: Vec<String>,
#[cfg(feature = "profiles")]
pub restore_profile: Option<String>,
#[cfg(feature = "profiles")]
pub restore_profile_as: Option<String>,
}
impl RestoreOptions {
#[must_use]
pub fn from_path(path: impl Into<PathBuf>) -> Self {
Self {
backup_path: path.into(),
..Default::default()
}
}
#[must_use]
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
#[must_use]
pub fn dry_run(mut self, dry_run: bool) -> Self {
self.flags.control.dry_run = dry_run;
self
}
#[must_use]
pub fn overwrite(mut self, overwrite: bool) -> Self {
self.flags.control.overwrite_existing = overwrite;
self
}
#[must_use]
pub fn verify_checksum(mut self, verify: bool) -> Self {
self.flags.control.verify_checksum = verify;
self
}
#[must_use]
pub fn restore_settings(mut self, restore: bool) -> Self {
self.flags.scope.restore_settings = restore;
self
}
#[must_use]
pub fn restore_external(mut self, id: impl Into<String>) -> Self {
self.restore_external_configs.push(id.into());
self
}
#[must_use]
pub fn restore_sub_settings(mut self, category: impl Into<String>) -> Self {
self.restore_sub_settings
.insert(category.into(), Vec::new());
self
}
#[must_use]
pub fn restore_sub_settings_items(
mut self,
category: impl Into<String>,
items: &[impl AsRef<str>],
) -> Self {
let items: Vec<String> = items.iter().map(|s| s.as_ref().to_string()).collect();
self.restore_sub_settings.insert(category.into(), items);
self
}
#[cfg(feature = "profiles")]
#[must_use]
pub fn restore_profile(mut self, profile: impl Into<String>) -> Self {
self.restore_profile = Some(profile.into());
self
}
#[cfg(feature = "profiles")]
#[must_use]
pub fn restore_profile_as(mut self, name: impl Into<String>) -> Self {
self.restore_profile_as = Some(name.into());
self
}
}
#[derive(Debug, Clone)]
pub enum ExportSource {
File(PathBuf),
Command { program: String, args: Vec<String> },
Content(Vec<u8>),
}
pub type ImportHandler = std::sync::Arc<dyn Fn(&[u8]) -> crate::error::Result<()> + Send + Sync>;
#[derive(Clone)]
pub enum ImportTarget {
File(PathBuf),
Command { program: String, args: Vec<String> },
Handler(ImportHandler),
ReadOnly,
}
impl std::fmt::Debug for ImportTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImportTarget::File(p) => f.debug_tuple("File").field(p).finish(),
ImportTarget::Command { program, args } => f
.debug_struct("Command")
.field("program", program)
.field("args", args)
.finish(),
ImportTarget::Handler(_) => f.debug_tuple("Handler").field(&"<fn>").finish(),
ImportTarget::ReadOnly => write!(f, "ReadOnly"),
}
}
}
#[derive(Debug, Clone)]
pub struct ExternalConfig {
pub id: String,
pub archive_filename: String,
pub display_name: String,
pub description: Option<String>,
pub export_source: ExportSource,
pub import_target: ImportTarget,
pub is_sensitive: bool,
pub optional: bool,
pub is_directory: bool,
}
impl ExternalConfig {
pub fn new(id: impl Into<String>, path: impl Into<PathBuf>) -> Self {
let id = id.into();
let path = path.into();
let archive_filename = path
.file_name()
.map_or_else(|| format!("{id}.dat"), |s| s.to_string_lossy().to_string());
Self {
display_name: id.clone(),
id,
archive_filename,
export_source: ExportSource::File(path.clone()),
import_target: ImportTarget::File(path),
description: None,
is_sensitive: false,
optional: false,
is_directory: false,
}
}
pub fn from_command(id: impl Into<String>, archive_filename: impl Into<String>) -> Self {
let id = id.into();
Self {
display_name: id.clone(),
id,
archive_filename: archive_filename.into(),
export_source: ExportSource::Command {
program: String::new(),
args: Vec::new(),
},
import_target: ImportTarget::ReadOnly,
description: None,
is_sensitive: false,
optional: false,
is_directory: false,
}
}
pub fn from_content(
id: impl Into<String>,
archive_filename: impl Into<String>,
content: Vec<u8>,
) -> Self {
let id = id.into();
Self {
display_name: id.clone(),
id,
archive_filename: archive_filename.into(),
export_source: ExportSource::Content(content),
import_target: ImportTarget::ReadOnly,
description: None,
is_sensitive: false,
optional: false,
is_directory: false,
}
}
#[must_use]
pub fn export_command(mut self, program: impl Into<String>, args: &[&str]) -> Self {
self.export_source = ExportSource::Command {
program: program.into(),
args: args.iter().map(std::string::ToString::to_string).collect(),
};
self
}
#[must_use]
pub fn import_file(mut self, path: impl Into<PathBuf>) -> Self {
self.import_target = ImportTarget::File(path.into());
self
}
#[must_use]
pub fn import_command(mut self, program: impl Into<String>, args: &[&str]) -> Self {
self.import_target = ImportTarget::Command {
program: program.into(),
args: args.iter().map(std::string::ToString::to_string).collect(),
};
self
}
#[must_use]
pub fn import_handler<F>(mut self, handler: F) -> Self
where
F: Fn(&[u8]) -> crate::error::Result<()> + Send + Sync + 'static,
{
self.import_target = ImportTarget::Handler(std::sync::Arc::new(handler));
self
}
#[must_use]
pub fn import_read_only(mut self) -> Self {
self.import_target = ImportTarget::ReadOnly;
self
}
#[must_use]
pub fn display_name(mut self, name: impl Into<String>) -> Self {
self.display_name = name.into();
self
}
#[must_use]
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
#[must_use]
pub fn sensitive(mut self) -> Self {
self.is_sensitive = true;
self
}
#[must_use]
pub fn optional(mut self) -> Self {
self.optional = true;
self
}
#[must_use]
pub fn directory(mut self) -> Self {
self.is_directory = true;
self
}
#[must_use]
pub fn exists(&self) -> bool {
match &self.export_source {
ExportSource::File(path) => path.exists(),
ExportSource::Command { .. } | ExportSource::Content(_) => true,
}
}
}
pub trait ExternalConfigProvider: Send + Sync {
fn get_configs(&self) -> Vec<ExternalConfig>;
}
pub const MANIFEST_VERSION_CURRENT: u32 = 1;
pub const MANIFEST_VERSION_MIN_SUPPORTED: u32 = 1;
pub const MANIFEST_VERSION_MAX_SUPPORTED: u32 = 1;
#[must_use]
pub fn is_manifest_version_supported(version: u32) -> bool {
version >= MANIFEST_VERSION_MIN_SUPPORTED && version <= MANIFEST_VERSION_MAX_SUPPORTED
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupManifest {
pub version: u32,
pub backup: BackupInfo,
pub contents: BackupContents,
pub integrity: BackupIntegrity,
}
impl Default for BackupManifest {
fn default() -> Self {
Self {
version: MANIFEST_VERSION_CURRENT,
backup: BackupInfo::default(),
contents: BackupContents::default(),
integrity: BackupIntegrity::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupInfo {
pub app_name: String,
pub app_version: String,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
pub export_type: ExportType,
pub encrypted: bool,
pub user_note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secret_policy: Option<crate::SecretBackupPolicy>,
}
impl Default for BackupInfo {
fn default() -> Self {
Self {
app_name: String::new(),
app_version: String::new(),
created_at: OffsetDateTime::now_utc(),
export_type: ExportType::Full,
encrypted: false,
user_note: None,
secret_policy: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BackupIntegrity {
pub sha256: Option<String>,
pub size_bytes: u64,
pub compressed_size_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum ProfileEntry {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum SubSettingsManifestEntry {
SingleFile(String),
MultiFile(Vec<String>),
#[cfg(feature = "profiles")]
Profiled {
profiles: std::collections::HashMap<String, ProfileEntry>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BackupContents {
pub settings: bool,
pub sub_settings: std::collections::HashMap<String, SubSettingsManifestEntry>,
pub external_configs: Vec<String>,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub external_config_files: std::collections::HashMap<String, String>,
#[cfg(feature = "profiles")]
#[serde(skip_serializing_if = "Option::is_none")]
pub profiles: Option<Vec<String>>,
pub file_count: u32,
}
impl BackupContents {
#[must_use]
pub fn sub_settings_list(&self) -> std::collections::HashMap<String, Vec<String>> {
self.sub_settings
.iter()
.map(|(k, v)| {
let items = match v {
SubSettingsManifestEntry::SingleFile(_) => Vec::new(), SubSettingsManifestEntry::MultiFile(items) => items.clone(),
#[cfg(feature = "profiles")]
SubSettingsManifestEntry::Profiled { .. } => Vec::new(), };
(k.clone(), items)
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct BackupAnalysis {
pub manifest: BackupManifest,
pub is_valid: bool,
pub warnings: Vec<String>,
pub requires_password: bool,
pub created_at: String,
pub backup_type: String,
pub is_encrypted: bool,
pub format_version: String,
pub user_note: Option<String>,
}