use crate::login::ManagedOnedrive;
use anyhow::{anyhow, Context as _, Result};
use clap::{Args, Parser};
use fuser::MountOption;
use onedrive_api::{Auth, ClientCredential, Permission, Tenant, TokenResponse};
use std::path::PathBuf;
use url::Url;
mod config;
mod fuse_fs;
mod login;
mod paths;
mod vfs;
#[tokio::main]
async fn main() -> Result<()> {
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
default_hook(info);
std::process::exit(101);
}));
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let opt: Opt = Opt::parse();
match opt {
Opt::Login(opt) => main_login(opt).await,
Opt::Mount(opt) => main_mount(opt).await,
}
}
const REDIRECT_URI: &str = "http://localhost:0/onedrive-fuse-login";
const HTTP_SERVER_PATH: &str = "/onedrive-fuse-login";
async fn main_login(opt: OptLogin) -> Result<()> {
let credential_path = opt
.credential
.or_else(paths::default_credential_path)
.context("No credential file provided to save to")?;
let perm = if opt.read_write {
"READONLY"
} else {
"READWRITE"
};
eprintln!("You are logining for {perm} permission.");
let perm = Permission::new_read()
.write(opt.read_write)
.offline_access(true);
let tokens = if let Some(code) = &opt.code {
eprintln!("Logining...");
let auth = Auth::new(
opt.client_id.clone(),
perm,
REDIRECT_URI.to_owned(),
Tenant::Consumers,
);
auth.login_with_code(code, &ClientCredential::None).await?
} else {
let client_id = opt.client_id.clone();
tokio::task::spawn_blocking(move || login_with_http_server(client_id, perm)).await??
};
let refresh_token = tokens.refresh_token.expect("Missing refresh token");
eprintln!("Login successfully, saving credential...");
login::Credential {
readonly: !opt.read_write,
client_id: opt.client_id,
redirect_uri: REDIRECT_URI.to_owned(),
refresh_token,
}
.save(&credential_path)
.context("Cannot save credential file")?;
Ok(())
}
fn login_with_http_server(client_id: String, perm: Permission) -> Result<TokenResponse> {
use reqwest::StatusCode;
use std::io::Cursor;
use tiny_http::{Header, Response, Server};
let server = Server::http("localhost:0")
.map_err(|err| anyhow!("Failed to listen on localhost: {err}"))?;
let listen_addr = server.server_addr().to_ip().expect("Listen on IP address");
eprintln!("Listening at {listen_addr} for callback");
let redirect_uri = format!(
"http://localhost:{}{}",
listen_addr.port(),
HTTP_SERVER_PATH
);
let auth = Auth::new(client_id, perm, redirect_uri, Tenant::Consumers);
let auth_url = auth.code_auth_url();
let _ = open::that(auth_url.as_str());
eprintln!(
"\
Please login to your OneDrive (Microsoft) Account in browser.
Your browser should be opened with the login page. If not, please manually open the link below:
{auth_url}
"
);
let base_url = Url::parse("http://localhost/").unwrap();
loop {
let req = server.recv()?;
if !req.url().starts_with(HTTP_SERVER_PATH) {
let _ = req.respond(Response::empty(StatusCode::NOT_FOUND.as_u16()));
continue;
}
let ret = (|| -> Result<_> {
let url = base_url.join(req.url()).context("Invalid URL")?;
let code = url
.query_pairs()
.find_map(|(key, value)| (key == "code" && !value.is_empty()).then_some(value))
.context("Missing code")?;
eprintln!("Logining...");
let tokens = tokio::runtime::Handle::current()
.block_on(auth.login_with_code(&code, &ClientCredential::None))?;
Ok(tokens)
})();
let headers =
vec![Header::from_bytes("content-type", "text/plain; charset=utf-8").unwrap()];
let (status, ret_str) = match &ret {
Ok(_) => (
StatusCode::OK,
"Login successfully. You can close this page now.".to_owned(),
),
Err(err) => {
println!("{err:#}");
(
StatusCode::BAD_REQUEST,
format!("Login failed. Please close this page and try again.\n{err:#}"),
)
}
};
let _ = req.respond(Response::new(
status.as_u16().into(),
headers,
Cursor::new(ret_str),
None,
None,
));
if let Ok(ret) = ret {
return Ok(ret);
}
}
}
async fn main_mount(opt: OptMount) -> Result<()> {
let credential_path = opt
.credential
.or_else(paths::default_credential_path)
.context("No credential file provided")?;
let config = config::Config::merge_from_default(opt.config.as_deref(), &opt.option)?;
let readonly = config.permission.readonly;
let client = reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.gzip(true)
.https_only(true)
.connect_timeout(config.net.connect_timeout)
.timeout(config.net.request_timeout)
.build()?;
let unlimit_client = reqwest::ClientBuilder::new()
.https_only(true)
.connect_timeout(config.net.connect_timeout)
.build()?;
let onedrive =
ManagedOnedrive::login(client, credential_path, config.relogin, readonly).await?;
let vfs = vfs::Vfs::new(
fuser::FUSE_ROOT_ID,
readonly,
config.vfs,
onedrive.clone(),
unlimit_client,
)
.await
.context("Failed to initialize vfs")?;
log::info!("Mounting...");
let fuse_options = [
MountOption::FSName("onedrive".into()),
MountOption::DefaultPermissions, MountOption::NoDev,
MountOption::NoSuid,
MountOption::NoAtime,
if config.permission.executable {
MountOption::Exec
} else {
MountOption::NoExec
},
if readonly {
MountOption::RO
} else {
MountOption::RW
},
];
let fs = fuse_fs::Filesystem::new(vfs, config.permission);
tokio::task::spawn_blocking(move || fuser::mount2(fs, &opt.mount_point, &fuse_options))
.await??;
Ok(())
}
#[derive(Debug, Parser)]
#[command(about = "Mount OneDrive storage as FUSE filesystem.")]
#[command(after_help = concat!("\
Copyright (C) 2019-2023, Oxalica
This is free software; see the source for copying conditions. There is NO warranty;
not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
"))]
enum Opt {
Login(OptLogin),
Mount(OptMount),
}
#[derive(Debug, Args)]
#[command(after_help = "\
EXAMPLES:
# Login with some client id.
onedrive-fuse --client-id 00000000-0000-0000-0000-000000000000
# And save credential to a custom path.
onedrive-fuse -c /path/to/credential --client-id 00000000-0000-0000-0000-000000000000
")]
struct OptLogin {
#[arg(short, long)]
credential: Option<PathBuf>,
#[arg(long)]
client_id: String,
#[arg(short = 'w', long)]
read_write: bool,
#[arg(long)]
no_listen: bool,
code: Option<String>,
}
#[derive(Debug, Args)]
#[command(after_help = "\
EXAMPLES:
# Using default credential file to mount OneDrive on `~/mnt`.
onedrive-fuse mount ~/mnt
# Use custom credential file.
onedrive-fuse mount -c /path/to/credential ~/mnt
# Modify some default settings.
onedrive-fuse mount -o permission.umask=0o077 -o relogin.enable=false ~/mnt
")]
struct OptMount {
#[arg(short, long)]
credential: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
mount_point: PathBuf,
#[arg(short, long)]
option: Vec<String>,
}