Merge branch 'master'
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
backup.dat
|
||||||
Generated
+16
@@ -0,0 +1,16 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.98"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zfs_backup"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "zfs_backup"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.98"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
backup
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
backup.dat
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
test
|
||||||
|
test2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
zroot
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
test
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use crate::cmd::run_command;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub fn open(path: &Path, name: &str) -> Result<()> {
|
||||||
|
run_command(
|
||||||
|
"/usr/bin/cryptsetup",
|
||||||
|
vec!["open", path.to_str().unwrap(), name],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(name: &str) -> Result<()> {
|
||||||
|
run_command("/usr/bin/cryptsetup", vec!["close", name])
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
pub mod cryptsetup;
|
||||||
|
pub mod zfs;
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow, bail};
|
||||||
|
use std::process::{Child, Command, Output, Stdio};
|
||||||
|
|
||||||
|
fn run_command(cmd: &str, args: Vec<&str>) -> Result<()> {
|
||||||
|
let mut command = Command::new(cmd);
|
||||||
|
command.args(args);
|
||||||
|
command.stdout(std::process::Stdio::null());
|
||||||
|
let status = command.status()?;
|
||||||
|
if !status.success() {
|
||||||
|
bail!("error on running command {} {:?}", cmd, command.get_args());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_output(cmd: &str, args: Vec<&str>) -> Result<Output> {
|
||||||
|
let mut command = Command::new(cmd);
|
||||||
|
command.args(args);
|
||||||
|
|
||||||
|
let output = command.output()?;
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_command_with_output_pipe(cmd: &str, args: Vec<&str>) -> Result<Child> {
|
||||||
|
let mut command = Command::new(cmd);
|
||||||
|
command.args(args);
|
||||||
|
command.stdout(Stdio::piped());
|
||||||
|
let child = command.spawn()?;
|
||||||
|
Ok(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_command_with_input_pipe(cmd: &str, args: Vec<&str>, handle: Child) -> Result<()> {
|
||||||
|
let mut command = Command::new(cmd);
|
||||||
|
command.args(args);
|
||||||
|
command.stdin(Stdio::from(
|
||||||
|
handle.stdout.ok_or(anyhow!("cannot get stdout"))?,
|
||||||
|
));
|
||||||
|
command.output()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use std::process::Child;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
use crate::cmd::{
|
||||||
|
get_output, run_command, run_command_with_input_pipe, run_command_with_output_pipe,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn zpool_import(pool_name: &str) -> Result<()> {
|
||||||
|
run_command(
|
||||||
|
"zpool",
|
||||||
|
vec!["import", "-d", "/dev/mapper", "-N", pool_name],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zpool_export(pool_name: &str) -> Result<()> {
|
||||||
|
run_command("zpool", vec!["export", pool_name])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn zfs_list_snapshots(dataset_name: &str) -> Result<Vec<String>> {
|
||||||
|
let output = get_output(
|
||||||
|
"zfs",
|
||||||
|
vec![
|
||||||
|
"list",
|
||||||
|
"-t",
|
||||||
|
"snapshot",
|
||||||
|
"-H",
|
||||||
|
"-o",
|
||||||
|
"name",
|
||||||
|
dataset_name
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
let items = String::from_utf8(output.stdout)?;
|
||||||
|
|
||||||
|
let result = items
|
||||||
|
.lines()
|
||||||
|
.map(|snapshot_raw| {
|
||||||
|
let after_at = snapshot_raw.split_once('@').unwrap().1;
|
||||||
|
String::from(after_at)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pool_exists(pool: &str) -> bool {
|
||||||
|
run_command("zfs", vec!["list", pool]).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zfs_send_absolute(qualified_name: &str) -> Result<Child> {
|
||||||
|
run_command_with_output_pipe("zfs", vec!["send", "-R", qualified_name])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zfs_send_incremental(from: &str, to: &str) -> Result<Child> {
|
||||||
|
run_command_with_output_pipe("zfs", vec!["send", "-R", "-I", from, to])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zfs_receive(qualified_name: &str, child: Child) -> Result<()> {
|
||||||
|
run_command_with_input_pipe(
|
||||||
|
"zfs",
|
||||||
|
vec![
|
||||||
|
"receive",
|
||||||
|
"-d", // discard first element of sent snapshot's filename
|
||||||
|
"-F", // rollback to most recent snapshot
|
||||||
|
"-u", // do not mount received filesystem
|
||||||
|
qualified_name,
|
||||||
|
],
|
||||||
|
child,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zfs_list_main_datasets(name: &str) -> Result<Vec<String>> {
|
||||||
|
let output = get_output(
|
||||||
|
"zfs",
|
||||||
|
vec!["list", "-H", "-o", "name", "-r", "-d", "1", name],
|
||||||
|
)
|
||||||
|
.context("running zfs list ...")?;
|
||||||
|
let stdout = String::from_utf8(output.stdout).context("converting to String")?;
|
||||||
|
|
||||||
|
let result = stdout.lines().map(String::from).collect();
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub fn read_var_from_file(filename: &str) -> Result<String> {
|
||||||
|
let content = std::fs::read_to_string(PathBuf::from(filename))?;
|
||||||
|
let s = content.trim();
|
||||||
|
Ok(s.trim().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_vars_from_file(filename: &str) -> Result<Vec<String>> {
|
||||||
|
let content = std::fs::read_to_string(PathBuf::from(filename))?;
|
||||||
|
let s = content.trim();
|
||||||
|
Ok(s.lines().map(|s| s.to_owned()).collect())
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod cmd;
|
||||||
|
pub mod io;
|
||||||
|
pub mod zfs;
|
||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::io::stdin;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use zfs_backup::cmd::cryptsetup;
|
||||||
|
use zfs_backup::cmd::zfs::{zpool_export, zpool_import};
|
||||||
|
use zfs_backup::io::read_var_from_file;
|
||||||
|
use zfs_backup::zfs::{Dataset, Pool, Snapshot};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
verify_running_as_root()?;
|
||||||
|
|
||||||
|
decrypt_backup()?;
|
||||||
|
import_backup_pool()?;
|
||||||
|
do_backup()?;
|
||||||
|
export_backup_pool()?;
|
||||||
|
encrypt_backup()?;
|
||||||
|
|
||||||
|
println!("all done");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export the ZFS pool after the backup so it can be re-encrypted.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if the name of the backup pool is not provided or the pool
|
||||||
|
/// cannot be exported.
|
||||||
|
fn export_backup_pool() -> Result<()> {
|
||||||
|
let pool_name = &read_var_from_file("backup_pool.txt")?;
|
||||||
|
zpool_export(pool_name)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import the decrypted ZFS pool so that a backup can be sent.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if the name of the backup pool is not provided or the pool
|
||||||
|
/// cannot be imported.
|
||||||
|
fn import_backup_pool() -> Result<()> {
|
||||||
|
let pool_name = &read_var_from_file("backup_pool.txt")?;
|
||||||
|
zpool_import(pool_name)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_backup() -> Result<()> {
|
||||||
|
let (local, remote) =
|
||||||
|
load_zfs_pools("local_pool.txt", "backup_pool.txt").context("loading zfs pools")?;
|
||||||
|
let snapshot_tag = read_var_from_file("snapshot.txt").context("fetching snapshot tag")?;
|
||||||
|
let todos = Todo::plan(&local, &remote, snapshot_tag).context("planning todos")?;
|
||||||
|
if ask_user_confirmation(&todos).context("asking user confirmation")? {
|
||||||
|
println!("executing:");
|
||||||
|
execute_todos(local, remote, todos)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_todos(local: Pool, remote: Pool, todos: Vec<Todo>) -> Result<()> {
|
||||||
|
for todo in todos {
|
||||||
|
todo.print();
|
||||||
|
match todo {
|
||||||
|
Todo::Absolute(dataset, snapshot) => {
|
||||||
|
let handle = local.send_absolute(&dataset, &snapshot)?;
|
||||||
|
remote.receive(handle)?;
|
||||||
|
}
|
||||||
|
Todo::Incremental(dataset, common_snapshot, recent_snapshot) => {
|
||||||
|
let handle =
|
||||||
|
local.send_incremental(&dataset, &common_snapshot, &recent_snapshot)?;
|
||||||
|
remote.receive(handle)?;
|
||||||
|
}
|
||||||
|
Todo::UpToDate(_, _) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ask_user_confirmation(todos: &Vec<Todo>) -> Result<bool> {
|
||||||
|
println!("Would to the following:");
|
||||||
|
|
||||||
|
for todo in todos {
|
||||||
|
todo.print();
|
||||||
|
}
|
||||||
|
println!("Continue? [y/N] ");
|
||||||
|
let mut input = String::new();
|
||||||
|
let stdin = stdin();
|
||||||
|
stdin.read_line(&mut input).context("reading input line")?;
|
||||||
|
|
||||||
|
input = input.to_lowercase();
|
||||||
|
if input.starts_with('y') {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_zfs_pools(local_pool: &str, backup_pool: &str) -> Result<(Pool, Pool)> {
|
||||||
|
let mut local =
|
||||||
|
Pool::new(&read_var_from_file(local_pool).context("getting local pool from config")?)
|
||||||
|
.context("finding local pool")?;
|
||||||
|
local
|
||||||
|
.load_datasets_from_file("datasets.txt")
|
||||||
|
.context("loading datasets to send")?;
|
||||||
|
|
||||||
|
let mut remote =
|
||||||
|
Pool::new(&read_var_from_file(backup_pool).context("getting backup pool from config")?)
|
||||||
|
.context("finding backup pool")?;
|
||||||
|
remote
|
||||||
|
.get_datasets_from_system()
|
||||||
|
.context("loading remote datasets")?;
|
||||||
|
|
||||||
|
Ok((local, remote))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_backup() -> Result<()> {
|
||||||
|
// let mut uuid = "/dev/disk/by-uuid/".to_string(); // TODO change back
|
||||||
|
let mut pathbuf = PathBuf::from("/home/timeshifter/rust/zfs_backup/");
|
||||||
|
pathbuf.push(&read_var_from_file("backup_uuid.txt")?);
|
||||||
|
|
||||||
|
cryptsetup::open(&pathbuf, "crypt-backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_backup() -> Result<()> {
|
||||||
|
cryptsetup::close("crypt-backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_running_as_root() -> Result<()> {
|
||||||
|
let uid = env::var("USER")?;
|
||||||
|
if uid != "root" {
|
||||||
|
bail!("must be run as root")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Todo {
|
||||||
|
Absolute(Dataset, Snapshot),
|
||||||
|
Incremental(Dataset, Snapshot, Snapshot),
|
||||||
|
UpToDate(Dataset, Snapshot),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Todo {
|
||||||
|
fn print(&self) {
|
||||||
|
match self {
|
||||||
|
Todo::Absolute(dataset, snapshot) => {
|
||||||
|
println!(" {dataset}@{snapshot} -> [new]")
|
||||||
|
}
|
||||||
|
Todo::Incremental(dataset, last_common_snapshot, recent_snapshot) => {
|
||||||
|
println!(" {dataset}@{last_common_snapshot} -> ...@{recent_snapshot}")
|
||||||
|
}
|
||||||
|
Todo::UpToDate(dataset, snapshot) => {
|
||||||
|
println!(" {dataset}@{snapshot} is already backed up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plan(local: &Pool, remote: &Pool, snapshot_tag: String) -> Result<Vec<Self>> {
|
||||||
|
let mut result = vec![];
|
||||||
|
|
||||||
|
for local_dataset in &local.datasets {
|
||||||
|
let last_snapshot_with_tag =
|
||||||
|
local_dataset.find_last_snapshot_with_tag(&snapshot_tag)?;
|
||||||
|
|
||||||
|
let remote_dataset = local_dataset.change_pool_name(remote);
|
||||||
|
if !remote.contains(&remote_dataset) {
|
||||||
|
result.push(Todo::Absolute(
|
||||||
|
local_dataset.clone(),
|
||||||
|
last_snapshot_with_tag,
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let remote_dataset = remote.get_matching_dataset(local_dataset).unwrap();
|
||||||
|
|
||||||
|
let last_common_snapshot = Snapshot::find_last_common_snapshot_with_tag(
|
||||||
|
local_dataset,
|
||||||
|
&remote_dataset,
|
||||||
|
&snapshot_tag,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let todo = if last_common_snapshot == last_snapshot_with_tag {
|
||||||
|
Todo::UpToDate(local_dataset.clone(), last_common_snapshot)
|
||||||
|
} else {
|
||||||
|
Todo::Incremental(
|
||||||
|
local_dataset.clone(),
|
||||||
|
last_common_snapshot,
|
||||||
|
last_snapshot_with_tag,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(todo);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
+227
@@ -0,0 +1,227 @@
|
|||||||
|
use std::fmt::Write;
|
||||||
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::Child,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow, bail};
|
||||||
|
|
||||||
|
use crate::cmd::zfs::*;
|
||||||
|
use crate::io::read_vars_from_file;
|
||||||
|
|
||||||
|
/// A ZFS pool. It holds the name of the pool and the list of datasets belonging to it.
|
||||||
|
pub struct Pool {
|
||||||
|
name: String,
|
||||||
|
pub datasets: Vec<Dataset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pool {
|
||||||
|
pub fn new(arg: &str) -> Result<Self> {
|
||||||
|
let result = Self {
|
||||||
|
name: arg.to_string(),
|
||||||
|
datasets: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !result.exists() {
|
||||||
|
bail!("pool does not exist")
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(&self) -> bool {
|
||||||
|
pool_exists(&self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_absolute(&self, dataset_to_send: &Dataset, snapshot: &Snapshot) -> Result<Child> {
|
||||||
|
assert!(self.datasets.contains(dataset_to_send));
|
||||||
|
|
||||||
|
let mut qualified_name = dataset_to_send.0.to_str().unwrap().to_string();
|
||||||
|
qualified_name.push('@');
|
||||||
|
qualified_name.push_str(&snapshot.0);
|
||||||
|
|
||||||
|
let handle = zfs_send_absolute(&qualified_name)?;
|
||||||
|
Ok(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_incremental(
|
||||||
|
&self,
|
||||||
|
dataset_to_send: &Dataset,
|
||||||
|
common_snapshot: &Snapshot,
|
||||||
|
recent_snapshot: &Snapshot,
|
||||||
|
) -> Result<Child> {
|
||||||
|
let mut from = String::new();
|
||||||
|
let mut to = String::new();
|
||||||
|
write!(from, "@{common_snapshot}")?;
|
||||||
|
write!(to, "{dataset_to_send}@{recent_snapshot}")?;
|
||||||
|
zfs_send_incremental(&from, &to)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive(&self, handle: Child) -> Result<()> {
|
||||||
|
zfs_receive(&self.name, handle)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_datasets_from_file(&mut self, filename: &str) -> Result<()> {
|
||||||
|
self.datasets = Dataset::from_file(&PathBuf::from(filename), &self.name)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_datasets_from_system(&mut self) -> Result<()> {
|
||||||
|
let datasets = zfs_list_main_datasets(&self.name)?;
|
||||||
|
|
||||||
|
self.datasets = datasets
|
||||||
|
.iter()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(Dataset::new)
|
||||||
|
.skip(1)
|
||||||
|
.collect(); // first one is the Pool
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, dataset: &Dataset) -> bool {
|
||||||
|
self.datasets.contains(dataset)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_matching_dataset(&self, dataset_from_other_pool: &Dataset) -> Option<Dataset> {
|
||||||
|
let mut required_name = PathBuf::new();
|
||||||
|
required_name.push(&self.name);
|
||||||
|
dataset_from_other_pool
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.for_each(|element| required_name.push(element));
|
||||||
|
let matching_dataset = Dataset::new(required_name);
|
||||||
|
if self.contains(&matching_dataset) {
|
||||||
|
Some(matching_dataset)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import(name: &str) -> Result<Pool> {
|
||||||
|
zpool_import(name)?;
|
||||||
|
let result = Pool::new(name)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full path of a ZFS dataset.
|
||||||
|
///
|
||||||
|
/// E.g. if the name of the pool is `tank`, with the elements `ROOT` and `default` below it, this
|
||||||
|
/// struct would contain `tank/ROOT/default`.
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct Dataset(PathBuf);
|
||||||
|
|
||||||
|
impl Dataset {
|
||||||
|
fn new(path: PathBuf) -> Self {
|
||||||
|
Self(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_file(filename: &Path, pool: &str) -> Result<Vec<Self>> {
|
||||||
|
let datasets = read_vars_from_file(filename.to_str().unwrap())?;
|
||||||
|
Ok(datasets
|
||||||
|
.into_iter()
|
||||||
|
.map(|ds| {
|
||||||
|
let mut pb = PathBuf::new();
|
||||||
|
pb.push(pool);
|
||||||
|
pb.push(ds);
|
||||||
|
Self::new(pb)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_snapshots_like_tag(&self, snapshot_tag: &str) -> Result<Vec<Snapshot>> {
|
||||||
|
let snapshots = zfs_list_snapshots(self.0.to_str().unwrap())?;
|
||||||
|
|
||||||
|
let mut result = vec![];
|
||||||
|
|
||||||
|
for item in snapshots {
|
||||||
|
let snapshot_name = item.split('@').next_back().unwrap();
|
||||||
|
if snapshot_name.contains(snapshot_tag) {
|
||||||
|
result.push(Snapshot(snapshot_name.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_last_snapshot_with_tag(&self, snapshot_tag: &str) -> Result<Snapshot> {
|
||||||
|
let mut snapshots = self.find_snapshots_like_tag(snapshot_tag)?;
|
||||||
|
snapshots.sort_unstable();
|
||||||
|
let result = snapshots
|
||||||
|
.last()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(anyhow!("could not find any snapshots"))?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_snapshots(&self) -> Result<Vec<Snapshot>> {
|
||||||
|
let snapshots_string = zfs_list_snapshots(self.0.to_str().unwrap())?;
|
||||||
|
let snapshots = snapshots_string
|
||||||
|
.into_iter()
|
||||||
|
// TODO should snapshot just take a string?
|
||||||
|
.map(|s| Snapshot::new(&s))
|
||||||
|
.collect();
|
||||||
|
Ok(snapshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn change_pool_name(&self, new_pool: &Pool) -> Self {
|
||||||
|
let new_pool = new_pool.name.as_str();
|
||||||
|
let mut pathbuf = PathBuf::new();
|
||||||
|
pathbuf.push(new_pool);
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.for_each(|element| pathbuf.push(element));
|
||||||
|
|
||||||
|
Self(pathbuf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Dataset {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0.to_str().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
|
||||||
|
/// A ZFS snapshot.
|
||||||
|
///
|
||||||
|
/// If the absolute ZFS path were "zpool/dataset@snapshot", this would contain "snapshot".
|
||||||
|
pub struct Snapshot(String);
|
||||||
|
|
||||||
|
impl Snapshot {
|
||||||
|
fn new(name: &str) -> Self {
|
||||||
|
Self(name.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_last_common_snapshot_with_tag(
|
||||||
|
ds1: &Dataset,
|
||||||
|
ds2: &Dataset,
|
||||||
|
tag: &str,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let sn1 = ds1.get_snapshots().ok()?;
|
||||||
|
let sn2 = ds2.get_snapshots().ok()?;
|
||||||
|
let mut common = vec![];
|
||||||
|
|
||||||
|
for snapshot in sn1 {
|
||||||
|
if !snapshot.0.contains(tag) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if sn2.contains(&snapshot) {
|
||||||
|
common.push(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
common.sort();
|
||||||
|
common.last().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Snapshot {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user