Menu
Public repository / master
AkurAI-Drive / client/src/main.rs
Latest change 2d36cd6c72fc6a63a7edb87bb54a4020e50f2f8a - desktop: Tauri v2 tray + Google-Drive-style file browser app by olibuijr
//! AkurAI Drive sync client — a native Linux Dropbox/Nextcloud-style desktop
//! client that keeps a local folder in sync with AkurAI Drive S3 storage.
//!
//! It talks only to the AkurAI Drive backend (`AKURAI_DRIVE_API`, default
//! `https://akurai-drive.olibuijr.com`) and to S3 via the presigned URLs the
//! backend returns. No AWS credentials ever touch this client.
mod daemon;
// The reusable sync logic now lives in the `akurai_drive_sync` library crate.
// Re-export its modules at this binary crate's root so the binary's own modules
// (e.g. `daemon`) and the command handlers below keep referring to them through
// `crate::…` paths exactly as before — no call sites change.
pub use akurai_drive_sync::{api, auth, config, state, sync, util};
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use crate::api::Api;
use crate::config::{state_db_path, Config};
#[derive(Parser)]
#[command(
name = "akurai-drive-sync",
version,
about = "Sync a local folder with your AkurAI Drive storage"
)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Log in via the browser and save a device token.
Login,
/// List the buckets available to your account.
Buckets,
/// Configure a sync folder + bucket and do an initial sync.
Init {
/// Local folder to sync (created if missing).
folder: String,
/// Bucket name to sync against.
#[arg(long)]
bucket: String,
},
/// Do one bidirectional sync between the folder and the bucket.
Sync,
/// Run continuously: watch the folder and poll the remote.
Daemon,
/// Show configuration and the last sync summary.
Status,
}
fn main() {
if let Err(e) = run() {
eprintln!("error: {e:#}");
std::process::exit(1);
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
match cli.cmd {
Cmd::Login => cmd_login(),
Cmd::Buckets => cmd_buckets(),
Cmd::Init { folder, bucket } => cmd_init(&folder, &bucket),
Cmd::Sync => {
sync::run_sync(&Config::load()?, false)?;
Ok(())
}
Cmd::Daemon => daemon::run(&Config::load()?),
Cmd::Status => cmd_status(),
}
}
fn cmd_login() -> Result<()> {
let mut cfg = Config::load()?;
let api_base = cfg.resolved_api();
let token = auth::login(&api_base)?;
cfg.api = Some(api_base);
cfg.token = Some(token);
cfg.save()?;
println!(
"Logged in. Token saved to {}",
config::config_path()?.display()
);
Ok(())
}
fn cmd_buckets() -> Result<()> {
let cfg = Config::load()?;
let api = authed_api(&cfg)?;
let buckets = api.buckets()?;
if buckets.is_empty() {
println!("No buckets yet. Create one in the AkurAI Drive web app.");
return Ok(());
}
println!("Buckets:");
for b in buckets {
println!(
" {} [region={} plan={} org={} created={}]",
b.bucket, b.region, b.plan, b.org, b.created
);
}
Ok(())
}
fn cmd_init(folder: &str, bucket: &str) -> Result<()> {
let mut cfg = Config::load()?;
if !cfg.is_logged_in() {
bail!("not logged in — run `akurai-drive-sync login` first");
}
let path = expand_path(folder)?;
std::fs::create_dir_all(&path).with_context(|| format!("creating {}", path.display()))?;
let canonical =
std::fs::canonicalize(&path).with_context(|| format!("resolving {}", path.display()))?;
cfg.folder = Some(canonical.to_string_lossy().to_string());
cfg.bucket = Some(bucket.to_string());
cfg.api = Some(cfg.resolved_api());
cfg.save()?;
println!("Configured: {} ⇄ bucket {}", canonical.display(), bucket);
println!("Running initial sync...");
sync::run_sync(&cfg, false)?;
Ok(())
}
fn cmd_status() -> Result<()> {
let cfg = Config::load()?;
println!("AkurAI Drive sync — status");
println!(" config : {}", config::config_path()?.display());
println!(" api : {}", cfg.resolved_api());
println!(
" logged in : {}",
if cfg.is_logged_in() { "yes" } else { "no" }
);
println!(
" folder : {}",
cfg.folder.as_deref().unwrap_or("(not set)")
);
println!(
" bucket : {}",
cfg.bucket.as_deref().unwrap_or("(not set)")
);
if let Ok(state) = state::State::open(&state_db_path()?) {
let last = state
.meta_get("last_sync")?
.unwrap_or_else(|| "(never)".into());
let at = state
.meta_get("last_sync_at")?
.unwrap_or_else(|| "-".into());
println!(" last sync : {at}");
println!(" : {last}");
}
Ok(())
}
/// Build an authenticated API client, erroring clearly if not logged in.
fn authed_api(cfg: &Config) -> Result<Api> {
let token = cfg
.token
.clone()
.filter(|t| !t.is_empty())
.context("not logged in — run `akurai-drive-sync login`")?;
Api::new(cfg.resolved_api(), token)
}
/// Expand a leading `~/` to the user's home directory.
fn expand_path(input: &str) -> Result<PathBuf> {
if let Some(rest) = input.strip_prefix("~/") {
let home = dirs::home_dir().context("cannot determine home directory")?;
return Ok(home.join(rest));
}
if input == "~" {
return dirs::home_dir().context("cannot determine home directory");
}
Ok(PathBuf::from(input))
}