jax-daemon 0.1.11

End-to-end encrypted storage buckets with peer-to-peer synchronization
Documentation
use std::fmt;
use std::path::PathBuf;

use clap::Args;
use owo_colors::OwoColorize;
use uuid::Uuid;

use crate::cli::op::Op;
use jax_daemon::http_server::api::client::{resolve_bucket, ApiError};

use super::clone_state::{CloneConfig, CloneStateError, CloneStateManager, PathHashMap};

#[derive(Args, Debug, Clone)]
pub struct Clone {
    /// Bucket name or UUID
    pub bucket: String,

    /// Directory to clone into (will be created if it doesn't exist)
    pub directory: PathBuf,
}

#[derive(Debug)]
pub struct CloneOutput {
    pub name: String,
    pub bucket_id: Uuid,
    pub directory: PathBuf,
    pub files_exported: usize,
}

impl fmt::Display for CloneOutput {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(
            f,
            "{} bucket {} to {}",
            "Cloned".green().bold(),
            self.name.bold(),
            self.directory.display().to_string().bold()
        )?;
        write!(f, "  {} {}", "files:".dimmed(), self.files_exported)
    }
}

#[derive(Debug, thiserror::Error)]
pub enum CloneError {
    #[error("API error: {0}")]
    Api(#[from] ApiError),
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Clone state error: {0}")]
    CloneState(#[from] CloneStateError),
    #[error("Directory already exists and is not empty: {0}")]
    DirectoryNotEmpty(PathBuf),
    #[error("Directory already initialized as a clone")]
    AlreadyCloned,
    #[error("HTTP error: {0}")]
    Reqwest(#[from] reqwest::Error),
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),
    #[error("URL parse error: {0}")]
    UrlParse(#[from] url::ParseError),
}

#[async_trait::async_trait]
impl Op for Clone {
    type Error = CloneError;
    type Output = CloneOutput;

    async fn execute(&self, ctx: &crate::cli::op::OpContext) -> Result<Self::Output, Self::Error> {
        let mut client = ctx.client.clone();
        let bucket_id = resolve_bucket(&mut client, &self.bucket).await?;

        // Check if directory exists and is empty
        if self.directory.exists() {
            let state_manager = CloneStateManager::new(self.directory.clone());
            if state_manager.is_initialized() {
                return Err(CloneError::AlreadyCloned);
            }

            let entries: Vec<_> = std::fs::read_dir(&self.directory)?
                .filter_map(|e| e.ok())
                .filter(|e| e.file_name() != ".jax")
                .collect();

            if !entries.is_empty() {
                return Err(CloneError::DirectoryNotEmpty(self.directory.clone()));
            }
        } else {
            std::fs::create_dir_all(&self.directory)?;
        }

        #[derive(serde::Serialize)]
        struct ExportRequest {
            bucket_id: Uuid,
            target_dir: PathBuf,
        }

        let export_request = ExportRequest {
            bucket_id,
            target_dir: self.directory.clone(),
        };

        #[derive(serde::Deserialize)]
        struct ExportResponse {
            bucket_name: String,
            link: common::linked_data::Link,
            height: u64,
            files_exported: usize,
            hash_map: PathHashMap,
        }

        let export_result: ExportResponse = client
            .http_client()
            .post(client.base_url().join("/api/v0/bucket/export").unwrap())
            .json(&export_request)
            .send()
            .await?
            .error_for_status()?
            .json()
            .await?;

        let state_manager = CloneStateManager::new(self.directory.clone());
        let config = CloneConfig {
            bucket_id,
            bucket_name: export_result.bucket_name.clone(),
            last_synced_link: export_result.link,
            last_synced_height: export_result.height,
        };

        state_manager.init(config)?;
        state_manager.write_hash_map(&export_result.hash_map)?;

        Ok(CloneOutput {
            name: export_result.bucket_name,
            bucket_id,
            directory: self.directory.clone(),
            files_exported: export_result.files_exported,
        })
    }
}