Skip to main content

shard_core/
api.rs

1use crate::branch;
2use crate::index::Index;
3use crate::metadata::{self, MetadataFormat};
4use crate::store::Store;
5use anyhow::Result;
6use axum::{
7    extract::State,
8    http::StatusCode,
9    routing::{get, post},
10    Json, Router,
11};
12use serde::Deserialize;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use tokio::sync::Mutex;
16use tracing::info;
17
18#[derive(Clone)]
19struct AppState {
20    pub repo_path: PathBuf,
21    pub shard_dir: PathBuf,
22}
23
24#[derive(Deserialize)]
25struct InitRequest {
26    backend: Option<String>,
27    compression: Option<String>,
28    chunker: Option<String>,
29    chunk_size: Option<u64>,
30    is_private: Option<bool>,
31    passphrase: Option<String>,
32}
33
34#[derive(Deserialize)]
35struct AddRequest {
36    path: String,
37}
38
39#[derive(Deserialize)]
40struct CommitRequest {
41    message: String,
42    author: Option<String>,
43}
44
45#[derive(Deserialize)]
46struct PullRequest {
47    peer: String,
48    commit_id: String,
49}
50
51#[derive(Deserialize)]
52struct PushRequest {
53    peer: String,
54}
55
56#[derive(Deserialize)]
57struct BranchCreateRequest {
58    name: String,
59    commit_id: Option<String>,
60}
61
62fn err_json(e: impl ToString) -> (StatusCode, Json<serde_json::Value>) {
63    (
64        StatusCode::BAD_REQUEST,
65        Json(serde_json::json!({"status": "error", "error": e.to_string()})),
66    )
67}
68
69fn ok_json(v: serde_json::Value) -> (StatusCode, Json<serde_json::Value>) {
70    (StatusCode::OK, Json(v))
71}
72
73pub async fn serve(path: &Path, addr: &str, json: bool) -> Result<()> {
74    let shard_dir = path.join(".shard");
75    let state = AppState {
76        repo_path: path.to_path_buf(),
77        shard_dir,
78    };
79
80    let app = Router::new()
81        .route("/health", get(health_handler))
82        .route("/api/v1/init", post(init_handler))
83        .route("/api/v1/add", post(add_handler))
84        .route("/api/v1/commit", post(commit_handler))
85        .route("/api/v1/log", get(log_handler))
86        .route("/api/v1/status", get(status_handler))
87        .route("/api/v1/pull", post(pull_handler))
88        .route("/api/v1/push", post(push_handler))
89        .route("/api/v1/branch", get(branch_list_handler))
90        .route("/api/v1/branch/create", post(branch_create_handler))
91        .layer(tower_http::cors::CorsLayer::permissive())
92        .with_state(Arc::new(Mutex::new(state)));
93
94    let listener = tokio::net::TcpListener::bind(addr).await?;
95    if json {
96        info!(
97            "{}",
98            serde_json::json!({"event": "api_start", "addr": addr})
99        );
100    } else {
101        info!("API server listening on {}", addr);
102    }
103    axum::serve(listener, app).await?;
104    Ok(())
105}
106
107async fn health_handler() -> Json<serde_json::Value> {
108    Json(serde_json::json!({"status": "ok"}))
109}
110
111async fn init_handler(
112    State(state): State<Arc<Mutex<AppState>>>,
113    Json(req): Json<InitRequest>,
114) -> (StatusCode, Json<serde_json::Value>) {
115    let s = state.lock().await;
116    let pass = req.passphrase.as_deref().unwrap_or("");
117    match crate::init_with_passphrase(
118        &s.repo_path,
119        req.backend.as_deref().unwrap_or("flat"),
120        req.compression.as_deref().unwrap_or("zstd"),
121        req.chunker.as_deref().unwrap_or("fixed"),
122        req.chunk_size,
123        req.is_private.unwrap_or(false),
124        false,
125        pass,
126    ) {
127        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "initialized"})),
128        Err(e) => err_json(e),
129    }
130}
131
132async fn add_handler(
133    State(state): State<Arc<Mutex<AppState>>>,
134    Json(req): Json<AddRequest>,
135) -> (StatusCode, Json<serde_json::Value>) {
136    let s = state.lock().await;
137    match crate::add(&s.repo_path, &PathBuf::from(&req.path), false) {
138        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "added"})),
139        Err(e) => err_json(e),
140    }
141}
142
143async fn commit_handler(
144    State(state): State<Arc<Mutex<AppState>>>,
145    Json(req): Json<CommitRequest>,
146) -> (StatusCode, Json<serde_json::Value>) {
147    let s = state.lock().await;
148    let author = req.author.as_deref().unwrap_or("User <user@example.com>");
149    match crate::commit(&s.repo_path, &req.message, author, false) {
150        Ok(commit_id) => ok_json(serde_json::json!({"status": "ok", "commit_id": commit_id})),
151        Err(e) => err_json(e),
152    }
153}
154
155async fn log_handler(
156    State(state): State<Arc<Mutex<AppState>>>,
157) -> (StatusCode, Json<serde_json::Value>) {
158    let s = state.lock().await;
159    let shard_dir = &s.shard_dir;
160    if !shard_dir.exists() {
161        return err_json("not a shard repository");
162    }
163    let store = match Store::open(shard_dir) {
164        Ok(s) => s,
165        Err(e) => return err_json(e),
166    };
167    let head = branch::resolve_head(shard_dir).ok().and_then(|(_, h)| h);
168
169    let mut entries: Vec<serde_json::Value> = Vec::new();
170    let mut stack = head.clone().into_iter().collect::<Vec<_>>();
171    let mut seen = std::collections::HashSet::new();
172    while let Some(cid) = stack.pop() {
173        if !seen.insert(cid.clone()) {
174            continue;
175        }
176        if let Ok(data) = store.get_chunk(&cid) {
177            if let Ok(commit) = metadata::deserialize::<crate::commit::Commit>(&data) {
178                entries.push(serde_json::json!({
179                    "commit_id": cid,
180                    "message": commit.message,
181                    "author": commit.author,
182                    "timestamp": commit.timestamp.to_string(),
183                }));
184                for p in &commit.parents {
185                    stack.push(p.clone());
186                }
187            }
188        }
189    }
190    ok_json(serde_json::json!({"status": "ok", "entries": entries}))
191}
192
193async fn status_handler(
194    State(state): State<Arc<Mutex<AppState>>>,
195) -> (StatusCode, Json<serde_json::Value>) {
196    let s = state.lock().await;
197    let shard_dir = &s.shard_dir;
198    if !shard_dir.exists() {
199        return err_json("not a shard repository");
200    }
201    let (current_branch, head) = branch::resolve_head(shard_dir).unwrap_or((None, None));
202    let index = Index::load(shard_dir, &MetadataFormat::Json).ok();
203    let staged: Vec<String> = index
204        .as_ref()
205        .map(|i| i.files.keys().cloned().collect())
206        .unwrap_or_default();
207    ok_json(serde_json::json!({
208        "status": "ok",
209        "current_branch": current_branch,
210        "head": head,
211        "staged": staged,
212    }))
213}
214
215async fn pull_handler(
216    State(state): State<Arc<Mutex<AppState>>>,
217    Json(req): Json<PullRequest>,
218) -> (StatusCode, Json<serde_json::Value>) {
219    let s = state.lock().await;
220    match crate::pull(&s.repo_path, &req.peer, &req.commit_id, false).await {
221        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "pulled"})),
222        Err(e) => err_json(e),
223    }
224}
225
226async fn push_handler(
227    State(state): State<Arc<Mutex<AppState>>>,
228    Json(req): Json<PushRequest>,
229) -> (StatusCode, Json<serde_json::Value>) {
230    let s = state.lock().await;
231    match crate::push(&s.repo_path, &req.peer, false).await {
232        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "pushed"})),
233        Err(e) => err_json(e),
234    }
235}
236
237async fn branch_list_handler(
238    State(state): State<Arc<Mutex<AppState>>>,
239) -> (StatusCode, Json<serde_json::Value>) {
240    let s = state.lock().await;
241    let shard_dir = &s.shard_dir;
242    if !shard_dir.exists() {
243        return err_json("not a shard repository");
244    }
245    let (current, branches) = branch::list_branches(shard_dir).unwrap_or((None, Vec::new()));
246    let branch_list: Vec<serde_json::Value> = branches
247        .into_iter()
248        .map(|(name, tip)| serde_json::json!({"name": name, "tip": tip}))
249        .collect();
250    ok_json(serde_json::json!({"status": "ok", "current": current, "branches": branch_list}))
251}
252
253async fn branch_create_handler(
254    State(state): State<Arc<Mutex<AppState>>>,
255    Json(req): Json<BranchCreateRequest>,
256) -> (StatusCode, Json<serde_json::Value>) {
257    let s = state.lock().await;
258    match crate::branch_create(&s.repo_path, &req.name, req.commit_id.as_deref()) {
259        Ok(()) => ok_json(serde_json::json!({"status": "ok", "message": "branch_created"})),
260        Err(e) => err_json(e),
261    }
262}