socket_patch_cli/commands/
repair.rs1use clap::Args;
2use socket_patch_core::api::blob_fetcher::{
3 fetch_missing_sources, format_fetch_result, get_missing_archives, get_missing_blobs,
4 DownloadMode,
5};
6use socket_patch_core::api::client::get_api_client_with_overrides;
7use socket_patch_core::manifest::operations::read_manifest;
8use socket_patch_core::patch::apply::PatchSources;
9use socket_patch_core::utils::cleanup_blobs::{
10 cleanup_unused_archives, cleanup_unused_blobs, format_cleanup_result,
11};
12use socket_patch_core::utils::telemetry::{track_patch_repair_failed, track_patch_repaired};
13use std::path::Path;
14use std::time::Duration;
15
16use crate::args::{apply_env_toggles, GlobalArgs};
17use crate::commands::lock_cli::{acquire_or_emit, lock_broken_event};
18use crate::json_envelope::{Command, Envelope, EnvelopeError, PatchAction, PatchEvent};
19
20#[derive(Args)]
21pub struct RepairArgs {
22 #[command(flatten)]
23 pub common: GlobalArgs,
24
25 #[arg(long = "download-only", env = "SOCKET_DOWNLOAD_ONLY", default_value_t = false)]
28 pub download_only: bool,
29}
30
31pub async fn run(args: RepairArgs) -> i32 {
32 apply_env_toggles(&args.common);
33
34 if args.common.offline && args.download_only {
37 let msg =
38 "--offline and --download-only are mutually exclusive".to_string();
39 if args.common.json {
40 let mut env = Envelope::new(Command::Repair);
41 env.dry_run = args.common.dry_run;
42 env.mark_error(EnvelopeError::new("invalid_args", msg));
43 println!("{}", env.to_pretty_json());
44 } else {
45 eprintln!("Error: {msg}");
46 }
47 return 2;
48 }
49
50 let manifest_path = args.common.resolved_manifest_path();
51
52 if tokio::fs::metadata(&manifest_path).await.is_err() {
53 if args.common.json {
54 let mut env = Envelope::new(Command::Repair);
55 env.dry_run = args.common.dry_run;
56 env.mark_error(EnvelopeError::new(
57 "manifest_not_found",
58 format!("Manifest not found at {}", manifest_path.display()),
59 ));
60 println!("{}", env.to_pretty_json());
61 } else {
62 eprintln!("Manifest not found at {}", manifest_path.display());
63 }
64 return 1;
65 }
66
67 let socket_dir = manifest_path.parent().unwrap_or(Path::new("."));
70 let acquired = match acquire_or_emit(
71 socket_dir,
72 Command::Repair,
73 args.common.json,
74 args.common.silent,
75 args.common.dry_run,
76 Duration::from_secs(args.common.lock_timeout.unwrap_or(0)),
77 args.common.break_lock,
78 ) {
79 Ok(acquired) => acquired,
80 Err(code) => return code,
81 };
82 let _lock = acquired.guard;
83 let lock_was_broken = acquired.broke_lock;
84
85 match repair_inner(&args, &manifest_path).await {
86 Ok((mut env, counts)) => {
87 if lock_was_broken {
88 env.record(lock_broken_event(socket_dir));
93 }
94 track_patch_repaired(
95 counts.downloaded,
96 counts.cleaned,
97 0,
98 args.common.api_token.as_deref(),
99 args.common.org.as_deref(),
100 )
101 .await;
102 if args.common.json {
103 println!("{}", env.to_pretty_json());
104 }
105 0
106 }
107 Err(e) => {
108 track_patch_repair_failed(
109 &e,
110 args.common.api_token.as_deref(),
111 args.common.org.as_deref(),
112 )
113 .await;
114 if args.common.json {
115 let mut env = Envelope::new(Command::Repair);
116 env.dry_run = args.common.dry_run;
117 env.mark_error(EnvelopeError::new("repair_failed", e));
118 println!("{}", env.to_pretty_json());
119 } else {
120 eprintln!("Error: {e}");
121 }
122 1
123 }
124 }
125}
126
127struct RepairCounts {
129 downloaded: usize,
130 cleaned: usize,
131}
132
133async fn repair_inner(
134 args: &RepairArgs,
135 manifest_path: &Path,
136) -> Result<(Envelope, RepairCounts), String> {
137 let manifest = read_manifest(manifest_path)
138 .await
139 .map_err(|e| e.to_string())?
140 .ok_or_else(|| "Invalid manifest".to_string())?;
141
142 let socket_dir = manifest_path.parent().unwrap();
143 let blobs_path = socket_dir.join("blobs");
144 let diffs_path = socket_dir.join("diffs");
145 let packages_path = socket_dir.join("packages");
146
147 let download_mode = DownloadMode::parse(&args.common.download_mode).map_err(|e| e.to_string())?;
148
149 let mut downloaded_count = 0usize;
150 let mut download_failed_count = 0usize;
151 let mut blobs_cleaned = 0usize;
152 let mut blobs_checked = 0usize;
153
154 let missing_artifacts: Vec<String> = match download_mode {
158 DownloadMode::File => get_missing_blobs(&manifest, &blobs_path)
159 .await
160 .into_iter()
161 .collect(),
162 DownloadMode::Diff => get_missing_archives(&manifest, &diffs_path)
163 .await
164 .into_iter()
165 .collect(),
166 DownloadMode::Package => get_missing_archives(&manifest, &packages_path)
167 .await
168 .into_iter()
169 .collect(),
170 };
171 let missing_count = missing_artifacts.len();
172
173 if !args.common.offline {
174 if !missing_artifacts.is_empty() {
175 if !args.common.json {
176 println!(
177 "Found {} missing {} artifact(s)",
178 missing_artifacts.len(),
179 download_mode.as_tag()
180 );
181 }
182
183 if args.common.dry_run {
184 if !args.common.json {
185 println!("\nDry run - would download:");
186 for id in missing_artifacts.iter().take(10) {
187 println!(" - {}...", &id[..12.min(id.len())]);
188 }
189 if missing_artifacts.len() > 10 {
190 println!(" ... and {} more", missing_artifacts.len() - 10);
191 }
192 }
193 } else {
194 if !args.common.json {
195 println!("\nDownloading missing {}s...", download_mode.as_tag());
196 }
197 let (client, _) =
198 get_api_client_with_overrides(args.common.api_client_overrides()).await;
199 let sources = PatchSources {
200 blobs_path: &blobs_path,
201 packages_path: Some(&packages_path),
202 diffs_path: Some(&diffs_path),
203 };
204 let fetch_result =
205 fetch_missing_sources(&manifest, &sources, download_mode, &client, None).await;
206 downloaded_count = fetch_result.downloaded;
207 download_failed_count = fetch_result.failed;
208 if !args.common.json {
209 println!("{}", format_fetch_result(&fetch_result));
210 }
211 }
212 } else if !args.common.json {
213 println!(
214 "All {} artifacts are present locally.",
215 download_mode.as_tag()
216 );
217 }
218 } else if !missing_artifacts.is_empty() {
219 if !args.common.json {
220 println!(
221 "Warning: {} {} artifact(s) are missing (offline mode - not downloading)",
222 missing_artifacts.len(),
223 download_mode.as_tag()
224 );
225 for id in missing_artifacts.iter().take(5) {
226 println!(" - {}...", &id[..12.min(id.len())]);
227 }
228 if missing_artifacts.len() > 5 {
229 println!(" ... and {} more", missing_artifacts.len() - 5);
230 }
231 }
232 } else if !args.common.json {
233 println!(
234 "All {} artifacts are present locally.",
235 download_mode.as_tag()
236 );
237 }
238
239 if !args.download_only {
241 if !args.common.json {
242 println!();
243 }
244 match cleanup_unused_blobs(&manifest, &blobs_path, args.common.dry_run).await {
245 Ok(cleanup_result) => {
246 blobs_checked += cleanup_result.blobs_checked;
247 blobs_cleaned += cleanup_result.blobs_removed;
248 if !args.common.json {
249 if cleanup_result.blobs_checked == 0 {
250 println!("No blobs directory found, nothing to clean up.");
251 } else if cleanup_result.blobs_removed == 0 {
252 println!(
253 "Checked {} blob(s), all are in use.",
254 cleanup_result.blobs_checked
255 );
256 } else {
257 println!("{}", format_cleanup_result(&cleanup_result, args.common.dry_run));
258 }
259 }
260 }
261 Err(e) => {
262 if !args.common.json {
263 eprintln!("Warning: blob cleanup failed: {e}");
264 }
265 }
266 }
267
268 match cleanup_unused_archives(&manifest, &diffs_path, args.common.dry_run).await {
270 Ok(cleanup_result) => {
271 blobs_checked += cleanup_result.blobs_checked;
272 blobs_cleaned += cleanup_result.blobs_removed;
273 if !args.common.json && cleanup_result.blobs_removed > 0 {
274 println!(
275 "{}",
276 format_cleanup_result(&cleanup_result, args.common.dry_run)
277 .replace("blob(s)", "diff archive(s)")
278 );
279 }
280 }
281 Err(e) => {
282 if !args.common.json {
283 eprintln!("Warning: diff cleanup failed: {e}");
284 }
285 }
286 }
287
288 match cleanup_unused_archives(&manifest, &packages_path, args.common.dry_run).await {
290 Ok(cleanup_result) => {
291 blobs_checked += cleanup_result.blobs_checked;
292 blobs_cleaned += cleanup_result.blobs_removed;
293 if !args.common.json && cleanup_result.blobs_removed > 0 {
294 println!(
295 "{}",
296 format_cleanup_result(&cleanup_result, args.common.dry_run)
297 .replace("blob(s)", "package archive(s)")
298 );
299 }
300 }
301 Err(e) => {
302 if !args.common.json {
303 eprintln!("Warning: package cleanup failed: {e}");
304 }
305 }
306 }
307 }
308
309 if !args.common.dry_run && !args.common.json {
310 println!("\nRepair complete.");
311 }
312
313 let mut env = Envelope::new(Command::Repair);
317 env.dry_run = args.common.dry_run;
318 let action_for_repair = if args.common.dry_run {
319 PatchAction::Verified
320 } else {
321 PatchAction::Downloaded
322 };
323 if downloaded_count > 0 || (args.common.dry_run && missing_count > 0) {
324 let count = if args.common.dry_run {
325 missing_count
326 } else {
327 downloaded_count
328 };
329 env.record(
330 PatchEvent::artifact(action_for_repair).with_details(serde_json::json!({
331 "count": count,
332 "mode": download_mode.as_tag(),
333 })),
334 );
335 }
336 if download_failed_count > 0 {
337 env.record(
338 PatchEvent::artifact(PatchAction::Failed).with_error(
339 "download_failed",
340 format!("{} artifact(s) failed to download", download_failed_count),
341 ),
342 );
343 env.mark_partial_failure();
344 }
345 if blobs_cleaned > 0 {
346 let cleanup_action = if args.common.dry_run {
347 PatchAction::Verified
348 } else {
349 PatchAction::Removed
350 };
351 env.record(PatchEvent::artifact(cleanup_action).with_details(serde_json::json!({
352 "count": blobs_cleaned,
353 "checked": blobs_checked,
354 })));
355 }
356 Ok((
357 env,
358 RepairCounts {
359 downloaded: downloaded_count,
360 cleaned: blobs_cleaned,
361 },
362 ))
363}