Commit 6412af76 authored by Sophie Herold's avatar Sophie Herold

Introduce config format version 1

parent 200a19ad
pub mod prelude;
mod utils;
use arc_swap::ArcSwap;
......@@ -5,60 +6,10 @@ use arc_swap::ArcSwap;
use std::io::{BufRead, BufReader};
use std::sync::Arc;
use super::shared::{self, *};
use crate::shared::{self, *};
use crate::ui::prelude::*;
use utils::*;
/*
thread_local!(
static SERVICE: Service = Service {
volume_monitor: gio::VolumeMonitor::get(),
}
);
struct Service {
volume_monitor: gio::VolumeMonitor,
}
*/
pub fn init_device_listening() {
// TODO: Reactivate detection
/*
SERVICE.with(|service| {
service.volume_monitor.connect_mount_added(|_, mount| {
ui::APP.with(|app| {
let backups = &SETTINGS.load().backups;
let uuid = shared::get_mount_uuid(mount);
if let Some(uuid) = uuid {
let backup = backups
.values()
.find(|b| b.volume_uuid.as_ref() == Some(&uuid));
if let Some(backup) = backup {
let notification = gio::Notification::new("Backup Medium Connected");
notification.set_body(Some(
format!(
"{} on Disk '{}'",
backup.label.as_ref().unwrap(),
&backup.device.as_ref().unwrap()
)
.as_str(),
));
notification.add_button_with_target_value(
"Run Backup",
"app.detail",
Some(&backup.id.to_variant()),
);
gtk_app()
.send_notification(Some(uuid.as_str()), &notification);
}
}
});
});
});
*/
}
#[derive(Default, Debug, Clone)]
pub struct Status {
pub run: Run,
......@@ -102,13 +53,15 @@ pub struct StatsArchiveStats {
pub original_size: u64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Info {
pub archives: Vec<InfoArchive>,
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct List {
pub archives: Vec<ListArchive>,
pub encryption: Encryption,
pub repository: Repository,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct InfoArchive {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ListArchive {
pub id: String,
pub name: String,
pub comment: String,
......@@ -116,116 +69,95 @@ pub struct InfoArchive {
pub hostname: String,
pub start: chrono::naive::NaiveDateTime,
pub end: chrono::naive::NaiveDateTime,
pub stats: StatsArchiveStats,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct List {
pub archives: Vec<ListArchive>,
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Encryption {
pub mode: String,
pub keyfile: Option<std::path::PathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ListArchive {
pub struct Repository {
pub id: String,
pub name: String,
pub comment: String,
pub username: String,
pub hostname: String,
pub start: chrono::naive::NaiveDateTime,
pub end: chrono::naive::NaiveDateTime,
pub last_modified: chrono::naive::NaiveDateTime,
pub location: std::path::PathBuf,
}
#[derive(Clone)]
pub struct Borg {
config: BackupConfig,
password: Option<Password>,
last: u64,
}
impl Borg {
pub fn new(config: BackupConfig) -> Self {
Self {
config,
password: None,
last: 1000,
}
}
#[derive(Clone)]
pub struct BorgOnlyRepo {
repo: BackupRepo,
password: Option<Password>,
}
pub fn get_config(&self) -> BackupConfig {
self.config.clone()
}
pub trait BorgRunConfig {
fn get_repo(&self) -> BackupRepo;
fn get_password(&self) -> Option<Password>;
fn unset_password(&mut self);
fn set_password(&mut self, password: Password);
fn is_encrypted(&self) -> bool;
fn get_config_id(&self) -> Option<String>;
}
pub fn set_password(&mut self, password: Password) {
impl BorgRunConfig for Borg {
fn get_repo(&self) -> BackupRepo {
self.config.repo.clone()
}
fn get_password(&self) -> Option<Password> {
self.password.clone()
}
fn set_password(&mut self, password: Password) {
self.password = Some(password);
}
pub fn unset_password(&mut self) {
fn unset_password(&mut self) {
self.password = None;
}
pub fn set_limit_last(&mut self, last: u64) -> &Self {
self.last = last;
self
fn is_encrypted(&self) -> bool {
self.config.encrypted
}
pub fn version() -> Result<String, BorgErr> {
let borg = BorgCall::new_raw()
.add_options(&["--log-json", "--version"])
.output()?;
check_stderr(&borg)?;
Ok(String::from_utf8_lossy(&borg.stdout).to_string())
fn get_config_id(&self) -> Option<String> {
Some(self.config.id.clone())
}
}
pub fn peek(&self) -> Result<(), BorgErr> {
let borg = BorgCall::new("list")
.add_options(&["--json", "--last=1"])
.add_envs(vec![
("BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK", "yes"),
("BORG_RELOCATED_REPO_ACCESS_IS_OK", "yes"),
])
.add_basics(self)?
.output()?;
check_stderr(&borg)?;
let _: serde_json::Value = serde_json::from_slice(&borg.stdout)?;
Ok(())
impl BorgRunConfig for BorgOnlyRepo {
fn get_repo(&self) -> BackupRepo {
self.repo.clone()
}
pub fn list(&self) -> Result<Vec<ListArchive>, BorgErr> {
let borg = BorgCall::new("list")
.add_options(&[
"--json",
"--last",
&self.last.to_string(),
"--format={hostname}{username}{comment}{end}",
])
.add_basics(self)?
.output()?;
check_stderr(&borg)?;
let json: List = serde_json::from_slice(&borg.stdout)?;
Ok(json.archives)
fn get_password(&self) -> Option<Password> {
self.password.clone()
}
fn set_password(&mut self, password: Password) {
self.password = Some(password);
}
fn unset_password(&mut self) {
self.password = None;
}
fn is_encrypted(&self) -> bool {
false
}
fn get_config_id(&self) -> Option<String> {
None
}
}
pub fn mount(&self) -> Result<(), BorgErr> {
std::fs::DirBuilder::new()
.recursive(true)
.create(self.get_mount_point())?;
let borg = BorgCall::new("mount")
.add_basics(self)?
.add_positional(&self.get_mount_point().to_string_lossy())
.output()?;
check_stderr(&borg)?;
/// Features that need a complete backup config
impl Borg {
pub fn new(config: BackupConfig) -> Self {
Self {
config,
password: None,
}
}
Ok(())
pub fn get_config(&self) -> BackupConfig {
self.config.clone()
}
pub fn umount(&self) -> Result<(), BorgErr> {
......@@ -244,7 +176,7 @@ impl Borg {
Ok(())
}
fn get_mount_dir() -> std::path::PathBuf {
pub fn get_mount_dir() -> std::path::PathBuf {
let mut dir = shared::get_home_dir();
dir.push(crate::REPO_MOUNT_DIR);
dir
......@@ -256,23 +188,14 @@ impl Borg {
dir
}
pub fn info(&self) -> Result<Info, BorgErr> {
let borg = BorgCall::new("info")
.add_options(&["--json", "--last=100"])
.add_basics(self)?
.output()?;
check_stderr(&borg)?;
let x = serde_json::from_slice(&borg.stdout)?;
Ok(x)
}
pub fn mount(&self) -> Result<(), BorgErr> {
std::fs::DirBuilder::new()
.recursive(true)
.create(self.get_mount_point())?;
pub fn init(&self) -> Result<(), BorgErr> {
let borg = BorgCall::new("init")
.add_options(&["--encryption=repokey"])
let borg = BorgCall::new("mount")
.add_basics(self)?
.add_positional(&self.get_mount_point().to_string_lossy())
.output()?;
check_stderr(&borg)?;
......@@ -381,6 +304,82 @@ impl Borg {
}
}
impl BorgOnlyRepo {
pub fn new(repo: BackupRepo) -> Self {
Self {
repo,
password: None,
}
}
}
impl BorgBasics for Borg {}
impl BorgBasics for BorgOnlyRepo {}
/// Features that are available without complete backup config
pub trait BorgBasics: BorgRunConfig + Sized + Clone + Send {
fn peek(&self) -> Result<List, BorgErr> {
let borg = BorgCall::new("list")
.add_options(&[
"--json",
"--last=1",
"--format={hostname}{username}{comment}{end}",
])
.add_envs(vec![
("BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK", "yes"),
("BORG_RELOCATED_REPO_ACCESS_IS_OK", "yes"),
])
.add_basics(self)?
.output()?;
check_stderr(&borg)?;
let json: List = serde_json::from_slice(&borg.stdout)?;
Ok(json)
}
fn list(&self) -> Result<Vec<ListArchive>, BorgErr> {
let borg = BorgCall::new("list")
.add_options(&[
"--json",
"--last",
//TODO: pass as arg
"100",
"--format={hostname}{username}{comment}{end}",
])
.add_basics(self)?
.output()?;
check_stderr(&borg)?;
let json: List = serde_json::from_slice(&borg.stdout)?;
Ok(json.archives)
}
fn init(&self) -> Result<List, BorgErr> {
let borg = BorgCall::new("init")
.add_options(&["--encryption=repokey"])
.add_basics(self)?
.output()?;
check_stderr(&borg)?;
self.peek()
}
}
pub fn version() -> Result<String, BorgErr> {
let borg = BorgCall::new_raw()
.add_options(&["--log-json", "--version"])
.output()?;
check_stderr(&borg)?;
Ok(String::from_utf8_lossy(&borg.stdout).to_string())
}
#[derive(Default, Debug, Clone)]
pub struct Communication {
pub status: Arc<ArcSwap<Status>>,
......
pub use super::{BorgBasics, BorgRunConfig};
use super::Borg;
use super::{Borg, BorgRunConfig};
use std::io::Write;
use std::os::unix::io::AsRawFd;
use std::os::unix::io::IntoRawFd;
......@@ -138,7 +138,7 @@ impl BorgCall {
self
}
pub fn add_password(&mut self, borg: &Borg) -> Result<&mut Self, BorgErr> {
pub fn add_password<T: BorgRunConfig>(&mut self, borg: &T) -> Result<&mut Self, BorgErr> {
// Password pipe
let (pipe_reader, mut pipe_writer) = std::os::unix::net::UnixStream::pair()?;
......@@ -159,15 +159,15 @@ impl BorgCall {
pipe_reader.into_raw_fd().to_string(),
);
if let Some(ref password) = borg.password {
if let Some(ref password) = borg.get_password() {
debug!("Using password enforced by explicitly passed password");
pipe_writer.write_all(password)?;
} else if borg.config.encrypted {
} else if borg.is_encrypted() {
debug!("Config says the backup is encrypted");
let password: Zeroizing<Vec<u8>> =
secret_service::SecretService::new(secret_service::EncryptionType::Dh)?
.search_items(vec![
("backup_id", &borg.config.id),
("backup_id", &borg.get_config_id().unwrap_or_default()),
("program", env!("CARGO_PKG_NAME")),
])?
.get(0)
......@@ -185,11 +185,11 @@ impl BorgCall {
Ok(self)
}
pub fn add_basics(&mut self, borg: &Borg) -> Result<&mut Self, BorgErr> {
pub fn add_basics<T: BorgRunConfig>(&mut self, borg: &T) -> Result<&mut Self, BorgErr> {
self.add_options(&["--log-json"]);
if self.positional.is_empty() {
self.add_positional(&borg.config.repo.to_string());
self.add_positional(&borg.get_repo().to_string());
}
self.add_password(borg)?;
......
......@@ -9,6 +9,17 @@ extern crate matches;
#[macro_use]
extern crate enclose;
static CONFIG_VERSION: u16 = 1;
static BORG_DELAY_RECONNECT: std::time::Duration = std::time::Duration::from_secs(60);
static BORG_LOCK_WAIT_RECONNECT: std::time::Duration = std::time::Duration::from_secs(60 * 7);
const REPO_MOUNT_DIR: &str = ".mnt/borg";
// require borg 1.1
const BORG_MIN_MAJOR: u32 = 1;
const BORG_MIN_MINOR: u32 = 1;
macro_rules! data_dir {
() => {
concat!(env!("CARGO_MANIFEST_DIR"), "/data")
......@@ -24,15 +35,6 @@ macro_rules! application_id {
static APPLICATION_ID: &str = application_id!();
static APPLICATION_NAME: &str = include_str!(concat!(data_dir!(), "/APPLICATION_NAME"));
static BORG_DELAY_RECONNECT: std::time::Duration = std::time::Duration::from_secs(60);
static BORG_LOCK_WAIT_RECONNECT: std::time::Duration = std::time::Duration::from_secs(60 * 7);
const REPO_MOUNT_DIR: &str = ".mnt/borg";
// require borg 1.1
const BORG_MIN_MAJOR: u32 = 1;
const BORG_MIN_MINOR: u32 = 1;
pub mod borg;
pub mod globals;
pub mod shared;
......
......@@ -7,14 +7,24 @@ use zeroize::Zeroizing;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BackupConfig {
#[serde(default)]
pub config_version: u16,
pub id: String,
#[serde(default = "fake_repo_id")]
pub repo_id: String,
pub repo: BackupRepo,
pub encrypted: bool,
#[serde(default)]
pub encryption_mode: String,
pub include: BTreeSet<path::PathBuf>,
pub exclude: BTreeSet<Pattern>,
pub last_run: Option<RunInfo>,
}
fn fake_repo_id() -> String {
format!("-randomid-{}", glib::uuid_string_random().to_string())
}
impl BackupConfig {
pub fn include_dirs(&self) -> Vec<path::PathBuf> {
let mut dirs = Vec::new();
......@@ -83,9 +93,9 @@ impl RunInfo {
pub type Password = Zeroizing<Vec<u8>>;
impl BackupConfig {
impl BackupRepo {
pub fn new_from_uri(uri: String) -> Self {
Self::new_from_repo(BackupRepo::Remote { uri })
BackupRepo::Remote { uri }
}
pub fn new_from_path(repo: &path::Path) -> Self {
......@@ -112,7 +122,7 @@ impl BackupConfig {
.as_ref()
.map(std::string::ToString::to_string);
Self::new_from_repo(BackupRepo::Local {
BackupRepo::Local {
path: repo.to_path_buf(),
icon,
label: mount
......@@ -125,19 +135,24 @@ impl BackupConfig {
.map(Into::into),
removable: drive.as_ref().map_or(false, gio::Drive::is_removable),
volume_uuid,
})
}
}
}
pub fn new_from_repo(repo: BackupRepo) -> Self {
impl BackupConfig {
pub fn new(repo: BackupRepo, info: borg::List, encrypted: bool) -> Self {
let mut include = std::collections::BTreeSet::new();
include.insert("".into());
let mut exclude = std::collections::BTreeSet::new();
exclude.insert(Pattern::PathPrefix(".cache".into()));
Self {
config_version: crate::CONFIG_VERSION,
id: glib::uuid_string_random().to_string(),
repo,
encrypted: false,
repo_id: info.repository.id,
encrypted,
encryption_mode: info.encryption.mode,
include,
exclude,
last_run: None,
......
......@@ -118,7 +118,6 @@ fn init(_app: &gtk::Application) {
init_actions();
init_timeouts();
borg::init_device_listening();
ui::page_archives::init();
ui::page_detail::init();
......@@ -207,7 +206,7 @@ fn init_actions() {
}
fn init_check_borg() {
let version_result = borg::Borg::version();
let version_result = borg::version();
ui::utils::dialog_catch_errb(
&version_result,
......
......@@ -5,6 +5,7 @@ use gtk::prelude::*;
use zeroize::Zeroizing;
use crate::borg;
use crate::borg::prelude::*;
use crate::shared;
use crate::shared::*;
use crate::ui;
......@@ -135,7 +136,7 @@ fn on_add_repo_list_activated(row: &gtk::ListBoxRow, ui: Rc<builder::DialogAddCo
let path = match glib::filename_from_uri(&name) {
Ok((path, _)) => path,
Err(err) => {
ui::utils::dialog_error(format!("URI conversion: {:?}", err));
ui::utils::show_error("URI conversion failed", err);
return;
}
};
......@@ -207,7 +208,7 @@ fn on_init_button_clicked(ui: Rc<builder::DialogAddConfig>) {
return;
}
let mut config = if ui.init_location().get_visible_child()
let repo = if ui.init_location().get_visible_child()
== Some(ui.init_local().upcast::<gtk::Widget>())
{
let mut path = std::path::PathBuf::new();
......@@ -222,47 +223,48 @@ fn on_init_button_clicked(ui: Rc<builder::DialogAddConfig>) {
path.push(ui.init_dir().get_text().as_str());
trace!("Init repo at {:?}", &path);
BackupConfig::new_from_path(&path)
BackupRepo::new_from_path(&path)
} else {
let url = ui.init_url().get_text().to_string();
if url.is_empty() {
ui::utils::dialog_error(gettext("You have to enter a repository location."));
return;
}
BackupConfig::new_from_uri(url)
BackupRepo::new_from_uri(url)
};
config.encrypted = encrypted;
let mut borg = borg::Borg::new(config.clone());
page_pending::show(&gettext("Initializing new backup repository …"));
ui.new_backup().hide();
let mut borg = borg::BorgOnlyRepo::new(repo.clone());
let password = Zeroizing::new(ui.password().get_text().as_bytes().to_vec());
if encrypted {
let password = Zeroizing::new(ui.password().get_text().as_bytes().to_vec());
if ui.password_store().get_active() {
ui::utils::dialog_catch_err(
ui::utils::secret_service_set_password(&config, &password),
gettext("Failed to store password."),
);
}
borg.set_password(password);
borg.set_password(password.clone());
}
page_pending::show(&gettext("Initializing new backup repository …"));
ui.new_backup().hide();
ui::utils::async_react(
"borg::init",
move || borg.init(),
enclose!((config, ui) move |result| {
if ui::utils::dialog_catch_err(result, gettext("Failed to initialize repository")) {
enclose!((repo, ui, password) move |result: Result<borg::List, _>| match result {
Err(err) => {
ui::utils::show_error(&gettext("Failed to initialize repository"), &err);
page_pending::back();
ui.new_backup().show();
return;
}
Ok(info) => {
let config = shared::BackupConfig::new(repo.clone(), info, encrypted);
insert_backup_config(config.clone());
insert_backup_config(config.clone());
if encrypted && ui.password_store().get_active() {
ui::utils::dialog_catch_err(
ui::utils::secret_service_set_password(&config, &password),
gettext("Failed to store password."),
);
}
ui::page_detail::view_backup_conf(&config.id);