AkurAI Build
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))
}