use std::fmt;
use std::path::PathBuf;
use clap::Args;
use uuid::Uuid;
use crate::cli::op::Op;
use crate::cli::ui;
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 {
pub bucket: String,
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,
"{}",
ui::success(
"Cloned",
&format!("bucket {} to {}", self.name, self.directory.display())
)
)?;
write!(f, "{}", ui::label("files", &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?;
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,
})
}
}