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}