datman 0.1.0

A chunked and deduplicated backup system using Yama
Documentation
/*
This file is part of Yama.

Yama is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Yama is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Yama.  If not, see <https://www.gnu.org/licenses/>.
*/


use std::path::{Path, PathBuf};

use clap::Clap;
use env_logger::Env;

use anyhow::bail;
use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
use datman::commands::backup::{backup_all_sources_to_destination, backup_source_to_destination};
use datman::commands::ilabel::interactive_labelling_session;
use datman::commands::init_descriptor;
use datman::descriptor::load_descriptor;
use std::str::FromStr;

#[derive(Clap)]
pub enum DatmanCommand {
    /// Initialise a datman descriptor in this directory.
    Init {},

    ///
    Status {},

    #[clap(name = "ilabel")]
    InteractiveLabelling {
        /// Name of the source to label.
        source_name: String,
    },

    #[clap(name = "ibrowse")]
    InteractiveBrowsing {
        /// Name of the source to browse.
        source_name: String,
    },

    BackupOne {
        /// Name of the source to back up.
        source_name: String,

        /// Name of the destination to back up to.
        destination_name: String,
    },

    BackupAll {
        /// Name of the destination to back up to.
        destination_name: String,
    },

    Extract {
        /// Name of the 'source' to extract
        /// Omit for 'all'.
        #[clap(short)]
        source_name: Option<String>,

        /// If specified, will get the first backup after this date.
        #[clap(long)]
        after: Option<HumanDateTime>,

        /// If specified, will get the last backup before this date. The default behaviour is to get the latest.
        #[clap(long)]
        before: Option<HumanDateTime>,

        /// If not specified, time-restricted extractions that don't have a pointer for every source
        /// will instead lead to an error.
        #[clap(long)]
        accept_partial: bool, // TODO unimplemented.

        /// Name of the pile to extract from
        pile_name: String,

        /// Place to extract to.
        destination: PathBuf,

        /// Skip applying metadata. Might be needed to extract without superuser privileges.
        #[clap(long)]
        skip_metadata: bool,
    },
}

pub struct HumanDateTime(pub DateTime<Local>);

impl FromStr for HumanDateTime {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Ok(date_only) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
            let local_date = chrono::offset::Local.from_local_date(&date_only).unwrap();
            let local_datetime = local_date.and_hms(0, 0, 0);
            Ok(HumanDateTime(local_datetime))
        } else if let Ok(date_and_time) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
            let local_datetime = chrono::offset::Local
                .from_local_datetime(&date_and_time)
                .unwrap();
            Ok(HumanDateTime(local_datetime))
        } else if let Ok(date_and_time) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
            let local_datetime = chrono::offset::Local
                .from_local_datetime(&date_and_time)
                .unwrap();
            Ok(HumanDateTime(local_datetime))
        } else {
            bail!("Couldn't parse using either format. Use one of: 2021-05-16 OR 2021-05-16T17:42:14 OR 2021-05-16 17:42:14");
        }
    }
}

fn main() -> anyhow::Result<()> {
    env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();

    let opts: DatmanCommand = DatmanCommand::parse();

    match opts {
        DatmanCommand::Init {} => {
            init_descriptor(Path::new(".")).unwrap();
        }
        DatmanCommand::Status { .. } => {
            unimplemented!();
        }
        DatmanCommand::InteractiveLabelling { source_name } => {
            interactive_labelling_session(Path::new("."), source_name).unwrap();
        }
        DatmanCommand::InteractiveBrowsing { source_name } => {
            datman::commands::ibrowse::session(Path::new("."), source_name).unwrap();
        }
        DatmanCommand::BackupOne {
            source_name,
            destination_name,
        } => {
            let descriptor = load_descriptor(Path::new(".")).unwrap();
            let source = &descriptor.source[&source_name];
            let destination = &descriptor.piles[&destination_name];
            backup_source_to_destination(
                source,
                destination,
                &descriptor,
                Path::new("."),
                &source_name,
                &destination_name,
                yama::utils::get_number_of_workers("YAMA_CHUNKERS"),
            )
            .unwrap();
        }
        DatmanCommand::BackupAll { destination_name } => {
            let descriptor = load_descriptor(Path::new(".")).unwrap();
            let destination = &descriptor.piles[&destination_name];
            backup_all_sources_to_destination(
                destination,
                &descriptor,
                Path::new("."),
                &destination_name,
                yama::utils::get_number_of_workers("YAMA_CHUNKERS"),
            )
            .unwrap();
        }
        DatmanCommand::Extract {
            source_name,
            after,
            before,
            accept_partial,
            pile_name,
            destination,
            skip_metadata,
        } => {
            if !accept_partial {
                bail!("Specify --accept-partial until running without it is supported.");
            }

            if after.is_some() && before.is_some() {
                bail!("Can't specify both before and after!");
            }

            let before = before.map(|dt| dt.0.with_timezone(&Utc));
            let after = after.map(|dt| dt.0.with_timezone(&Utc));

            datman::commands::extract::extract(
                &destination,
                Path::new("."),
                source_name.as_ref().map(|x| x.as_ref()),
                &pile_name,
                before.into(),
                after.into(),
                !skip_metadata,
                !skip_metadata,
                !skip_metadata,
                yama::utils::get_number_of_workers("YAMA_EXTRACTORS"),
            )?;
        }
    }
    Ok(())
}