diff --git a/Cargo.lock b/Cargo.lock index 70aed6f..0c55253 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,54 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -543,6 +591,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "core-foundation" version = "0.9.3" @@ -802,6 +896,12 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.3" @@ -1450,6 +1550,7 @@ dependencies = [ "async-trait", "aws-config", "aws-sdk-s3", + "clap", "dotenvy", "rusqlite", "tempfile", @@ -1458,6 +1559,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -1713,6 +1820,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 8919d1f..751c052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ anyhow = "1.0.75" async-trait = "0.1.73" aws-config = "0.56.1" aws-sdk-s3 = "0.31.2" +clap = { version = "4.4.5", features = ["derive"] } dotenvy = "0.15.7" rusqlite = { version = "0.29.0", features = ["backup"] } tempfile = "3.8.0" diff --git a/src/argument.rs b/src/argument.rs index e80bc83..e2a86a8 100644 --- a/src/argument.rs +++ b/src/argument.rs @@ -1,20 +1,14 @@ -use anyhow::{bail, Result}; - -use crate::errors::SqliteBackupError; +use clap::Parser; +/// Easily to backup your SQLite database +#[derive(Parser)] +#[command(author, version, about, long_about = None)] pub struct Argument { - pub source_path: String, -} + /// The project name + #[arg(long, default_value = "default")] + pub project_name: String, -impl Argument { - pub fn build(args: &[String]) -> Result { - if let Some(source_path) = args.get(1) { - let argument = Self { - source_path: source_path.clone(), - }; - Ok(argument) - } else { - bail!(SqliteBackupError::NoSourceFileError); - } - } + /// The path to the database to backup + #[arg(long)] + pub db: String, } diff --git a/src/backup.rs b/src/backup.rs index d90d3ab..747e1e7 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -15,23 +15,14 @@ pub trait Backup { pub struct SqliteSourceFile<'a> { pub path: &'a Path, pub filename: &'a str, - pub db_name: &'a str, - pub db_extension: &'a str, } impl<'a> SqliteSourceFile<'a> { pub fn from(src_path: &'a str) -> Result { let path = Path::new(src_path); let filename = convert_os_str_result_to_str(path.file_name())?; - let db_name = convert_os_str_result_to_str(path.file_stem())?; - let db_extension = convert_os_str_result_to_str(path.extension())?; - - Ok(Self { - path, - filename, - db_name, - db_extension, - }) + + Ok(Self { path, filename }) } } diff --git a/src/config.rs b/src/config.rs index 9cf6d74..33e60ac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,8 +3,8 @@ use std::{env, fmt::Display}; use anyhow::Result; -#[derive(PartialEq)] -enum AppEnv { +#[derive(PartialEq, Debug)] +pub enum AppEnv { Prod, Dev, Test, @@ -22,6 +22,7 @@ impl Display for AppEnv { #[derive(Debug)] pub struct Config { + pub app_env: AppEnv, pub bucket_name: String, pub account_id: String, pub access_key_id: String, @@ -47,6 +48,7 @@ impl Config { } let config = Self { + app_env, bucket_name: env::var("BUCKET_NAME")?, account_id: env::var("ACCOUNT_ID")?, access_key_id: env::var("ACCESS_KEY_ID")?, diff --git a/src/main.rs b/src/main.rs index da24a44..a1e7cd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,18 @@ use anyhow::{Context, Result}; +use clap::Parser; use rusqlite::Connection; use sqlite_backup::{ - argument, + argument::{self, Argument}, backup::{Backup, SqliteBackup, SqliteSourceFile}, config::Config, uploader::{R2Uploader, Uploader}, }; -use std::env; #[tokio::main] async fn main() -> Result<()> { let cfg = Config::load().context("load env vars")?; - let args = env::args().collect::>(); - match argument::Argument::build(&args) { - Ok(arg) => run(&arg, &cfg).await?, - - Err(err) => eprintln!("Application Error: {}", err), - } + let args = Argument::parse(); + run(&args, &cfg).await?; println!("Done"); @@ -28,7 +24,7 @@ async fn run(arg: &argument::Argument, cfg: &Config) -> Result<()> { let tmp_dir = tempfile::tempdir()?; // backup data - let src_file = SqliteSourceFile::from(arg.source_path.as_str()).context("parse source path")?; + let src_file = SqliteSourceFile::from(arg.db.as_str()).context("parse source path")?; let src_conn = Connection::open(src_file.path).context("create source connection")?; let dest = tmp_dir.path().join(src_file.filename); SqliteBackup::new(src_conn, dest.display().to_string(), |p| { @@ -41,14 +37,8 @@ async fn run(arg: &argument::Argument, cfg: &Config) -> Result<()> { .context("backup source to destination")?; // upload - let uploader = R2Uploader::new(cfg).await; - uploader - .upload_object( - dest, - format!("sqlite__{}", src_file.db_name).as_str(), - src_file.db_extension, - ) - .await?; + let uploader = R2Uploader::new(arg, cfg).await; + uploader.upload_object(dest, src_file.filename).await?; // close temp dir tmp_dir.close()?; diff --git a/src/uploader.rs b/src/uploader.rs index acef087..11e8703 100644 --- a/src/uploader.rs +++ b/src/uploader.rs @@ -9,20 +9,25 @@ use aws_sdk_s3::{ }; use time::format_description; -use crate::config::{self}; +use crate::{ + argument, + config::{self}, +}; #[async_trait] pub trait Uploader { - async fn upload_object(&self, path: PathBuf, db_name: &str, extension: &str) -> Result<()>; + async fn upload_object(&self, src_path: PathBuf, dest_name: &str) -> Result<()>; } pub struct R2Uploader { client: Client, bucket: String, + project_name: String, + app_env: String, } impl R2Uploader { - pub async fn new(cfg: &config::Config) -> Self { + pub async fn new(arg: &argument::Argument, cfg: &config::Config) -> Self { let endpoint = format!( "https://{}.r2.cloudflarestorage.com", cfg.account_id.clone() @@ -46,20 +51,25 @@ impl R2Uploader { Self { client, bucket: cfg.bucket_name.clone(), + project_name: arg.project_name.clone(), + app_env: cfg.app_env.to_string(), } } } #[async_trait] impl Uploader for R2Uploader { - async fn upload_object(&self, path: PathBuf, db_name: &str, extension: &str) -> Result<()> { - let body = ByteStream::from_path(path) + async fn upload_object(&self, src_path: PathBuf, dest_name: &str) -> Result<()> { + let body = ByteStream::from_path(src_path) .await .context("create file stream")?; let key = uuid::Uuid::new_v4(); let format = format_description::parse("[year]-[month]-[day]")?; let today = time::OffsetDateTime::now_utc().format(&format)?; - let object_key = format!("{db_name}/{today}__{key}.{extension}"); + let object_key = format!( + "{}/{}/{}/{today}__{key}", + self.app_env, self.project_name, dest_name, + ); self.client .put_object()