1use std::sync::{Arc, Mutex};
13
14use serde::{Deserialize, Serialize};
15
16use edgestore::EdgestoreError;
17use edgestore::Engine;
18
19#[derive(Serialize, Deserialize)]
21struct MerkleResponse {
22 root: Vec<u8>,
23}
24
25#[derive(Serialize, Deserialize)]
27struct SegmentEntry {
28 segment_id: u64,
29 segment_hash: Vec<u8>,
30}
31
32pub struct HttpReplicationServer {
36 engine: Arc<Mutex<Engine>>,
37}
38
39impl HttpReplicationServer {
40 pub fn new(engine: Arc<Mutex<Engine>>) -> Self {
42 HttpReplicationServer { engine }
43 }
44
45 pub fn start(
50 &self,
51 bind_addr: &str,
52 ) -> Result<(std::thread::JoinHandle<()>, u16), EdgestoreError> {
53 let server = tiny_http::Server::http(bind_addr)
54 .map_err(|e| EdgestoreError::ReplicationError(format!("bind error: {}", e)))?;
55
56 let bound_port = match server.server_addr() {
58 tiny_http::ListenAddr::IP(addr) => addr.port(),
59 #[cfg(unix)]
60 tiny_http::ListenAddr::Unix(_) => {
61 return Err(EdgestoreError::ReplicationError(
62 "Unix socket bind not supported for replication server".into(),
63 ))
64 }
65 };
66
67 let engine = Arc::clone(&self.engine);
68
69 let handle = std::thread::spawn(move || {
70 for request in server.incoming_requests() {
71 handle_request(request, &engine);
72 }
73 });
74
75 Ok((handle, bound_port))
76 }
77}
78
79fn parse_url(url: &str) -> (Vec<&str>, bool) {
84 let (path_part, query_part) = match url.split_once('?') {
85 Some((p, q)) => (p, q),
86 None => (url, ""),
87 };
88
89 let debug_json = query_part.split('&').any(|kv| kv == "debug=json");
90
91 let parts: Vec<&str> = path_part.split('/').filter(|s| !s.is_empty()).collect();
92
93 (parts, debug_json)
94}
95
96fn respond_msgpack<T: Serialize>(request: tiny_http::Request, value: &T, debug_json: bool) {
98 if debug_json {
99 match serde_json::to_vec(value) {
100 Ok(json_bytes) => {
101 let response = tiny_http::Response::from_data(json_bytes).with_header(
102 tiny_http::Header::from_bytes("Content-Type", "application/json").unwrap(),
103 );
104 let _ = request.respond(response);
105 }
106 Err(e) => {
107 respond_error(request, 500, &format!("json serialization error: {}", e));
108 }
109 }
110 } else {
111 match rmp_serde::to_vec_named(value) {
112 Ok(msgpack_bytes) => {
113 let response = tiny_http::Response::from_data(msgpack_bytes).with_header(
114 tiny_http::Header::from_bytes("Content-Type", "application/msgpack").unwrap(),
115 );
116 let _ = request.respond(response);
117 }
118 Err(e) => {
119 respond_error(request, 500, &format!("msgpack serialization error: {}", e));
120 }
121 }
122 }
123}
124
125fn respond_raw(request: tiny_http::Request, data: Vec<u8>) {
127 let response = tiny_http::Response::from_data(data).with_header(
128 tiny_http::Header::from_bytes("Content-Type", "application/octet-stream").unwrap(),
129 );
130 let _ = request.respond(response);
131}
132
133fn respond_error(request: tiny_http::Request, status: u16, msg: &str) {
135 let response = tiny_http::Response::from_string(msg.to_string())
136 .with_status_code(tiny_http::StatusCode(status));
137 let _ = request.respond(response);
138}
139
140fn handle_request(mut request: tiny_http::Request, engine: &Arc<Mutex<Engine>>) {
142 let mut _body = Vec::new();
144 let _ = request.as_reader().read_to_end(&mut _body);
145
146 let method = request.method().clone();
147 let url = request.url().to_string();
148
149 let (parts, debug_json) = parse_url(&url);
150
151 match (method, parts.as_slice()) {
152 (tiny_http::Method::Get, ["merkle"]) => {
153 match engine.lock() {
155 Ok(eng) => match eng.range_merkle_root() {
156 Ok(root) => {
157 let resp = MerkleResponse {
158 root: root.to_vec(),
159 };
160 respond_msgpack(request, &resp, debug_json);
161 }
162 Err(e) => {
163 respond_error(request, 500, &format!("merkle root error: {}", e));
164 }
165 },
166 Err(_) => {
167 respond_error(request, 500, "engine lock poisoned");
168 }
169 }
170 }
171
172 (tiny_http::Method::Get, ["segments"]) => {
173 match engine.lock() {
175 Ok(eng) => match eng.export_manifest() {
176 Ok(refs) => {
177 let entries: Vec<SegmentEntry> = refs
178 .into_iter()
179 .map(|sr| SegmentEntry {
180 segment_id: sr.segment_id,
181 segment_hash: sr.segment_hash.to_vec(),
182 })
183 .collect();
184 respond_msgpack(request, &entries, debug_json);
185 }
186 Err(e) => {
187 respond_error(request, 500, &format!("export manifest error: {}", e));
188 }
189 },
190 Err(_) => {
191 respond_error(request, 500, "engine lock poisoned");
192 }
193 }
194 }
195
196 (tiny_http::Method::Get, ["segments", hash_hex]) => {
197 let hash_hex = *hash_hex;
200 if hash_hex.len() != 64 {
201 respond_error(request, 400, "hash_hex must be 64 hex characters");
202 return;
203 }
204
205 let mut hash_bytes = [0u8; 32];
207 let mut valid = true;
208 for i in 0..32 {
209 match u8::from_str_radix(&hash_hex[i * 2..i * 2 + 2], 16) {
210 Ok(b) => hash_bytes[i] = b,
211 Err(_) => {
212 valid = false;
213 break;
214 }
215 }
216 }
217 if !valid {
218 respond_error(request, 400, "invalid hex encoding in hash");
219 return;
220 }
221
222 let dat_bytes = {
227 let eng_guard = match engine.lock() {
228 Ok(g) => g,
229 Err(_) => {
230 respond_error(request, 500, "engine lock poisoned");
231 return;
232 }
233 };
234
235 let manifest = match eng_guard.export_manifest() {
237 Ok(m) => m,
238 Err(e) => {
239 respond_error(request, 500, &format!("manifest error: {}", e));
240 return;
241 }
242 };
243
244 let segment_id = manifest
245 .iter()
246 .find(|sr| sr.segment_hash == hash_bytes)
247 .map(|sr| sr.segment_id);
248
249 let base_path = eng_guard.db_path().to_path_buf();
250
251 drop(eng_guard); match segment_id {
254 Some(sid) => {
255 let dat_path = base_path.join(format!("segment-{:08}.dat", sid));
257 match std::fs::read(&dat_path) {
258 Ok(bytes) => bytes,
259 Err(_) => {
260 let fallback = base_path.join(format!("{}.dat", hash_hex));
262 match std::fs::read(&fallback) {
263 Ok(b) => b,
264 Err(e) => {
265 respond_error(
266 request,
267 404,
268 &format!("segment not found: {}", e),
269 );
270 return;
271 }
272 }
273 }
274 }
275 }
276 None => {
277 let dat_path = base_path.join(format!("{}.dat", hash_hex));
279 match std::fs::read(&dat_path) {
280 Ok(b) => b,
281 Err(_) => {
282 respond_error(request, 404, "segment not found");
283 return;
284 }
285 }
286 }
287 }
288 };
289
290 respond_raw(request, dat_bytes);
291 }
292
293 _ => {
294 respond_error(request, 404, "not found");
295 }
296 }
297}