#![deny(deprecated)]
#![deny(clippy::panic)]
#![warn(rust_2018_idioms)]
#![warn(clippy::cargo)]
#![warn(clippy::decimal_literal_representation)]
#![warn(clippy::if_not_else)]
#![warn(clippy::large_digit_groups)]
#![warn(clippy::missing_docs_in_private_items)]
#![warn(clippy::missing_errors_doc)]
#![warn(clippy::needless_continue)]
#![allow(clippy::multiple_crate_versions)]
mod api;
mod env;
mod config;
mod login;
mod macros;
mod sync;
use clap::Arg;
use crate::env::Env;
use crate::config::Configuration;
use crate::api::GoogleError;
pub type Result<T> = std::result::Result<T, (Error, u32, &'static str)>;
#[derive(Debug)]
pub enum Error {
GoogleError(GoogleError),
DatabaseError(rusqlite::Error),
RequestError(reqwest::Error),
Other(String)
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() {
let matches = clap::App::new("gsync")
.version(VERSION)
.author("Tobias de Bruijn <t.debruijn@array21.dev>")
.about("Sync folders and files to Google Drive while respecting gitignore files")
.subcommand(clap::SubCommand::with_name("config")
.about("Configure GSync. Not all options have to be supplied, if you don't want to overwrite them. If this is the first time you're running the config command, you must provide all options.")
.arg(Arg::with_name("client-id")
.short("i")
.long("id")
.value_name("CLIENT_ID")
.help("The Client ID provided by Google")
.takes_value(true)
.required(false))
.arg(Arg::with_name("client-secret")
.short("s")
.long("secret")
.value_name("CLIENT_SECRET")
.help("The Client Secret provided by Google")
.takes_value(true)
.required(false))
.arg(Arg::with_name("files")
.short("f")
.long("files")
.value_name("FILES")
.help("The files you want to sync, comma seperated String")
.takes_value(true)
.required(false))
.arg(Arg::with_name("drive_id")
.short("d")
.long("drive")
.value_name("ID")
.help("The ID of the Team Drive to use, if you are not using a Team Drive leave this empty.")
.takes_value(true)
.required(false)))
.subcommand(clap::SubCommand::with_name("show")
.about("Show the current GSync configuration"))
.subcommand(clap::SubCommand::with_name("login")
.about("Login to Google"))
.subcommand(clap::SubCommand::with_name("sync")
.about("Start syncing the configured folders to Google Drive"))
.subcommand(clap::SubCommand::with_name("drives")
.about("Get a list of all shared drives and their IDs."))
.get_matches();
let empty_env = Env::empty();
{
let conn = empty_env.get_conn().expect("Failed to create database connection. ");
conn.execute("CREATE TABLE IF NOT EXISTS user (id TEXT PRIMARY KEY, refresh_token TEXT, access_token TEXT, expiry INTEGER)", rusqlite::named_params! {}).expect("Failed to create table 'users'");
conn.execute("CREATE TABLE IF NOT EXISTS config (client_id TEXT, client_secret TEXT, input_files TEXT, drive_id TEXT)", rusqlite::named_params! {}).expect("Failed to create table 'config'");
}
if let Some(matches) = matches.subcommand_matches("config") {
let new_config = Configuration {
client_id: option_str_string(matches.value_of("client-id")),
client_secret: option_str_string(matches.value_of("client-secret")),
input_files: option_str_string(matches.value_of("files")),
drive_id: option_str_string(matches.value_of("drive_id"))
};
let current_config = handle_err!(Configuration::get_config(&empty_env));
let config = Configuration::merge(new_config, current_config);
match config.is_complete() {
(true, _) => {},
(false, str) => {
eprintln!("Error: Configuration is incomplete; {}", str);
std::process::exit(1);
}
}
handle_err!(config.write(&empty_env));
println!("Configuration updated!");
std::process::exit(0);
}
if matches.subcommand_matches("show").is_some() {
let config = handle_err!(Configuration::get_config(&empty_env));
if config.is_empty() {
println!("GSync is unconfigured. Run 'gsync config -h` for more information on how to configure GSync'");
std::process::exit(0);
}
println!("Current GSync configuration:");
println!("Client ID: {}", option_unwrap_text(config.client_id));
println!("Client Secret: {}", option_unwrap_text(config.client_secret));
println!("Input Files: {}", option_unwrap_text(config.input_files));
println!("Drive ID: {}", option_unwrap_text(config.drive_id));
std::process::exit(0);
}
if matches.subcommand_matches("login").is_some() {
let config = handle_err!(Configuration::get_config(&empty_env));
if config.is_empty() {
println!("GSync is unconfigured. Run 'gsync config -h` for more information on how to configure GSync'");
std::process::exit(0);
}
match config.is_complete() {
(true, _) => {},
(false, str) => {
eprintln!("Error: Configuration is incomplete; {}", str);
std::process::exit(1);
}
}
let env = Env::new(config.client_id.as_ref().unwrap(), config.client_secret.as_ref().unwrap(), config.drive_id.as_ref(), String::new());
let login_data = handle_err!(crate::login::perform_oauth2_login(&env));
println!("Info: Inserting tokens into database.");
handle_err!(crate::login::db::save_to_database(&login_data, &env));
println!("Info: Login successful!");
std::process::exit(0);
}
if matches.subcommand_matches("sync").is_some() {
let config = handle_err!(Configuration::get_config(&empty_env));
if config.is_empty() {
println!("GSync is unconfigured. Run 'gsync config -h` for more information on how to configure GSync'");
std::process::exit(0);
}
match config.is_complete() {
(true, _) => {},
(false, str) => {
eprintln!("Error: Configuration is incomplete; {}", str);
std::process::exit(1);
}
}
if !handle_err!(is_logged_in(&empty_env)) {
eprintln!("Error: GSync isn't logged in with Google. Have you run `gsync login` yet?");
std::process::exit(1);
}
let mut env = Env::new(config.client_id.as_ref().unwrap(), config.client_secret.as_ref().unwrap(), config.drive_id.as_ref(), String::new());
println!("Info: Querying Drive for root folder");
let list = handle_err!(crate::api::drive::list_files(&env, Some("name = 'GSync' and mimeType = 'application/vnd.google-apps.folder' and trashed = false"), config.drive_id.as_deref()));
let root_folder_id = if list.is_empty() {
println!("Info: Root folder doesn't exist. Creating one now.");
match &env.drive_id {
Some(drive_id) => handle_err!(crate::api::drive::create_folder(&env, "GSync", drive_id)),
None => handle_err!(crate::api::drive::create_folder(&env, "GSync", "root"))
}
} else {
println!("Info: Root folder exists.");
list.get(0).unwrap().id.clone()
};
env.root_folder = root_folder_id;
handle_err!(crate::sync::sync(&config, &env));
std::process::exit(0);
}
if matches.subcommand_matches("drives").is_some() {
let config = handle_err!(Configuration::get_config(&empty_env));
if config.is_empty() {
println!("GSync is unconfigured. Run 'gsync config -h` for more information on how to configure GSync'");
std::process::exit(0);
}
match config.is_complete() {
(true, _) => {},
(false, str) => {
eprintln!("Error: Configuration is incomplete; {}", str);
std::process::exit(1);
}
}
if !handle_err!(is_logged_in(&empty_env)) {
eprintln!("Error: GSync isn't logged in with Google. Have you run `gsync login` yet?");
std::process::exit(1);
}
let env = Env::new(config.client_id.as_ref().unwrap(), config.client_secret.as_ref().unwrap(), config.drive_id.as_ref(), String::new());
let shared_drives = handle_err!(crate::api::drive::get_shared_drives(&env));
for drive in shared_drives {
println!("Shared drive '{}' with identifier '{}'", &drive.name, &drive.id);
}
std::process::exit(0);
}
println!("No command specified. Run 'gsync -h' for available commands.");
}
fn option_str_string(i: Option<&str>) -> Option<String> {
i.map(|i| i.to_string())
}
fn option_unwrap_text(i: Option<String>) -> String {
match i {
Some(i) => i,
None => "None".to_string()
}
}
fn is_logged_in(env: &Env) -> Result<bool> {
let conn = unwrap_db_err!(env.get_conn());
let mut stmt = unwrap_db_err!(conn.prepare("SELECT * FROM user"));
let mut result = unwrap_db_err!(stmt.query(rusqlite::named_params! {}));
let mut is_logged_in = false;
while let Ok(Some(_)) = result.next() {
is_logged_in = true;
}
Ok(is_logged_in)
}