1use crate::core::{protocol_mod as protocol, BraidError, Result};
2use crate::fs::state::DaemonState;
3use braid_http::types::{BraidRequest, Version as BraidVersion};
4use std::path::PathBuf;
5use std::process::Command;
6use tracing::{error, info};
7
8pub async fn sync_local_to_remote(
10 _path: &PathBuf,
11 url_in: &str,
12 parents: &[String],
13 _original_content: Option<String>,
14 new_content: String,
15 content_type: Option<String>,
16 state: DaemonState,
17) -> Result<()> {
18 let url_str = url_in.trim_matches('"').trim().to_string();
19 info!("[BraidFS] Syncing {} to remote...", url_str);
20
21 if url_str.contains("braid.org") {
24 let mut cookie_str = String::new();
25 let mut parents_header = String::new();
26
27 if let Ok(u) = url::Url::parse(&url_str) {
28 if let Some(domain) = u.domain() {
29 let cfg = state.config.read().await;
31 if let Some(token) = cfg.cookies.get(domain) {
32 cookie_str = if token.contains('=') {
33 token.clone()
34 } else {
35 format!("client={}", token)
36 };
37 }
38 }
39 }
40
41 let mut head_req = BraidRequest::new().with_method("GET");
43 if !cookie_str.is_empty() {
44 head_req = head_req.with_header("Cookie", cookie_str.clone());
45 }
46
47 if let Ok(res) = state.client.fetch(&url_str, head_req).await {
48 if let Some(v_header) = res.header("version").or(res.header("current-version")) {
49 parents_header = v_header.to_string();
50 info!("[BraidFS] Found parents for braid.org: {}", parents_header);
51 }
52 }
53
54 let mut curl_cmd = Command::new("curl.exe");
55 curl_cmd
56 .arg("-i")
57 .arg("-s")
58 .arg("-X")
59 .arg("PUT")
60 .arg(&url_str);
61
62 if !cookie_str.is_empty() {
63 curl_cmd.arg("-H").arg(format!("Cookie: {}", cookie_str));
64 }
65
66 if !parents_header.is_empty() {
67 curl_cmd
68 .arg("-H")
69 .arg(format!("Parents: {}", parents_header));
70 }
71
72 let output = curl_cmd
73 .arg("-H")
74 .arg("Accept: */*")
75 .arg("-H")
76 .arg("User-Agent: curl/8.0.1")
77 .arg("-d")
78 .arg(&new_content)
79 .output();
80
81 match output {
82 Ok(out) => {
83 let stdout = String::from_utf8_lossy(&out.stdout);
84 let stderr = String::from_utf8_lossy(&out.stderr);
85
86 if stdout.contains("200 OK")
87 || stdout.contains("209 Subscription")
88 || stdout.contains("204 No Content")
89 || stdout.contains("201 Created")
90 {
91 state.failed_syncs.write().await.remove(&url_str);
92 info!("[BraidFS] Sync success (curl) for {}", url_str);
93 state
94 .content_cache
95 .write()
96 .await
97 .insert(url_str.clone(), new_content.clone());
98 return Ok(());
99 } else {
100 let status_line = stdout.lines().next().unwrap_or("Unknown").to_string();
101 let status_code = if status_line.contains("309") {
102 309
103 } else {
104 500
105 };
106 let err_msg = format!(
107 "Sync failed (curl). Status: {}. Stderr: {}",
108 status_line, stderr
109 );
110 error!("[BraidFS] {}", err_msg);
111 state
112 .failed_syncs
113 .write()
114 .await
115 .insert(url_str.clone(), (status_code, std::time::Instant::now()));
116 return Err(BraidError::Http(err_msg));
117 }
118 }
119 Err(e) => {
120 error!("[BraidFS] Failed to spawn curl: {}", e);
121 return Err(BraidError::Io(e));
122 }
123 }
124 }
125
126 let mut request = BraidRequest::new().with_method("PUT");
128 let mut effective_parents = parents.to_vec();
129
130 if effective_parents.is_empty() {
131 let mut head_req = BraidRequest::new()
132 .with_method("GET")
133 .with_header("Accept", "text/plain");
134
135 if let Ok(u) = url::Url::parse(&url_str) {
136 if let Some(domain) = u.domain() {
137 let cfg = state.config.read().await;
138 if let Some(token) = cfg.cookies.get(domain) {
139 let cookie_str = if token.contains('=') {
140 token.clone()
141 } else {
142 format!("token={}", token)
143 };
144 head_req = head_req.with_header("Cookie", cookie_str);
145 }
146 }
147 }
148
149 if let Ok(res) = state.client.fetch(&url_str, head_req).await {
150 if let Some(v_header) = res
151 .headers
152 .get("version")
153 .or(res.headers.get("current-version"))
154 {
155 if let Ok(versions) = protocol::parse_version_header(v_header) {
156 for v in versions {
157 let v_str = match v {
158 BraidVersion::String(s) => s,
159 BraidVersion::Integer(i) => i.to_string(),
160 };
161 let normalized = v_str.trim_matches('"').to_string();
162 if !normalized.is_empty() {
163 effective_parents.push(normalized);
164 }
165 }
166 }
167 }
168 }
169 }
170
171 if !effective_parents.is_empty() {
172 let filtered_parents: Vec<BraidVersion> = effective_parents
173 .iter()
174 .filter(|p| !p.starts_with("temp-") && !p.starts_with("missing-"))
175 .map(|p| BraidVersion::new(p))
176 .collect();
177
178 if !filtered_parents.is_empty() {
179 request = request.with_parents(filtered_parents);
180 }
181 }
182
183 let ct = content_type.unwrap_or_else(|| "text/plain".to_string());
184 request = request.with_content_type(ct);
185 let mut final_request = request.with_body(new_content.clone());
186
187 if let Ok(u) = url::Url::parse(&url_str) {
188 if let Some(domain) = u.domain() {
189 let cfg = state.config.read().await;
190 if let Some(token) = cfg.cookies.get(domain) {
191 let cookie_str = if token.contains('=') {
192 token.clone()
193 } else {
194 format!("token={}", token)
195 };
196 final_request = final_request.with_header("Cookie", cookie_str);
197 }
198 }
199 }
200
201 let status = match state.client.fetch(&url_str, final_request).await {
202 Ok(res) => {
203 if (200..300).contains(&res.status) {
204 state.failed_syncs.write().await.remove(&url_str);
205 info!("[BraidFS] Sync success (braid) status: {}", res.status);
206 state
207 .content_cache
208 .write()
209 .await
210 .insert(url_str.clone(), new_content);
211 return Ok(());
212 }
213 res.status
214 }
215 Err(e) => {
216 error!("[BraidFS] Sync error: {}", e);
217 500
218 }
219 };
220
221 let err_msg = format!("Sync failed: HTTP {}", status);
222 state
223 .failed_syncs
224 .write()
225 .await
226 .insert(url_str, (status, std::time::Instant::now()));
227 Err(BraidError::Http(err_msg))
228}
229
230pub async fn sync_binary_to_remote(
232 _path: &std::path::Path,
233 url_in: &str,
234 parents: &[String],
235 data: bytes::Bytes,
236 content_type: Option<String>,
237 state: DaemonState,
238) -> Result<()> {
239 let url_str = url_in.trim_matches('"').trim().to_string();
240 info!("[BraidFS] Syncing binary {} to remote...", url_str);
241
242 let mut parents_header = String::new();
244 if parents.is_empty() {
245 let head_req = BraidRequest::new().with_method("GET");
246 if let Ok(res) = state.client.fetch(&url_str, head_req).await {
247 if let Some(v_header) = res.header("version").or(res.header("current-version")) {
248 parents_header = v_header.to_string();
249 }
250 }
251 } else {
252 parents_header = format!("\"{}\"", parents.join("\", \""));
254 }
255
256 let mut cookie_str = String::new();
258 if let Ok(u) = url::Url::parse(&url_str) {
259 if let Some(domain) = u.domain() {
260 let cfg = state.config.read().await;
261 if let Some(token) = cfg.cookies.get(domain) {
262 cookie_str = if token.contains('=') {
263 token.clone()
264 } else {
265 format!("client={}", token)
266 };
267 }
268 }
269 }
270
271 let mut curl_cmd = Command::new("curl.exe");
273 curl_cmd
274 .arg("-i")
275 .arg("-s")
276 .arg("-X")
277 .arg("PUT")
278 .arg(&url_str);
279
280 if !cookie_str.is_empty() {
281 curl_cmd.arg("-H").arg(format!("Cookie: {}", cookie_str));
282 }
283
284 if !parents_header.is_empty() {
285 curl_cmd
286 .arg("-H")
287 .arg(format!("Parents: {}", parents_header));
288 }
289
290 let ct = content_type.unwrap_or_else(|| "application/octet-stream".to_string());
291 curl_cmd.arg("-H").arg(format!("Content-Type: {}", ct));
292
293 use std::io::Write;
295 let mut child = curl_cmd
296 .stdin(std::process::Stdio::piped())
297 .stdout(std::process::Stdio::piped())
298 .stderr(std::process::Stdio::piped())
299 .spawn()
300 .map_err(|e| BraidError::Io(e))?;
301
302 let mut stdin = child
303 .stdin
304 .take()
305 .ok_or_else(|| BraidError::Fs("Failed to open stdin for curl".to_string()))?;
306 stdin.write_all(&data).map_err(|e| BraidError::Io(e))?;
307 drop(stdin);
308
309 let out = child.wait_with_output().map_err(|e| BraidError::Io(e))?;
310 let stdout = String::from_utf8_lossy(&out.stdout);
311 let stderr = String::from_utf8_lossy(&out.stderr);
312
313 if stdout.contains("200 OK")
314 || stdout.contains("209 Subscription")
315 || stdout.contains("204 No Content")
316 || stdout.contains("201 Created")
317 {
318 info!("[BraidFS] Binary sync success (curl) for {}", url_str);
319 return Ok(());
320 } else {
321 let status_line = stdout.lines().next().unwrap_or("Unknown").to_string();
322 let err_msg = format!(
323 "Binary sync failed (curl). Status: {}. Stderr: {}",
324 status_line, stderr
325 );
326 error!("[BraidFS] {}", err_msg);
327 return Err(BraidError::Http(err_msg));
328 }
329}