add backup add

- change Storage::parent
- split path diff calc to util
- test for backup add
This commit is contained in:
qwjyh 2024-03-14 08:54:12 +09:00
parent 41b2924ad7
commit 905d392419
13 changed files with 387 additions and 63 deletions

View file

@ -18,7 +18,7 @@
- [x] use subcommand
- [ ] backup subcommands
- [ ] backup add
- [ ] test for backup add
- [?] test for backup add
- [ ] backup list
- [ ] backup done
- [ ] no commit option

View file

@ -1,17 +1,47 @@
use std::path::PathBuf;
use core::panic;
use std::{
collections::HashMap,
fs, io,
path::{Path, PathBuf},
};
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use crate::storages::Storage;
use crate::{
devices::Device,
storages::{self, Storage},
};
/// Directory to store backup configs for each devices.
pub const BACKUPSDIR: &str = "backups";
/// File to store backups for the `device`.
/// Relative path from the config directory.
pub fn backups_file(device: &Device) -> PathBuf {
PathBuf::from(BACKUPSDIR).join(format!("{}.yml", device.name()))
}
/// Targets for backup source or destination.
#[derive(Debug, Serialize, Deserialize)]
pub struct BackupTarget {
storage: Storage,
/// `name()` of [`Storage`].
/// Use `String` for serialization/deserialization.
storage: String,
/// Relative path to the `storage`.
path: PathBuf,
}
impl BackupTarget {
pub fn new(storage_name: String, relative_path: PathBuf) -> Self {
BackupTarget {
storage: storage_name,
path: relative_path,
}
}
}
/// Type of backup commands.
#[derive(Debug, Serialize, Deserialize)]
pub enum BackupCommand {
@ -26,6 +56,12 @@ pub struct ExternallyInvoked {
pub note: String,
}
impl ExternallyInvoked {
pub fn new(name: String, note: String) -> Self {
ExternallyInvoked { name, note }
}
}
/// Backup execution log.
#[derive(Debug, Serialize, Deserialize)]
pub struct BackupLog {
@ -53,3 +89,86 @@ pub struct Backup {
command: BackupCommand,
logs: Vec<BackupLog>,
}
impl Backup {
/// With empty logs.
pub fn new(
name: String,
device_name: String,
from: BackupTarget,
to: BackupTarget,
command: BackupCommand,
) -> Self {
Backup {
name,
device: device_name,
from,
to,
command,
logs: Vec::new(),
}
}
pub fn name(&self) -> &String {
&self.name
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Backups {
pub list: HashMap<String, Backup>,
}
impl Backups {
/// Empty [`Backups`].
pub fn new() -> Backups {
Backups {
list: HashMap::new(),
}
}
pub fn get(&self, name: &String) -> Option<&Backup> {
self.list.get(name)
}
/// Add new [`Backup`].
/// New `backup` must has new unique name.
pub fn add(&mut self, backup: Backup) -> Result<()> {
if self.list.keys().any(|name| name == &backup.name) {
return Err(anyhow::anyhow!(format!(
"Backup with name {} already exists",
backup.name
)));
}
match self.list.insert(backup.name.clone(), backup) {
Some(v) => {
error!("Inserted backup with existing name: {}", v.name);
panic!("unexpected behavior (unreachable)")
}
None => Ok(()),
}
}
pub fn read(config_dir: &Path, device: &Device) -> Result<Backups> {
let backups_file = config_dir.join(backups_file(device));
if !backups_file.exists() {
return Err(anyhow!("Couldn't find backups file: {:?}", backups_file));
}
trace!("Reading {}", backups_file.display());
let f = fs::File::open(backups_file)?;
let reader = io::BufReader::new(f);
let yaml: Backups =
serde_yaml::from_reader(reader).context("Failed to parse backups file")?;
Ok(yaml)
}
pub fn write(self, config_dir: &Path, device: &Device) -> Result<()> {
let f = fs::File::create(config_dir.join(backups_file(device)))
.context("Failed to open backups file")?;
let writer = io::BufWriter::new(f);
serde_yaml::to_writer(writer, &self).context(format!(
"Failed writing to {}",
config_dir.join(backups_file(device)).display()
))
}
}

View file

@ -86,6 +86,10 @@ pub(crate) enum StorageCommands {
#[arg(short, long)]
path: path::PathBuf,
},
// /// Remove storage from the storage list
// Remove {
// storage: String,
// }
}
#[derive(Args, Debug)]

81
src/cmd_backup.rs Normal file
View file

@ -0,0 +1,81 @@
use std::{io::stdout, path::{Path, PathBuf}};
use anyhow::{Context, Result};
use git2::Repository;
use crate::{
add_and_commit,
backups::{self, Backup, BackupCommand, BackupTarget, Backups, ExternallyInvoked},
cmd_args::BackupAddCommands,
devices::{self, Device},
storages::{StorageExt, Storages},
util,
};
pub(crate) fn cmd_backup_add(
name: String,
src: PathBuf,
dest: PathBuf,
cmd: BackupAddCommands,
repo: Repository,
config_dir: &PathBuf,
storages: &Storages,
) -> Result<()> {
let device = devices::get_device(&config_dir)?;
let new_backup = new_backup(name, src, dest, cmd, &device, storages)?;
let new_backup_name = new_backup.name().clone();
let mut backups = Backups::read(&config_dir, &device)?;
println!("Backup config:");
serde_yaml::to_writer(stdout(), &new_backup)?;
backups.add(new_backup)?;
backups.write(&config_dir, &device)?;
add_and_commit(
&repo,
&backups::backups_file(&device),
&format!("Add new backup: {}", new_backup_name),
)?;
println!("Added new backup.");
trace!("Finished adding backup");
Ok(())
}
fn new_backup(
name: String,
src: PathBuf,
dest: PathBuf,
cmd: BackupAddCommands,
device: &Device,
storages: &Storages,
) -> Result<Backup> {
let (src_parent, src_diff) =
util::min_parent_storage(&src, &storages, &device).context(format!(
"Coundn't find parent storage for src directory {}",
src.display()
))?;
let (dest_parent, dest_diff) =
util::min_parent_storage(&dest, &storages, &device).context(format!(
"Couldn't find parent storage for dest directory: {}",
dest.display()
))?;
let src_target = BackupTarget::new(src_parent.name().to_string(), src_diff);
trace!("Backup source target: {:?}", src_target);
let dest_target = BackupTarget::new(dest_parent.name().to_string(), dest_diff);
trace!("Backup destination target: {:?}", dest_target);
let command: BackupCommand = match cmd {
BackupAddCommands::External { name, note } => {
BackupCommand::ExternallyInvoked(ExternallyInvoked::new(name, note))
}
};
trace!("Backup command: {:?}", command);
Ok(Backup::new(
name,
device.name(),
src_target,
dest_target,
command,
))
}

View file

@ -1,14 +1,16 @@
//! Init subcommand.
//! Initialize xdbm for the device.
use crate::backups::{backups_file, Backups};
use crate::storages::{Storages, STORAGESFILE};
use crate::{add_and_commit, full_status, get_devices, write_devices, Device, DEVICESFILE};
use crate::{
add_and_commit, backups, full_status, get_devices, write_devices, Device, DEVICESFILE,
};
use anyhow::{anyhow, Context, Ok, Result};
use core::panic;
use git2::{Cred, RemoteCallbacks, Repository};
use inquire::Password;
use std::env;
use std::fs::File;
use std::fs::{DirBuilder, File};
use std::io::{BufWriter, Write};
use std::path::{self, Path, PathBuf};
@ -128,6 +130,9 @@ pub(crate) fn cmd_init(
&format!("Initialize {}", STORAGESFILE),
)?;
// set up directory for backups
DirBuilder::new().create(&config_dir.join(backups::BACKUPSDIR))?;
repo
}
};
@ -153,6 +158,8 @@ pub(crate) fn cmd_init(
let mut devices: Vec<Device> = get_devices(&config_dir)?;
trace!("devices: {:?}", devices);
if devices.iter().any(|x| x.name() == device.name()) {
error!("Device name `{}` is already used.", device.name());
error!("Clear the config directory and try again with different name");
return Err(anyhow!("device name is already used."));
}
devices.push(device.clone());
@ -167,6 +174,18 @@ pub(crate) fn cmd_init(
&Path::new(DEVICESFILE),
&format!("Add new device: {}", &device.name()),
)?;
// backups/[device].yml
{
let backups = Backups::new();
backups.write(&config_dir, &device)?;
}
add_and_commit(
&repo,
&backups::backups_file(&device),
&format!("Add new backups for device: {}", &device.name()),
)?;
println!("Device added");
full_status(&repo)?;
Ok(())

View file

@ -1,17 +1,14 @@
//! Storage subcommands.
use std::{
collections::HashMap,
io::{self, Write},
path::{Path, PathBuf},
string,
};
use anyhow::{anyhow, Context, Result};
use byte_unit::Byte;
use clap::{error::ErrorKind, CommandFactory};
use git2::Repository;
use inquire::{min_length, Confirm, CustomType, Select, Text};
use inquire::{Confirm, CustomType, Text};
use unicode_width::{self, UnicodeWidthStr};
use crate::{
@ -215,7 +212,7 @@ fn write_storages_list(
|v| v.display().to_string(),
);
let parent_name = if let Storage::SubDirectory(s) = storage {
s.parent(&storages)?
s.parent(&storages)
.context(format!("Failed to get parent of storage {}", s))?
.name()
} else {

View file

@ -19,7 +19,7 @@ use std::path::Path;
use std::path::{self, PathBuf};
use storages::Storages;
use crate::cmd_args::{Cli, Commands, StorageCommands};
use crate::cmd_args::{BackupSubCommands, Cli, Commands, StorageCommands};
use crate::storages::{
directory, local_info, online_storage, physical_drive_partition, Storage, StorageExt,
StorageType, STORAGESFILE,
@ -28,12 +28,14 @@ use devices::{Device, DEVICESFILE, *};
mod backups;
mod cmd_args;
mod cmd_backup;
mod cmd_init;
mod cmd_storage;
mod cmd_sync;
mod devices;
mod inquire_filepath_completer;
mod storages;
mod util;
fn main() -> Result<()> {
let cli = Cli::parse();
@ -93,7 +95,27 @@ fn main() -> Result<()> {
let _storages = Storages::read(&config_dir)?;
todo!()
}
Commands::Backup(_) => todo!(),
Commands::Backup(backup) => {
trace!("backup subcommand with args: {:?}", backup);
let repo = Repository::open(&config_dir).context(
"Repository doesn't exist on the config path. Please run init to initialize the repository.",
)?;
let storages = Storages::read(&config_dir)?;
match backup {
BackupSubCommands::Add {
name,
src,
dest,
cmd,
} => cmd_backup::cmd_backup_add(name, src, dest, cmd, repo, &config_dir, &storages)?,
BackupSubCommands::List {} => todo!(),
BackupSubCommands::Done {
name,
exit_status,
log,
} => todo!(),
}
}
}
full_status(&Repository::open(&config_dir)?)?;
Ok(())

View file

@ -97,7 +97,7 @@ impl StorageExt for Storage {
}
}
fn parent<'a>(&'a self, storages: &'a Storages) -> Result<Option<&Storage>> {
fn parent<'a>(&'a self, storages: &'a Storages) -> Option<&'a Storage> {
match self {
Storage::PhysicalStorage(s) => s.parent(storages),
Storage::SubDirectory(s) => s.parent(storages),
@ -146,7 +146,7 @@ pub trait StorageExt {
) -> Result<()>;
/// Get parent
fn parent<'a>(&'a self, storages: &'a Storages) -> Result<Option<&Storage>>;
fn parent<'a>(&'a self, storages: &'a Storages) -> Option<&Storage>;
}
pub mod directory;
@ -196,7 +196,6 @@ impl Storages {
// dependency check
if self.list.iter().any(|(_k, v)| {
v.parent(&self)
.unwrap()
.is_some_and(|parent| parent.name() == storage.name())
}) {
return Err(anyhow!(

View file

@ -9,6 +9,7 @@ use std::{
};
use crate::devices;
use crate::util;
use super::{local_info::LocalInfo, Storage, StorageExt, Storages};
@ -55,29 +56,13 @@ impl Directory {
device: &devices::Device,
storages: &Storages,
) -> Result<Directory> {
let (parent_name, diff_path) = storages
.list
.iter()
.filter(|(_k, v)| v.has_alias(&device))
.filter_map(|(k, v)| {
let diff = pathdiff::diff_paths(&path, v.mount_path(&device, &storages).unwrap())?;
trace!("Pathdiff: {:?}", diff);
if diff.components().any(|c| c == path::Component::ParentDir) {
None
} else {
Some((k, diff))
}
})
.min_by_key(|(_k, v)| {
let diff_paths: Vec<path::Component<'_>> = v.components().collect();
diff_paths.len()
})
.context(format!("Failed to compare diff of paths"))?;
trace!("Selected parent: {}", parent_name);
let (parent, diff_path) = util::min_parent_storage(&path, storages, &device)
.context("Failed to compare diff of paths")?;
trace!("Selected parent: {}", parent.name());
let local_info = LocalInfo::new(alias, path);
Ok(Directory::new(
name,
parent_name.clone(),
parent.name().to_string(),
diff_path,
notes,
HashMap::from([(device.name(), local_info)]),
@ -97,7 +82,7 @@ impl Directory {
/// Resolve mount path of directory with current device.
fn mount_path(&self, device: &devices::Device, storages: &Storages) -> Result<path::PathBuf> {
let parent_mount_path = self
.parent(&storages)?
.parent(&storages)
.context("Can't find parent storage")?
.mount_path(&device, &storages)?;
Ok(parent_mount_path.join(self.relative_path.clone()))
@ -117,12 +102,12 @@ impl StorageExt for Directory {
self.local_infos.get(&device.name())
}
fn mount_path(
&self,
device: &devices::Device,
storages: &Storages,
) -> Result<path::PathBuf> {
Ok(self.mount_path(device, storages)?)
fn mount_path(&self, device: &devices::Device, storages: &Storages) -> Result<path::PathBuf> {
Ok(self
.local_infos
.get(&device.name())
.context(format!("LocalInfo for storage: {} not found", &self.name()))?
.mount_path())
}
/// This method doesn't use `mount_path`.
@ -141,14 +126,8 @@ impl StorageExt for Directory {
}
// Get parent `&Storage` of directory.
fn parent<'a>(&'a self, storages: &'a Storages) -> Result<Option<&Storage>> {
match storages.get(&self.parent).context(format!(
"No parent {} exists for directory {}",
&self.parent, &self.name
)) {
Ok(s) => Ok(Some(s)),
Err(e) => Err(anyhow!(e)),
}
fn parent<'a>(&'a self, storages: &'a Storages) -> Option<&Storage> {
storages.get(&self.parent)
}
}
@ -205,10 +184,15 @@ mod test {
local_infos,
);
let mut storages = Storages::new();
storages.add(storages::Storage::PhysicalStorage(physical)).unwrap();
storages
.add(storages::Storage::PhysicalStorage(physical))
.unwrap();
storages.add(Storage::SubDirectory(directory)).unwrap();
// assert_eq!(directory.name(), "test_name");
assert_eq!(storages.get(&"test_name".to_string()).unwrap().name(), "test_name");
assert_eq!(
storages.get(&"test_name".to_string()).unwrap().name(),
"test_name"
);
assert_eq!(
storages
.get(&"test_name".to_string())

View file

@ -11,7 +11,7 @@ use crate::devices;
use super::{
local_info::{self, LocalInfo},
StorageExt, Storages,
Storage, StorageExt, Storages,
};
#[derive(Serialize, Deserialize, Debug)]
@ -88,11 +88,8 @@ impl StorageExt for OnlineStorage {
Ok(())
}
fn parent(
&self,
storages: &Storages,
) -> Result<Option<&super::Storage>> {
Ok(None)
fn parent(&self, storages: &Storages) -> Option<&Storage> {
None
}
}

View file

@ -165,8 +165,8 @@ impl StorageExt for PhysicalDrivePartition {
Ok(())
}
fn parent(&self, storages: &Storages) -> Result<Option<&Storage>> {
Ok(None)
fn parent(&self, storages: &Storages) -> Option<&Storage> {
None
}
}

37
src/util.rs Normal file
View file

@ -0,0 +1,37 @@
use std::path::{self, PathBuf};
use crate::{
devices::Device,
storages::{Storage, StorageExt, Storages},
};
/// Find the closest parent storage from the `path`.
pub fn min_parent_storage<'a>(
path: &PathBuf,
storages: &'a Storages,
device: &'a Device,
) -> Option<(&'a Storage, PathBuf)> {
let (name, pathdiff) = storages
.list
.iter()
.filter_map(|(k, storage)| {
let storage_path = match storage.mount_path(device, storages) {
Ok(path) => path,
Err(_) => return None,
};
let diff = pathdiff::diff_paths(&path, storage_path)?;
if diff.components().any(|c| c == path::Component::ParentDir) {
None
} else {
Some((k, diff))
}
})
.min_by_key(|(k, pathdiff)| {
pathdiff
.components()
.collect::<Vec<path::Component>>()
.len()
})?;
let storage = storages.get(name)?;
Some((storage, pathdiff))
}

View file

@ -181,7 +181,7 @@ mod cmd_init {
.success()
.stdout(predicate::str::contains(""));
// Add storage (directory)
let sample_directory = sample_storage.join("foo").join("bar");
let sample_directory = &sample_storage.join("foo").join("bar");
DirBuilder::new()
.recursive(true)
.create(&sample_directory)?;
@ -228,6 +228,71 @@ mod cmd_init {
.success()
.stdout(predicate::str::contains(""));
// storage 3
let sample_storage_2 = assert_fs::TempDir::new()?;
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(&config_dir_2.path())
.arg("storage")
.arg("add")
.arg("online")
.arg("--provider")
.arg("me")
.arg("--capacity")
.arg("1000000000000")
.arg("--alias")
.arg("nas")
.arg("nas")
.arg(&sample_storage_2.path())
.assert()
.success();
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(&config_dir_2.path())
.arg("storage")
.arg("list")
.arg("-l")
.assert()
.success();
// backup add
let backup_src = &sample_storage_2.join("foo").join("bar");
DirBuilder::new().recursive(true).create(&backup_src)?;
let backup_dest = &sample_directory.join("docs");
DirBuilder::new().recursive(true).create(&backup_dest)?;
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(&config_dir_2.path())
.arg("backup")
.arg("add")
.arg("--src")
.arg(&backup_src)
.arg("--dest")
.arg(&backup_dest)
.arg("foodoc")
.arg("external")
.arg("rsync")
.arg("note: nonsense")
.assert()
.success();
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(&config_dir_2.path())
.arg("backup")
.arg("add")
.arg("--src")
.arg(&backup_src)
.arg("--dest")
.arg(&backup_dest)
.arg("foodoc")
.arg("external")
.arg("rsync")
.arg("note: nonsense")
.assert()
.failure()
.stderr(predicate::str::contains("already"));
Ok(())
}
}