aven 0.1.13

Local-first task manager CLI and sync server
Documentation
use std::fs;
use std::path::Path;

use anyhow::{Context, Result, bail};
use aven_core::data_safety::{AvenExport, IntegrityReport};
use aven_core::db::Database;

use crate::cli::{BackupCommand, BackupRestoreArgs, ExportArgs, ImportArgs};
use crate::config::{self as app_config, AppConfig};
use crate::db;
use crate::ids::now;
use crate::render::quote;

pub(crate) async fn cmd_backup(
    database: &Database,
    config: &AppConfig,
    db_path: &Path,
    args: BackupCommand,
) -> Result<()> {
    let output = match args.output {
        Some(path) => path,
        None => db::default_backup_path(db_path, "manual")?,
    };
    let blob_dir = app_config::resolve_blob_dir(db_path, config)?;
    database
        .create_backup_archive(db_path, &blob_dir, &output)
        .await?;
    let bytes = fs::metadata(&output)
        .with_context(|| format!("could not stat {}", output.display()))?
        .len();
    println!(
        "backup path={} bytes={bytes}",
        quote(&output.display().to_string())
    );
    Ok(())
}

pub(crate) async fn cmd_backup_restore(
    config: &AppConfig,
    db_path: &Path,
    args: BackupRestoreArgs,
) -> Result<()> {
    if !args.yes {
        bail!(
            "error backup-restore-requires-confirmation hint=\"pass --yes to replace local data\""
        );
    }
    let safety = if aven_core::data_safety::is_backup_archive(&args.path)? {
        let blob_dir = app_config::resolve_blob_dir(db_path, config)?;
        aven_core::data_safety::restore_backup_archive(db_path, &blob_dir, &args.path).await?
    } else {
        db::restore_database_file(db_path, &args.path).await?
    };
    println!(
        "restored-backup path={} safety_backup={}",
        quote(&args.path.display().to_string()),
        quote(&safety.display().to_string())
    );
    Ok(())
}

pub(crate) async fn cmd_export(database: &Database, args: ExportArgs) -> Result<()> {
    let export = database.export_data(now()).await?;
    if let Some(parent) = args.output.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("could not create {}", parent.display()))?;
    }
    let text = serde_json::to_string(&export).context("could not serialize export")?;
    fs::write(&args.output, text)
        .with_context(|| format!("could not write export file {}", args.output.display()))?;
    let bytes = fs::metadata(&args.output)
        .with_context(|| format!("could not stat {}", args.output.display()))?
        .len();
    println!(
        "exported path={} workspaces={} tasks={} bytes={bytes}",
        quote(&args.output.display().to_string()),
        export.tables.workspaces.len(),
        export.tables.tasks.len()
    );
    Ok(())
}

pub(crate) async fn cmd_import(
    database: &Database,
    db_path: &Path,
    args: ImportArgs,
) -> Result<()> {
    if !args.yes {
        bail!("error import-requires-confirmation hint=\"pass --yes to replace local data\"");
    }
    let text = fs::read_to_string(&args.path)
        .with_context(|| format!("could not read {}", args.path.display()))?;
    let export: AvenExport = serde_json::from_str(&text)
        .with_context(|| format!("could not parse {}", args.path.display()))?;
    database.validate_import_data(&export).await?;
    if export.blobs_included {
        bail!("error import-blobs-included-unsupported");
    }
    let safety = db::default_sqlite_backup_path(db_path, "before-import")?;
    db::backup_database(db_path, &safety)?;
    database.import_data(&export).await?;
    println!(
        "imported path={} safety_backup={} workspaces={} tasks={}",
        quote(&args.path.display().to_string()),
        quote(&safety.display().to_string()),
        export.tables.workspaces.len(),
        export.tables.tasks.len()
    );
    Ok(())
}

pub(crate) async fn database_integrity_report(database: &Database) -> Result<IntegrityReport> {
    database.database_integrity_report().await
}

pub(crate) async fn attachment_integrity_checks(
    database: &Database,
    blob_dir: &Path,
    deep: bool,
) -> Result<Vec<aven_core::data_safety::IntegrityCheck>> {
    database.attachment_integrity_checks(blob_dir, deep).await
}

pub(crate) fn ensure_integrity_ok(report: &IntegrityReport) -> Result<()> {
    let mut bad = vec![];
    if !report.quick_check_ok {
        bad.push("quick check");
    }
    for check in &report.checks {
        if !check.ok {
            bad.push(check.label);
        }
    }
    if bad.is_empty() {
        return Ok(());
    }
    bail!("error data-integrity-failed checks={}", bad.join(", "))
}