1use crate::core::{Object, ObjectHash};
7use crate::network::transport::RemoteRef;
8use crate::storage::ObjectStore;
9
10pub fn is_https_url(url: &str) -> bool {
12 url.starts_with("https://") || url.starts_with("http://")
13}
14
15fn agent() -> ureq::Agent {
17 ureq::Agent::new()
18}
19
20fn get(url: &str, token: Option<&str>) -> ureq::Request {
22 let req = agent().get(url);
23 if let Some(t) = token {
24 req.set("Authorization", &format!("Bearer {}", t))
25 } else {
26 req
27 }
28}
29
30fn post(url: &str, token: Option<&str>) -> ureq::Request {
31 let req = agent().post(url);
32 if let Some(t) = token {
33 req.set("Authorization", &format!("Bearer {}", t))
34 } else {
35 req
36 }
37}
38
39fn put(url: &str, token: Option<&str>) -> ureq::Request {
40 let req = agent().put(url);
41 if let Some(t) = token {
42 req.set("Authorization", &format!("Bearer {}", t))
43 } else {
44 req
45 }
46}
47
48fn read_json(resp: ureq::Response) -> Result<serde_json::Value, String> {
50 resp.into_json::<serde_json::Value>()
51 .map_err(|e| format!("Failed to parse response: {}", e))
52}
53
54fn check_response(resp: Result<ureq::Response, ureq::Error>) -> Result<ureq::Response, String> {
56 match resp {
57 Ok(r) => Ok(r),
58 Err(ureq::Error::Status(code, resp)) => {
59 let body = resp.into_string().unwrap_or_default();
60 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
62 if let Some(msg) = v
63 .get("error")
64 .and_then(|e| e.get("message"))
65 .and_then(|m| m.as_str())
66 {
67 return Err(format!("HTTP {}: {}", code, msg));
68 }
69 }
70 Err(format!("HTTP {}: {}", code, body))
71 }
72 Err(ureq::Error::Transport(t)) => Err(format!("Connection error: {}", t)),
73 }
74}
75
76pub fn list_refs_http(
78 base_url: &str,
79 kind: &str,
80 token: Option<&str>,
81) -> Result<Vec<RemoteRef>, String> {
82 let url = format!("{}/api/v1/transport/refs?kind={}", base_url, kind);
83 let resp = check_response(get(&url, token).call())?;
84 let json = read_json(resp)?;
85
86 let refs = json
87 .get("refs")
88 .and_then(|v| v.as_array())
89 .ok_or("Invalid refs response")?;
90
91 let mut result = Vec::new();
92 for r in refs {
93 let kind = r.get("kind").and_then(|v| v.as_str()).unwrap_or("heads");
94 let name = r
95 .get("name")
96 .and_then(|v| v.as_str())
97 .ok_or("Missing ref name")?;
98 let hash = r
99 .get("hash")
100 .and_then(|v| v.as_str())
101 .ok_or("Missing ref hash")?;
102 result.push(RemoteRef {
103 kind: kind.to_string(),
104 name: name.to_string(),
105 hash: hash.to_string(),
106 });
107 }
108 Ok(result)
109}
110
111pub fn read_ref_http(base_url: &str, branch: &str, token: Option<&str>) -> Result<String, String> {
113 let url = format!("{}/api/v1/transport/refs/heads/{}", base_url, branch);
114 let resp = check_response(get(&url, token).call())?;
115 let json = read_json(resp)?;
116 json.get("hash")
117 .and_then(|v| v.as_str())
118 .map(|s| s.to_string())
119 .ok_or("Missing hash in response".to_string())
120}
121
122pub fn read_head_http(base_url: &str, token: Option<&str>) -> Result<String, String> {
124 let url = format!("{}/api/v1/transport/head", base_url);
125 let resp = check_response(get(&url, token).call())?;
126 let json = read_json(resp)?;
127 json.get("head")
128 .and_then(|v| v.as_str())
129 .map(|s| s.to_string())
130 .ok_or("Missing head in response".to_string())
131}
132
133pub fn update_ref_http(
135 base_url: &str,
136 branch: &str,
137 hash: &str,
138 force: bool,
139 token: Option<&str>,
140) -> Result<(), String> {
141 let url = format!("{}/api/v1/transport/refs/heads/{}", base_url, branch);
142 let body = serde_json::json!({"hash": hash, "force": force});
143 check_response(put(&url, token).send_json(body))?;
144 Ok(())
145}
146
147pub fn negotiate_http(
149 base_url: &str,
150 wants: &[String],
151 haves: &[String],
152 token: Option<&str>,
153) -> Result<Vec<ObjectHash>, String> {
154 let url = format!("{}/api/v1/transport/negotiate", base_url);
155 let body = serde_json::json!({"wants": wants, "haves": haves});
156 let resp = check_response(post(&url, token).send_json(body))?;
157 let json = read_json(resp)?;
158
159 let needed = json
160 .get("needed")
161 .and_then(|v| v.as_array())
162 .ok_or("Invalid negotiate response")?;
163
164 Ok(needed
165 .iter()
166 .filter_map(|v| v.as_str())
167 .map(|s| ObjectHash::from_hex(s.to_string()))
168 .collect())
169}
170
171pub fn download_objects_http(
173 base_url: &str,
174 local_store: &ObjectStore,
175 hashes: &[ObjectHash],
176 token: Option<&str>,
177) -> Result<usize, String> {
178 let mut count = 0;
179 for chunk in hashes.chunks(50) {
181 for hash in chunk {
182 if local_store.exists(hash) {
183 continue;
184 }
185 let url = format!("{}/api/v1/transport/objects/{}", base_url, hash.as_str());
186 let resp = check_response(get(&url, token).call())?;
187 let json = read_json(resp)?;
188
189 let b64_data = json
190 .get("data")
191 .and_then(|v| v.as_str())
192 .ok_or("Missing object data")?;
193
194 let compressed = base64_decode(b64_data)?;
195
196 use std::io::Read as _;
197 let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
198 let mut raw = Vec::new();
199 decoder
200 .read_to_end(&mut raw)
201 .map_err(|e| format!("Decompress error: {}", e))?;
202
203 let obj = Object::from_bytes(&raw)?;
204 local_store.write(&obj)?;
205 count += 1;
206 }
207 }
208 Ok(count)
209}
210
211pub fn upload_objects_http(
213 base_url: &str,
214 local_store: &ObjectStore,
215 hashes: &[ObjectHash],
216 token: Option<&str>,
217) -> Result<usize, String> {
218 let url = format!("{}/api/v1/transport/objects", base_url);
219
220 let mut total = 0;
222 for chunk in hashes.chunks(50) {
223 let mut objects_json = Vec::new();
224 for hash in chunk {
225 let obj = local_store.read(hash)?;
226 let data = obj.to_bytes();
227 use std::io::Write as _;
228 let mut encoder =
229 flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
230 encoder
231 .write_all(&data)
232 .map_err(|e| format!("Compress error: {}", e))?;
233 let compressed = encoder
234 .finish()
235 .map_err(|e| format!("Compress error: {}", e))?;
236 let b64 = base64_encode(&compressed);
237 objects_json.push(serde_json::json!({"hash": hash.as_str(), "data": b64}));
238 }
239
240 let body = serde_json::json!({"objects": objects_json});
241 let resp = check_response(post(&url, token).send_json(body))?;
242 let json = read_json(resp)?;
243 total += json.get("written").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
244 }
245 Ok(total)
246}
247
248fn base64_encode(data: &[u8]) -> String {
251 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
252 let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
253 for chunk in data.chunks(3) {
254 let b0 = chunk[0] as u32;
255 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
256 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
257 let triple = (b0 << 16) | (b1 << 8) | b2;
258 out.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
259 out.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
260 if chunk.len() > 1 {
261 out.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
262 } else {
263 out.push('=');
264 }
265 if chunk.len() > 2 {
266 out.push(CHARS[(triple & 0x3F) as usize] as char);
267 } else {
268 out.push('=');
269 }
270 }
271 out
272}
273
274fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
275 fn val(c: u8) -> Result<u32, String> {
276 match c {
277 b'A'..=b'Z' => Ok((c - b'A') as u32),
278 b'a'..=b'z' => Ok((c - b'a' + 26) as u32),
279 b'0'..=b'9' => Ok((c - b'0' + 52) as u32),
280 b'+' => Ok(62),
281 b'/' => Ok(63),
282 b'=' => Ok(0),
283 _ => Err(format!("Invalid base64 character: {}", c as char)),
284 }
285 }
286 let bytes: Vec<u8> = input.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
287 let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
288 for chunk in bytes.chunks(4) {
289 if chunk.len() < 4 {
290 return Err("Invalid base64 length".to_string());
291 }
292 let a = val(chunk[0])?;
293 let b = val(chunk[1])?;
294 let c = val(chunk[2])?;
295 let d = val(chunk[3])?;
296 let triple = (a << 18) | (b << 12) | (c << 6) | d;
297 out.push(((triple >> 16) & 0xFF) as u8);
298 if chunk[2] != b'=' {
299 out.push(((triple >> 8) & 0xFF) as u8);
300 }
301 if chunk[3] != b'=' {
302 out.push((triple & 0xFF) as u8);
303 }
304 }
305 Ok(out)
306}