megalib 0.11.1

Rust client library for Mega.nz cloud storage
Documentation
use clap::Parser;
use indicatif::{ProgressBar, ProgressStyle};
use megalib::SessionHandle;
use megalib::api::ApiErrorCode;
use megalib::error::{MegaError, Result};
use megalib::progress::TransferProgress;
use std::io::{self, Write};
use tracing_subscriber::{EnvFilter, fmt};

#[derive(Debug, Parser)]
#[command(name = "sequence")]
struct Args {
    #[arg(short = 'e', long)]
    email: String,
    #[arg(short = 'p', long)]
    password: String,
    #[arg(long)]
    proxy: Option<String>,
    folder1: Option<String>,
    folder2: Option<String>,
    local1: Option<String>,
    local2: Option<String>,
}

fn init_tracing() {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("off"));
    fmt().with_env_filter(filter).with_target(false).init();
}

#[tokio::main]
async fn main() -> Result<()> {
    init_tracing();
    let args = Args::parse();

    let folder1 = args.folder1.unwrap_or_else(|| "/Root/lol1".to_string());
    let folder2 = args.folder2.unwrap_or_else(|| "/Root/lol2".to_string());
    let local1 = args.local1.unwrap_or_else(|| "./Cargo.toml".to_string());
    let local2 = args.local2.unwrap_or_else(|| "./Cargo.lock".to_string());

    println!("Logging in...");
    let session = if let Some(proxy) = &args.proxy {
        SessionHandle::login_with_proxy(&args.email, &args.password, proxy).await?
    } else {
        SessionHandle::login(&args.email, &args.password).await?
    };
    let info = session.account_info().await?;
    println!("Logged in as: {}", info.email);

    let style = ProgressStyle::with_template(
        "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta}) {msg}",
    )
    .unwrap_or_else(|_| ProgressStyle::default_bar())
    .progress_chars("=>-");
    let mut current_file = String::new();
    let mut current_bar: Option<ProgressBar> = None;
    session
        .watch_status(Box::new(move |progress: &TransferProgress| {
            if current_bar.is_none() || progress.filename != current_file {
                if let Some(previous_bar) = current_bar.take() {
                    previous_bar.finish_and_clear();
                }

                current_file = progress.filename.clone();
                let bar_len = progress.total.max(1);
                let bar = ProgressBar::new(bar_len);
                bar.set_style(style.clone());
                bar.set_message(current_file.clone());
                current_bar = Some(bar);
            }

            if let Some(bar) = current_bar.as_ref().cloned() {
                if progress.total > 0 {
                    bar.set_length(progress.total);
                    bar.set_position(progress.done.min(progress.total));
                } else {
                    bar.set_length(progress.done.max(1));
                    bar.set_position(progress.done);
                }

                if progress.is_complete() {
                    bar.finish_with_message(format!("{} complete", progress.filename));
                    current_bar = None;
                    current_file.clear();
                }
            }

            true
        }))
        .await?;

    println!("Refreshing filesystem...");
    session.refresh().await?;

    ensure_folder(&session, &folder1).await?;
    upload_and_export(&session, &local1, &folder1).await?;
    wait_for_enter("Verify exported link, then press Enter to upload Cargo.lock")?;

    upload_and_export(&session, &local2, &folder1).await?;
    wait_for_enter("Check both files in folder. Press Enter to continue")?;

    ensure_folder(&session, &folder2).await?;
    upload_and_export(&session, &local2, &folder2).await?;
    wait_for_enter("Verify exported link, then press Enter to upload Cargo.toml")?;

    upload_and_export(&session, &local1, &folder2).await?;
    println!("Please verify both files are now in the folder.");

    println!("Sequence complete.");
    Ok(())
}

async fn ensure_folder(session: &SessionHandle, path: &str) -> Result<()> {
    match session.mkdir_by_path(path).await {
        Ok(_) => {
            println!("Created folder: {}", path);
        }
        Err(MegaError::ApiError { code, .. }) if code == ApiErrorCode::Exist as i32 => {
            println!("Folder already exists: {}", path);
        }
        Err(e) => return Err(e),
    }
    Ok(())
}

async fn upload_and_export(
    session: &SessionHandle,
    local: &str,
    remote_folder: &str,
) -> Result<()> {
    println!("Uploading {local} to {remote_folder} ...");
    let remote_path = format!("{}/", remote_folder.trim_end_matches('/'));
    let node = session.upload_by_path(local, &remote_path).await?;
    println!("Uploaded as {} ({} bytes)", node.name, node.size);

    println!("Exporting {} ...", remote_folder);
    let link = session.export_by_path(remote_folder).await?;
    println!("Public link: {link}");
    Ok(())
}

fn wait_for_enter(prompt: &str) -> Result<()> {
    print!("{prompt}: ");
    io::stdout().flush().ok();
    let mut line = String::new();
    io::stdin()
        .read_line(&mut line)
        .map_err(|e| MegaError::Custom(format!("stdin error: {e}")))?;
    Ok(())
}