aven-core 0.1.32

Core library for the Aven local-first task manager
Documentation
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};

use crate::db::{self, Database};

mod archive;
mod export_types;
mod import;
mod integrity;
mod scan;
mod tables;
mod validation;

pub use export_types::{
    AvenExport, BlobInventoryExportRow, ChangeRow, ConflictRow, ExportTables, FieldVersionRow,
    LabelRow, MetaRow, MetadataFieldIdAliasRow, MetadataFieldRow, NoteRow, ProjectIdAliasRow,
    ProjectPathRow, ProjectRow, RecurrenceOccurrenceRow, RecurrencePauseIntervalRow,
    RecurrenceSeriesLabelRow, RecurrenceSeriesMetadataRow, RecurrenceSeriesRow, TaskAttachmentRow,
    TaskDependencyRow, TaskEpicLinkRow, TaskLabelRow, TaskMetadataRow, TaskRelatedLinkRow, TaskRow,
    WorkspaceRow,
};
use export_types::{EXPORT_FORMAT, EXPORT_VERSION};

#[derive(Debug, Clone)]
pub struct IntegrityReport {
    pub quick_check_ok: bool,
    pub quick_check_value: String,
    pub checks: Vec<IntegrityCheck>,
}

#[derive(Debug, Clone)]
pub struct IntegrityCheck {
    pub label: &'static str,
    pub ok: bool,
    pub value: String,
}

pub(crate) fn ensure_integrity_ok(report: &IntegrityReport) -> Result<()> {
    integrity::ensure_ok(report)
}

impl Database {
    pub async fn export_data(&self, exported_at: String) -> Result<AvenExport> {
        let mut conn = self.acquire_writer().await?;
        let mut tx = db::begin_immediate(&mut conn).await?;
        let schema_version = db::current_schema_version(&mut tx).await?;
        let tables = ExportTables {
            workspaces: scan::scan_workspaces(&mut tx).await?,
            projects: scan::scan_projects(&mut tx).await?,
            project_paths: scan::scan_project_paths(&mut tx).await?,
            project_id_aliases: scan::scan_project_id_aliases(&mut tx).await?,
            labels: scan::scan_labels(&mut tx).await?,
            metadata_fields: scan::scan_metadata_fields(&mut tx).await?,
            metadata_field_id_aliases: scan::scan_metadata_field_id_aliases(&mut tx).await?,
            tasks: scan::scan_tasks(&mut tx).await?,
            task_metadata: scan::scan_task_metadata(&mut tx).await?,
            task_labels: scan::scan_task_labels(&mut tx).await?,
            notes: scan::scan_notes(&mut tx).await?,
            task_dependencies: scan::scan_task_dependencies(&mut tx).await?,
            task_epic_links: scan::scan_task_epic_links(&mut tx).await?,
            task_related_links: scan::scan_task_related_links(&mut tx).await?,
            task_attachments: scan::scan_task_attachments(&mut tx).await?,
            blob_inventory: scan::scan_blob_inventory(&mut tx).await?,
            recurrence_series: scan::scan_recurrence_series(&mut tx).await?,
            recurrence_series_labels: scan::scan_recurrence_series_labels(&mut tx).await?,
            recurrence_series_metadata: scan::scan_recurrence_series_metadata(&mut tx).await?,
            recurrence_occurrences: scan::scan_recurrence_occurrences(&mut tx).await?,
            recurrence_pause_intervals: scan::scan_recurrence_pause_intervals(&mut tx).await?,
            changes: scan::scan_changes(&mut tx).await?,
            field_versions: scan::scan_field_versions(&mut tx).await?,
            conflicts: scan::scan_conflicts(&mut tx).await?,
            meta: scan::scan_meta(&mut tx).await?,
        };
        let version = if tables.task_related_links.is_empty() {
            2
        } else {
            EXPORT_VERSION
        };
        tx.commit().await?;
        Ok(AvenExport {
            format: EXPORT_FORMAT.to_string(),
            version,
            exported_at,
            schema_version,
            blobs_included: false,
            tables,
        })
    }

    pub async fn validate_import_data(&self, export: &AvenExport) -> Result<()> {
        let mut conn = self.acquire_reader().await?;
        validation::ensure_supported_export(&mut conn, export).await?;
        validation::validate_export_snapshot(export)
    }

    pub async fn import_data(&self, export: &AvenExport) -> Result<IntegrityReport> {
        let mut conn = self.acquire_writer().await?;
        validation::ensure_supported_export(&mut conn, export).await?;
        validation::validate_export_snapshot(export)?;
        let target_client_id = db::get_meta(&mut conn, "client_id")
            .await?
            .context("missing target client_id")?;
        let mut tx = db::begin_immediate(&mut conn).await?;
        import::replace_from_export(&mut tx, export, &target_client_id).await?;
        let report = integrity::database_report(&mut tx).await?;
        ensure_integrity_ok(&report)?;
        tx.commit().await?;
        Ok(report)
    }

    pub async fn database_integrity_report(&self) -> Result<IntegrityReport> {
        let mut conn = self.acquire_reader().await?;
        integrity::database_report(&mut conn).await
    }

    pub async fn attachment_integrity_checks(
        &self,
        blob_dir: &Path,
        deep: bool,
    ) -> Result<Vec<IntegrityCheck>> {
        let mut conn = self.acquire_reader().await?;
        integrity::attachment_integrity_checks(&mut conn, blob_dir, deep).await
    }

    pub async fn create_backup_archive(&self, blob_dir: &Path, output: &Path) -> Result<()> {
        let mut conn = self.acquire_writer().await?;
        let hashes: Vec<String> = sqlx::query_scalar(
            "SELECT sha256 FROM blob_inventory WHERE available = 1 ORDER BY sha256",
        )
        .fetch_all(&mut *conn)
        .await?;
        let mut leases = Vec::with_capacity(hashes.len());
        for hash in hashes {
            match crate::attachments::lifecycle::acquire_lease(
                &mut conn,
                &hash,
                "backup",
                &crate::attachments::lifecycle::SystemClock,
            )
            .await
            {
                Ok(lease) => leases.push(lease),
                Err(error) => {
                    for lease in leases {
                        let _ =
                            crate::attachments::lifecycle::release_lease(&mut conn, &lease).await;
                    }
                    return Err(error);
                }
            }
        }
        let backup_result = archive::create_backup_archive(&mut conn, blob_dir, output).await;
        for lease in leases {
            crate::attachments::lifecycle::release_lease(&mut conn, &lease).await?;
        }
        backup_result
    }
}

pub fn is_backup_archive(path: &Path) -> Result<bool> {
    archive::is_archive_path(path)
}

pub async fn restore_backup_archive(
    db_path: &Path,
    blob_dir: &Path,
    source: &Path,
) -> Result<PathBuf> {
    archive::restore_backup_archive(db_path, blob_dir, source).await
}