shard-core 2.2.1

Core library for shard distributed VCS: chunking, compression, commits, branching, merging, WAL
Documentation
use crate::branch;
use crate::index::Index;
use crate::metadata::{self, MetadataFormat};
use crate::store::Store;
use anyhow::Result;
use axum::{
    extract::State,
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::info;

#[derive(Clone)]
struct AppState {
    pub repo_path: PathBuf,
    pub shard_dir: PathBuf,
}

#[derive(Deserialize)]
struct InitRequest {
    backend: Option<String>,
    compression: Option<String>,
    chunker: Option<String>,
    chunk_size: Option<u64>,
    is_private: Option<bool>,
    passphrase: Option<String>,
}

#[derive(Deserialize)]
struct AddRequest {
    path: String,
}

#[derive(Deserialize)]
struct CommitRequest {
    message: String,
    author: Option<String>,
}

#[derive(Deserialize)]
struct PullRequest {
    peer: String,
    commit_id: String,
}

#[derive(Deserialize)]
struct PushRequest {
    peer: String,
}

#[derive(Deserialize)]
struct BranchCreateRequest {
    name: String,
    commit_id: Option<String>,
}

fn err_json(e: impl ToString) -> (StatusCode, Json<serde_json::Value>) {
    (
        StatusCode::BAD_REQUEST,
        Json(serde_json::json!({"status": "error", "error": e.to_string()})),
    )
}

fn ok_json(v: serde_json::Value) -> (StatusCode, Json<serde_json::Value>) {
    (StatusCode::OK, Json(v))
}

pub async fn serve(path: &Path, addr: &str, json: bool) -> Result<()> {
    let shard_dir = path.join(".shard");
    let state = AppState {
        repo_path: path.to_path_buf(),
        shard_dir,
    };

    let app = Router::new()
        .route("/health", get(health_handler))
        .route("/api/v1/init", post(init_handler))
        .route("/api/v1/add", post(add_handler))
        .route("/api/v1/commit", post(commit_handler))
        .route("/api/v1/log", get(log_handler))
        .route("/api/v1/status", get(status_handler))
        .route("/api/v1/pull", post(pull_handler))
        .route("/api/v1/push", post(push_handler))
        .route("/api/v1/branch", get(branch_list_handler))
        .route("/api/v1/branch/create", post(branch_create_handler))
        .layer(tower_http::cors::CorsLayer::permissive())
        .with_state(Arc::new(Mutex::new(state)));

    let listener = tokio::net::TcpListener::bind(addr).await?;
    if json {
        info!(
            "{}",
            serde_json::json!({"event": "api_start", "addr": addr})
        );
    } else {
        info!("API server listening on {}", addr);
    }
    axum::serve(listener, app).await?;
    Ok(())
}

async fn health_handler() -> Json<serde_json::Value> {
    Json(serde_json::json!({"status": "ok"}))
}

async fn init_handler(
    State(state): State<Arc<Mutex<AppState>>>,
    Json(req): Json<InitRequest>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    let pass = req.passphrase.as_deref().unwrap_or("");
    match crate::init_with_passphrase(
        &s.repo_path,
        req.backend.as_deref().unwrap_or("flat"),
        req.compression.as_deref().unwrap_or("zstd"),
        req.chunker.as_deref().unwrap_or("fixed"),
        req.chunk_size,
        req.is_private.unwrap_or(false),
        false,
        pass,
    ) {
        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "initialized"})),
        Err(e) => err_json(e),
    }
}

async fn add_handler(
    State(state): State<Arc<Mutex<AppState>>>,
    Json(req): Json<AddRequest>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    match crate::add(&s.repo_path, &PathBuf::from(&req.path), false) {
        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "added"})),
        Err(e) => err_json(e),
    }
}

async fn commit_handler(
    State(state): State<Arc<Mutex<AppState>>>,
    Json(req): Json<CommitRequest>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    let author = req.author.as_deref().unwrap_or("User <user@example.com>");
    match crate::commit(&s.repo_path, &req.message, author, false) {
        Ok(commit_id) => ok_json(serde_json::json!({"status": "ok", "commit_id": commit_id})),
        Err(e) => err_json(e),
    }
}

async fn log_handler(
    State(state): State<Arc<Mutex<AppState>>>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    let shard_dir = &s.shard_dir;
    if !shard_dir.exists() {
        return err_json("not a shard repository");
    }
    let store = match Store::open(shard_dir) {
        Ok(s) => s,
        Err(e) => return err_json(e),
    };
    let head = branch::resolve_head(shard_dir).ok().and_then(|(_, h)| h);

    let mut entries: Vec<serde_json::Value> = Vec::new();
    let mut stack = head.clone().into_iter().collect::<Vec<_>>();
    let mut seen = std::collections::HashSet::new();
    while let Some(cid) = stack.pop() {
        if !seen.insert(cid.clone()) {
            continue;
        }
        if let Ok(data) = store.get_chunk(&cid) {
            if let Ok(commit) = metadata::deserialize::<crate::commit::Commit>(&data) {
                entries.push(serde_json::json!({
                    "commit_id": cid,
                    "message": commit.message,
                    "author": commit.author,
                    "timestamp": commit.timestamp.to_string(),
                }));
                for p in &commit.parents {
                    stack.push(p.clone());
                }
            }
        }
    }
    ok_json(serde_json::json!({"status": "ok", "entries": entries}))
}

async fn status_handler(
    State(state): State<Arc<Mutex<AppState>>>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    let shard_dir = &s.shard_dir;
    if !shard_dir.exists() {
        return err_json("not a shard repository");
    }
    let (current_branch, head) = branch::resolve_head(shard_dir).unwrap_or((None, None));
    let index = Index::load(shard_dir, &MetadataFormat::Json).ok();
    let staged: Vec<String> = index
        .as_ref()
        .map(|i| i.files.keys().cloned().collect())
        .unwrap_or_default();
    ok_json(serde_json::json!({
        "status": "ok",
        "current_branch": current_branch,
        "head": head,
        "staged": staged,
    }))
}

async fn pull_handler(
    State(state): State<Arc<Mutex<AppState>>>,
    Json(req): Json<PullRequest>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    match crate::pull(&s.repo_path, &req.peer, &req.commit_id, false).await {
        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "pulled"})),
        Err(e) => err_json(e),
    }
}

async fn push_handler(
    State(state): State<Arc<Mutex<AppState>>>,
    Json(req): Json<PushRequest>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    match crate::push(&s.repo_path, &req.peer, false).await {
        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "pushed"})),
        Err(e) => err_json(e),
    }
}

async fn branch_list_handler(
    State(state): State<Arc<Mutex<AppState>>>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    let shard_dir = &s.shard_dir;
    if !shard_dir.exists() {
        return err_json("not a shard repository");
    }
    let (current, branches) = branch::list_branches(shard_dir).unwrap_or((None, Vec::new()));
    let branch_list: Vec<serde_json::Value> = branches
        .into_iter()
        .map(|(name, tip)| serde_json::json!({"name": name, "tip": tip}))
        .collect();
    ok_json(serde_json::json!({"status": "ok", "current": current, "branches": branch_list}))
}

async fn branch_create_handler(
    State(state): State<Arc<Mutex<AppState>>>,
    Json(req): Json<BranchCreateRequest>,
) -> (StatusCode, Json<serde_json::Value>) {
    let s = state.lock().await;
    match crate::branch_create(&s.repo_path, &req.name, req.commit_id.as_deref()) {
        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "branch_created"})),
        Err(e) => err_json(e),
    }
}