extern crate clap;
extern crate config;
extern crate ctrlc;
extern crate failure;
extern crate fuser;
extern crate gcsf;
#[macro_use]
extern crate log;
extern crate pretty_env_logger;
extern crate serde;
extern crate serde_json;
extern crate xdg;
use clap::{Parser, Subcommand};
use failure::{Error, err_msg};
use std::fs;
use std::io::prelude::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time;
use gcsf::{Config, DriveFacade, Gcsf, NullFs};
const DEBUG_LOG: &str = "hyper::client=error,hyper::http=error,hyper::net=error,debug";
fn print_auth_troubleshooting(session_name: &str) {
eprintln!();
eprintln!("This usually means your refresh token has expired.");
eprintln!();
eprintln!("Common causes:");
eprintln!(" 1. Google Cloud project is in 'Testing' mode (tokens expire after 7 days)");
eprintln!(" 2. You switched to 'Production' mode but haven't re-authenticated");
eprintln!(" 3. You revoked access in your Google Account settings");
eprintln!();
eprintln!("To fix:");
eprintln!(" 1. Ensure your Google Cloud project is in 'Production' mode:");
eprintln!(" - Go to https://console.cloud.google.com");
eprintln!(" - Navigate to 'APIs & Services' -> 'OAuth consent screen'");
eprintln!(" - If status is 'Testing', click 'Publish App'");
eprintln!(" 2. Re-authenticate:");
eprintln!(" gcsf logout {}", session_name);
eprintln!(" gcsf login {}", session_name);
}
const INFO_LOG: &str =
"hyper::client=error,hyper::http=error,hyper::net=error,fuse::session=error,info";
#[derive(Parser)]
#[command(name = "GCSF")]
#[command(version = "0.3.7")]
#[command(author = "Sergiu Puscas <srg.pscs@gmail.com>")]
#[command(about = "File system based on Google Drive")]
#[command(
after_help = "Note: this is a work in progress. It might cause data loss. Use with caution."
)]
#[command(subcommand_required = true)]
#[command(arg_required_else_help = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Mount {
#[arg(short = 's', long = "session", value_name = "session_name")]
session_name: String,
#[arg(value_name = "mount_directory")]
mountpoint: String,
#[arg(long = "read-only", short = 'r')]
read_only: bool,
},
Login {
#[arg(value_name = "session_name")]
session_name: String,
},
Logout {
#[arg(value_name = "session_name")]
session_name: String,
},
List,
Verify {
#[arg(value_name = "session_name")]
session_name: String,
},
}
const DEFAULT_CONFIG: &str = r#"
### This is the configuration file that GCSF uses.
### It should be placed in $XDG_CONFIG_HOME/gcsf/gcsf.toml, which is usually
### defined as $HOME/.config/gcsf/gcsf.toml
# Show additional logging info?
debug = false
# Perform a mount check and fail early if it fails. Disable this if you
# encounter this error:
#
# fuse: attempt to remount on active mount point: [...]
# Could not mount to [...]: Undefined error: 0 (os error 0)
mount_check = true
# How long to cache the contents of a file after it has been accessed.
cache_max_seconds = 300
# How how many files to cache.
cache_max_items = 10
# How long to cache the size and capacity of the file system. These are the
# values reported by `df`.
cache_statfs_seconds = 60
# How many seconds to wait before checking for remote changes and updating them
# locally.
sync_interval = 60
# Mount options
mount_options = [
"fsname=GCSF",
# Allow file system access to root. This only works if `user_allow_other`
# is set in /etc/fuse.conf
"allow_root",
]
# If set to true, Google Drive will provide a code after logging in and
# authorizing GCSF. This code must be copied and pasted into GCSF in order to
# complete the process. Useful for running GCSF on a remote server.
#
# If set to false, Google Drive will attempt to communicate with GCSF directly.
# This is usually faster and more convenient.
authorize_using_code = false
# Port for OAuth redirect during authentication. Change this if port 8081
# is already in use by another application.
auth_port = 8081
# If set to true, all files with identical name will get an increasing number
# attached to the suffix. This is most likely not necessary.
rename_identical_files = false
# If set to true, will add an extension to special files (docs, presentations, sheets, drawings, sites), e.g. "\#.ods" for spreadsheets.
add_extensions_to_special_files = false
# If set to true, deleted files and folder will not be moved to Trash Folder,
# instead they get deleted permanently.
skip_trash = false
# If set to true, the filesystem will be mounted in read-only mode.
# All write operations (create, delete, rename, write) will be rejected.
# This is useful for:
# - Preventing accidental modifications
# - Safely browsing Drive contents
# - Backup/archival scenarios
read_only = false
# The Google OAuth client secret for Google Drive APIs. Create your own
# credentials at https://console.developers.google.com and paste them here
client_secret = """
{
"installed": {
"client_id": "892276709198-2ksebnrqkhihtf5p743k4ce5bk0n7p5a.apps.googleusercontent.com",
"project_id": "gcsf-v02",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "1ImxorJzh-PuH2CxrcLPnJMU",
"redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
}
}"""
"#;
fn mount_gcsf(config: Config, mountpoint: &str) {
let mut options = vec![
fuser::MountOption::FSName(String::from("GCSF")),
fuser::MountOption::AllowRoot,
];
if config.read_only() {
options.push(fuser::MountOption::RO);
info!("Mounting in read-only mode");
}
if config.mount_check() {
match fuser::spawn_mount2(NullFs {}, mountpoint, &options) {
Ok(session) => {
debug!("Test mount of NullFs successful. Will mount GCSF next.");
drop(session);
}
Err(e) => {
error!("Could not mount to {}: {}", mountpoint, e);
return;
}
};
}
info!("Creating and populating file system...");
let fs: Gcsf = match Gcsf::with_config(config) {
Ok(fs) => fs,
Err(e) => {
error!("{}", e);
return;
}
};
info!("File system created.");
info!("Mounting to {}", &mountpoint);
match fuser::spawn_mount2(fs, mountpoint, &options) {
Ok(_session) => {
info!("Mounted to {}", &mountpoint);
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
info!("Ctrl-C detected");
r.store(false, Ordering::SeqCst);
})
.expect("Error setting Ctrl-C handler");
while running.load(Ordering::SeqCst) {
thread::sleep(time::Duration::from_millis(50));
}
}
Err(e) => error!("Could not mount to {}: {}", &mountpoint, e),
};
}
fn login(config: &mut Config) -> Result<(), Error> {
debug!("{:#?}", &config);
if config.token_file().exists() {
return Err(err_msg(format!(
"token file {:?} already exists.",
config.token_file()
)));
}
let mut df = DriveFacade::new(config);
let _result = df.root_id()?;
Ok(())
}
fn load_conf() -> Result<Config, Error> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("gcsf");
let config_file = xdg_dirs
.place_config_file("gcsf.toml")
.map_err(|_| err_msg("Cannot create configuration directory"))?;
info!("Config file: {:?}", &config_file);
if !config_file.exists() {
let mut config_file = fs::File::create(config_file.clone())
.map_err(|_| err_msg("Could not create config file"))?;
config_file.write_all(DEFAULT_CONFIG.as_bytes())?;
}
let settings = config::ConfigBuilder::<config::builder::DefaultState>::default()
.add_source(config::File::with_name(config_file.to_str().unwrap()))
.build()
.unwrap();
let mut config: gcsf::Config = settings.try_deserialize()?;
config.config_dir = xdg_dirs.get_config_home();
Ok(config)
}
fn main() {
let mut config = load_conf().expect("Could not load configuration file.");
pretty_env_logger::formatted_builder()
.parse_filters(if config.debug() { DEBUG_LOG } else { INFO_LOG })
.init();
let cli = Cli::parse();
match cli.command {
Commands::Login { session_name } => {
config.session_name = Some(session_name);
if config.token_file().exists() {
error!("Token file {:?} already exists.", config.token_file());
return;
}
let result = if config.authorize_using_code() {
let secret: serde_json::Value =
serde_json::from_str(config.client_secret()).expect("Invalid client_secret");
let installed = &secret["installed"];
gcsf::auth::headless_login(
installed["client_id"].as_str().expect("Missing client_id"),
installed["client_secret"]
.as_str()
.expect("Missing client_secret"),
&config.token_file(),
config.auth_port(),
)
} else {
login(&mut config)
};
match result {
Ok(_) => {
println!(
"Successfully logged in. Saved credentials to {:?}",
&config.token_file()
);
}
Err(e) => {
error!("Could not log in: {}", e);
}
};
}
Commands::Logout { session_name } => {
config.session_name = Some(session_name);
let tf = config.token_file();
match fs::remove_file(&tf) {
Ok(_) => {
println!("Successfully removed {:?}", &tf);
}
Err(e) => {
println!("Could not remove {:?}: {}", &tf, e);
}
};
}
Commands::List => {
let exception = String::from("gcsf.toml");
let mut sessions: Vec<_> = fs::read_dir(config.config_dir())
.unwrap()
.map(Result::unwrap)
.map(|f| f.file_name().to_str().unwrap().to_string())
.filter(|name| name != &exception)
.collect();
sessions.sort();
if sessions.is_empty() {
println!("No sessions found.");
} else {
println!("Sessions:");
for session in sessions {
println!("\t- {}", &session);
}
}
}
Commands::Verify { session_name } => {
config.session_name = Some(session_name.clone());
if !config.token_file().exists() {
error!("Token file {:?} does not exist.", config.token_file());
error!("Run `gcsf login {}` first.", session_name);
std::process::exit(1);
}
if config.client_secret.is_none() {
error!("No Google OAuth client secret was provided.");
std::process::exit(1);
}
println!("Verifying authentication for session '{}'...", session_name);
let mut df = DriveFacade::new(&config);
match df.validate_auth() {
Ok(_) => {
println!("Authentication is valid.");
println!("Token file: {:?}", config.token_file());
}
Err(e) => {
error!("Authentication failed: {}", e);
print_auth_troubleshooting(&session_name);
std::process::exit(1);
}
}
}
Commands::Mount {
session_name,
mountpoint,
read_only,
} => {
config.session_name = Some(session_name.clone());
if read_only {
config.read_only = Some(true);
}
if !config.token_file().exists() {
error!("Token file {:?} does not exist.", config.token_file());
error!("Try logging in first using `gcsf login`.");
return;
}
if config.client_secret.is_none() {
error!("No Google OAuth client secret was provided.");
error!(
"Try deleting your config file to force GCSF to generate it with the default credentials."
);
error!(
"Alternatively, you can create your own credentials or manually set the default ones from https://github.com/harababurel/gcsf/blob/master/sample_config.toml"
);
return;
}
info!("Validating authentication...");
let mut df = DriveFacade::new(&config);
match df.validate_auth() {
Ok(_) => {
info!("Authentication valid.");
}
Err(e) => {
error!("Authentication failed: {}", e);
print_auth_troubleshooting(&session_name);
return;
}
}
drop(df);
mount_gcsf(config, &mountpoint);
}
}
}