vflight 0.9.2

Share files over the Veilid distributed network with content-addressable storage
Documentation
#![recursion_limit = "512"]

use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use vflight::{fetch, metrics, seed, throttle};

#[derive(Parser)]
#[command(name = "vflight")]
#[command(about = "Share files over the Veilid network")]
struct Cli {
    /// Print performance metrics summary after command completes
    #[arg(long, global = true)]
    metrics: bool,

    /// Use insecure local fallback storage (for WSL/testing only)
    #[arg(long, global = true)]
    insecure_local_fallback: bool,

    /// Encrypt chunks with a password (recommended for sensitive files). Falls back to VFLIGHT_PASSWORD env var.
    #[arg(long, short = 'p', global = true)]
    password: Option<String>,

    /// Number of parallel chunk downloads (default: 8)
    #[arg(long, default_value = "8", global = true)]
    parallel: usize,

    /// Disable resumable downloads (start fresh)
    #[arg(long, global = true)]
    no_resume: bool,

    /// Trust cached chunks without re-verification (faster resume)
    #[arg(long, global = true)]
    trust_cache: bool,

    /// Limit transfer speed in KB/s for both seed and fetch (0 = unlimited)
    #[arg(long, default_value = "0", global = true)]
    throttle: u64,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Seed a file to the network
    Seed {
        /// Path to the file to seed
        file: PathBuf,
        /// Compress the file with zstd before chunking
        #[arg(long)]
        compress: bool,
    },
    /// Fetch a file from the network
    Fetch {
        /// DHT key of the file to fetch
        dht_key: String,
        /// Output directory (defaults to current directory)
        #[arg(default_value = ".")]
        output_dir: PathBuf,
    },
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize tracing with info as default level
    // Can be overridden via RUST_LOG environment variable
    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));

    tracing_subscriber::fmt().with_env_filter(env_filter).init();

    let cli = Cli::parse();
    let throttler = throttle::Throttler::new(cli.throttle).map(std::sync::Arc::new);

    // CLI flag takes precedence; fall back to VFLIGHT_PASSWORD env var
    let password = cli
        .password
        .or_else(|| std::env::var("VFLIGHT_PASSWORD").ok());

    let result = match cli.command {
        Commands::Seed { file, compress } => {
            if !file.exists() {
                anyhow::bail!("File not found: {}", file.display());
            }
            seed::seed_file(
                &file,
                cli.insecure_local_fallback,
                password.as_deref(),
                compress,
                throttler.clone(),
            )
            .await
        }
        Commands::Fetch {
            dht_key,
            output_dir,
        } => {
            fetch::fetch_file(
                &dht_key,
                &output_dir,
                cli.insecure_local_fallback,
                password.as_deref(),
                cli.parallel,
                cli.no_resume,
                cli.trust_cache,
                throttler.clone(),
            )
            .await
        }
    };

    // Print metrics summary if enabled (even on error)
    if cli.metrics {
        metrics::global_metrics().print_summary();
    }

    result
}