mirror of
https://github.com/qwjyh/xdbm
synced 2024-11-25 08:01:04 +09:00
add backup add
- change Storage::parent - split path diff calc to util - test for backup add
This commit is contained in:
parent
41b2924ad7
commit
905d392419
13 changed files with 387 additions and 63 deletions
|
@ -18,7 +18,7 @@
|
||||||
- [x] use subcommand
|
- [x] use subcommand
|
||||||
- [ ] backup subcommands
|
- [ ] backup subcommands
|
||||||
- [ ] backup add
|
- [ ] backup add
|
||||||
- [ ] test for backup add
|
- [?] test for backup add
|
||||||
- [ ] backup list
|
- [ ] backup list
|
||||||
- [ ] backup done
|
- [ ] backup done
|
||||||
- [ ] no commit option
|
- [ ] no commit option
|
||||||
|
|
125
src/backups.rs
125
src/backups.rs
|
@ -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 chrono::{DateTime, Local};
|
||||||
use serde::{Deserialize, Serialize};
|
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.
|
/// Targets for backup source or destination.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct BackupTarget {
|
pub struct BackupTarget {
|
||||||
storage: Storage,
|
/// `name()` of [`Storage`].
|
||||||
|
/// Use `String` for serialization/deserialization.
|
||||||
|
storage: String,
|
||||||
|
/// Relative path to the `storage`.
|
||||||
path: PathBuf,
|
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.
|
/// Type of backup commands.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum BackupCommand {
|
pub enum BackupCommand {
|
||||||
|
@ -26,6 +56,12 @@ pub struct ExternallyInvoked {
|
||||||
pub note: String,
|
pub note: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExternallyInvoked {
|
||||||
|
pub fn new(name: String, note: String) -> Self {
|
||||||
|
ExternallyInvoked { name, note }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Backup execution log.
|
/// Backup execution log.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct BackupLog {
|
pub struct BackupLog {
|
||||||
|
@ -53,3 +89,86 @@ pub struct Backup {
|
||||||
command: BackupCommand,
|
command: BackupCommand,
|
||||||
logs: Vec<BackupLog>,
|
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()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -86,6 +86,10 @@ pub(crate) enum StorageCommands {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
path: path::PathBuf,
|
path: path::PathBuf,
|
||||||
},
|
},
|
||||||
|
// /// Remove storage from the storage list
|
||||||
|
// Remove {
|
||||||
|
// storage: String,
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
|
|
81
src/cmd_backup.rs
Normal file
81
src/cmd_backup.rs
Normal 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,
|
||||||
|
))
|
||||||
|
}
|
|
@ -1,14 +1,16 @@
|
||||||
//! Init subcommand.
|
//! Init subcommand.
|
||||||
//! Initialize xdbm for the device.
|
//! Initialize xdbm for the device.
|
||||||
|
|
||||||
|
use crate::backups::{backups_file, Backups};
|
||||||
use crate::storages::{Storages, STORAGESFILE};
|
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 anyhow::{anyhow, Context, Ok, Result};
|
||||||
use core::panic;
|
use core::panic;
|
||||||
use git2::{Cred, RemoteCallbacks, Repository};
|
use git2::{Cred, RemoteCallbacks, Repository};
|
||||||
use inquire::Password;
|
use inquire::Password;
|
||||||
use std::env;
|
use std::fs::{DirBuilder, File};
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{BufWriter, Write};
|
use std::io::{BufWriter, Write};
|
||||||
use std::path::{self, Path, PathBuf};
|
use std::path::{self, Path, PathBuf};
|
||||||
|
|
||||||
|
@ -128,6 +130,9 @@ pub(crate) fn cmd_init(
|
||||||
&format!("Initialize {}", STORAGESFILE),
|
&format!("Initialize {}", STORAGESFILE),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// set up directory for backups
|
||||||
|
DirBuilder::new().create(&config_dir.join(backups::BACKUPSDIR))?;
|
||||||
|
|
||||||
repo
|
repo
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -153,6 +158,8 @@ pub(crate) fn cmd_init(
|
||||||
let mut devices: Vec<Device> = get_devices(&config_dir)?;
|
let mut devices: Vec<Device> = get_devices(&config_dir)?;
|
||||||
trace!("devices: {:?}", devices);
|
trace!("devices: {:?}", devices);
|
||||||
if devices.iter().any(|x| x.name() == device.name()) {
|
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."));
|
return Err(anyhow!("device name is already used."));
|
||||||
}
|
}
|
||||||
devices.push(device.clone());
|
devices.push(device.clone());
|
||||||
|
@ -167,6 +174,18 @@ pub(crate) fn cmd_init(
|
||||||
&Path::new(DEVICESFILE),
|
&Path::new(DEVICESFILE),
|
||||||
&format!("Add new device: {}", &device.name()),
|
&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");
|
println!("Device added");
|
||||||
full_status(&repo)?;
|
full_status(&repo)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
//! Storage subcommands.
|
//! Storage subcommands.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use byte_unit::Byte;
|
use byte_unit::Byte;
|
||||||
use clap::{error::ErrorKind, CommandFactory};
|
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
use inquire::{min_length, Confirm, CustomType, Select, Text};
|
use inquire::{Confirm, CustomType, Text};
|
||||||
use unicode_width::{self, UnicodeWidthStr};
|
use unicode_width::{self, UnicodeWidthStr};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -215,7 +212,7 @@ fn write_storages_list(
|
||||||
|v| v.display().to_string(),
|
|v| v.display().to_string(),
|
||||||
);
|
);
|
||||||
let parent_name = if let Storage::SubDirectory(s) = storage {
|
let parent_name = if let Storage::SubDirectory(s) = storage {
|
||||||
s.parent(&storages)?
|
s.parent(&storages)
|
||||||
.context(format!("Failed to get parent of storage {}", s))?
|
.context(format!("Failed to get parent of storage {}", s))?
|
||||||
.name()
|
.name()
|
||||||
} else {
|
} else {
|
||||||
|
|
26
src/main.rs
26
src/main.rs
|
@ -19,7 +19,7 @@ use std::path::Path;
|
||||||
use std::path::{self, PathBuf};
|
use std::path::{self, PathBuf};
|
||||||
use storages::Storages;
|
use storages::Storages;
|
||||||
|
|
||||||
use crate::cmd_args::{Cli, Commands, StorageCommands};
|
use crate::cmd_args::{BackupSubCommands, Cli, Commands, StorageCommands};
|
||||||
use crate::storages::{
|
use crate::storages::{
|
||||||
directory, local_info, online_storage, physical_drive_partition, Storage, StorageExt,
|
directory, local_info, online_storage, physical_drive_partition, Storage, StorageExt,
|
||||||
StorageType, STORAGESFILE,
|
StorageType, STORAGESFILE,
|
||||||
|
@ -28,12 +28,14 @@ use devices::{Device, DEVICESFILE, *};
|
||||||
|
|
||||||
mod backups;
|
mod backups;
|
||||||
mod cmd_args;
|
mod cmd_args;
|
||||||
|
mod cmd_backup;
|
||||||
mod cmd_init;
|
mod cmd_init;
|
||||||
mod cmd_storage;
|
mod cmd_storage;
|
||||||
mod cmd_sync;
|
mod cmd_sync;
|
||||||
mod devices;
|
mod devices;
|
||||||
mod inquire_filepath_completer;
|
mod inquire_filepath_completer;
|
||||||
mod storages;
|
mod storages;
|
||||||
|
mod util;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
@ -93,7 +95,27 @@ fn main() -> Result<()> {
|
||||||
let _storages = Storages::read(&config_dir)?;
|
let _storages = Storages::read(&config_dir)?;
|
||||||
todo!()
|
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)?)?;
|
full_status(&Repository::open(&config_dir)?)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -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 {
|
match self {
|
||||||
Storage::PhysicalStorage(s) => s.parent(storages),
|
Storage::PhysicalStorage(s) => s.parent(storages),
|
||||||
Storage::SubDirectory(s) => s.parent(storages),
|
Storage::SubDirectory(s) => s.parent(storages),
|
||||||
|
@ -146,7 +146,7 @@ pub trait StorageExt {
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
/// Get parent
|
/// 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;
|
pub mod directory;
|
||||||
|
@ -196,7 +196,6 @@ impl Storages {
|
||||||
// dependency check
|
// dependency check
|
||||||
if self.list.iter().any(|(_k, v)| {
|
if self.list.iter().any(|(_k, v)| {
|
||||||
v.parent(&self)
|
v.parent(&self)
|
||||||
.unwrap()
|
|
||||||
.is_some_and(|parent| parent.name() == storage.name())
|
.is_some_and(|parent| parent.name() == storage.name())
|
||||||
}) {
|
}) {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
|
|
|
@ -9,6 +9,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::devices;
|
use crate::devices;
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
use super::{local_info::LocalInfo, Storage, StorageExt, Storages};
|
use super::{local_info::LocalInfo, Storage, StorageExt, Storages};
|
||||||
|
|
||||||
|
@ -55,29 +56,13 @@ impl Directory {
|
||||||
device: &devices::Device,
|
device: &devices::Device,
|
||||||
storages: &Storages,
|
storages: &Storages,
|
||||||
) -> Result<Directory> {
|
) -> Result<Directory> {
|
||||||
let (parent_name, diff_path) = storages
|
let (parent, diff_path) = util::min_parent_storage(&path, storages, &device)
|
||||||
.list
|
.context("Failed to compare diff of paths")?;
|
||||||
.iter()
|
trace!("Selected parent: {}", parent.name());
|
||||||
.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 local_info = LocalInfo::new(alias, path);
|
let local_info = LocalInfo::new(alias, path);
|
||||||
Ok(Directory::new(
|
Ok(Directory::new(
|
||||||
name,
|
name,
|
||||||
parent_name.clone(),
|
parent.name().to_string(),
|
||||||
diff_path,
|
diff_path,
|
||||||
notes,
|
notes,
|
||||||
HashMap::from([(device.name(), local_info)]),
|
HashMap::from([(device.name(), local_info)]),
|
||||||
|
@ -97,7 +82,7 @@ impl Directory {
|
||||||
/// Resolve mount path of directory with current device.
|
/// Resolve mount path of directory with current device.
|
||||||
fn mount_path(&self, device: &devices::Device, storages: &Storages) -> Result<path::PathBuf> {
|
fn mount_path(&self, device: &devices::Device, storages: &Storages) -> Result<path::PathBuf> {
|
||||||
let parent_mount_path = self
|
let parent_mount_path = self
|
||||||
.parent(&storages)?
|
.parent(&storages)
|
||||||
.context("Can't find parent storage")?
|
.context("Can't find parent storage")?
|
||||||
.mount_path(&device, &storages)?;
|
.mount_path(&device, &storages)?;
|
||||||
Ok(parent_mount_path.join(self.relative_path.clone()))
|
Ok(parent_mount_path.join(self.relative_path.clone()))
|
||||||
|
@ -117,12 +102,12 @@ impl StorageExt for Directory {
|
||||||
self.local_infos.get(&device.name())
|
self.local_infos.get(&device.name())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mount_path(
|
fn mount_path(&self, device: &devices::Device, storages: &Storages) -> Result<path::PathBuf> {
|
||||||
&self,
|
Ok(self
|
||||||
device: &devices::Device,
|
.local_infos
|
||||||
storages: &Storages,
|
.get(&device.name())
|
||||||
) -> Result<path::PathBuf> {
|
.context(format!("LocalInfo for storage: {} not found", &self.name()))?
|
||||||
Ok(self.mount_path(device, storages)?)
|
.mount_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method doesn't use `mount_path`.
|
/// This method doesn't use `mount_path`.
|
||||||
|
@ -141,14 +126,8 @@ impl StorageExt for Directory {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get parent `&Storage` of directory.
|
// Get parent `&Storage` of directory.
|
||||||
fn parent<'a>(&'a self, storages: &'a Storages) -> Result<Option<&Storage>> {
|
fn parent<'a>(&'a self, storages: &'a Storages) -> Option<&Storage> {
|
||||||
match storages.get(&self.parent).context(format!(
|
storages.get(&self.parent)
|
||||||
"No parent {} exists for directory {}",
|
|
||||||
&self.parent, &self.name
|
|
||||||
)) {
|
|
||||||
Ok(s) => Ok(Some(s)),
|
|
||||||
Err(e) => Err(anyhow!(e)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,10 +184,15 @@ mod test {
|
||||||
local_infos,
|
local_infos,
|
||||||
);
|
);
|
||||||
let mut storages = Storages::new();
|
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();
|
storages.add(Storage::SubDirectory(directory)).unwrap();
|
||||||
// assert_eq!(directory.name(), "test_name");
|
// 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!(
|
assert_eq!(
|
||||||
storages
|
storages
|
||||||
.get(&"test_name".to_string())
|
.get(&"test_name".to_string())
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::devices;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
local_info::{self, LocalInfo},
|
local_info::{self, LocalInfo},
|
||||||
StorageExt, Storages,
|
Storage, StorageExt, Storages,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -88,11 +88,8 @@ impl StorageExt for OnlineStorage {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parent(
|
fn parent(&self, storages: &Storages) -> Option<&Storage> {
|
||||||
&self,
|
None
|
||||||
storages: &Storages,
|
|
||||||
) -> Result<Option<&super::Storage>> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -165,8 +165,8 @@ impl StorageExt for PhysicalDrivePartition {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parent(&self, storages: &Storages) -> Result<Option<&Storage>> {
|
fn parent(&self, storages: &Storages) -> Option<&Storage> {
|
||||||
Ok(None)
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
37
src/util.rs
Normal file
37
src/util.rs
Normal 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))
|
||||||
|
}
|
67
tests/cli.rs
67
tests/cli.rs
|
@ -181,7 +181,7 @@ mod cmd_init {
|
||||||
.success()
|
.success()
|
||||||
.stdout(predicate::str::contains(""));
|
.stdout(predicate::str::contains(""));
|
||||||
// Add storage (directory)
|
// Add storage (directory)
|
||||||
let sample_directory = sample_storage.join("foo").join("bar");
|
let sample_directory = &sample_storage.join("foo").join("bar");
|
||||||
DirBuilder::new()
|
DirBuilder::new()
|
||||||
.recursive(true)
|
.recursive(true)
|
||||||
.create(&sample_directory)?;
|
.create(&sample_directory)?;
|
||||||
|
@ -228,6 +228,71 @@ mod cmd_init {
|
||||||
.success()
|
.success()
|
||||||
.stdout(predicate::str::contains(""));
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue