1#![allow(clippy::print_stdout)]
6
7use crate::cli::CacheCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use crate::repository;
11use anyhow::Context;
12
13use std::fmt::Write;
14
15pub fn run(cmd: CacheCommand) -> anyhow::Result<()> {
21 match cmd {
22 CacheCommand::List {
23 repo,
24 json,
25 hostname,
26 } => list(repo.as_deref(), json, hostname.as_deref()),
27 CacheCommand::Delete {
28 key,
29 repo,
30 all,
31 key_prefix,
32 ref_,
33 hostname,
34 } => delete(
35 key.as_deref(),
36 repo.as_deref(),
37 all,
38 key_prefix.as_deref(),
39 ref_.as_deref(),
40 hostname.as_deref(),
41 ),
42 }
43}
44
45fn list(
46 repo: Option<&str>,
47 json: Option<Vec<String>>,
48 hostname: Option<&str>,
49) -> anyhow::Result<()> {
50 let spec = match repo {
51 Some(s) => repository::parse_repo_spec(s).context("invalid repository spec")?,
52 None => repository::detect_remote().ok_or_else(|| {
53 anyhow::anyhow!("could not detect repository; specify OWNER/REPO with --repo")
54 })?,
55 };
56
57 let host = hostname.unwrap_or("github.com");
58 let client = Client::new(host).context("failed to create HTTP client")?;
59
60 let path = format!(
61 "/repos/{}/{}/actions/caches?per_page=100",
62 spec.owner, spec.repo
63 );
64
65 let response = client.get(&path).context("failed to fetch caches")?;
66 let status = response.status();
67 if !status.is_success() {
68 anyhow::bail!("failed to list caches: HTTP {status}");
69 }
70
71 let result: serde_json::Value = response.json().context("failed to parse response")?;
72 let caches: Vec<serde_json::Value> = result["actions_caches"]
73 .as_array()
74 .map_or_else(Vec::new, Clone::clone);
75
76 if let Some(fields) = json {
77 let fields_ref: Option<&[String]> = if fields.is_empty() {
78 None
79 } else {
80 Some(&fields)
81 };
82 print_json(&caches, fields_ref);
83 return Ok(());
84 }
85
86 if caches.is_empty() {
87 println!("No caches found.");
88 return Ok(());
89 }
90
91 println!("{:<30} {:<10} CREATED", "KEY", "SIZE (MB)");
92 for c in &caches {
93 let key = c["key"].as_str().unwrap_or("—");
94 let size = c["size_in_bytes"].as_u64().unwrap_or(0);
95 let created = c["created_at"].as_str().unwrap_or("—");
96 let key_truncated = crate::cmd::util::truncate(key, 30);
97 println!("{key_truncated:<30} {:<10} {created}", size / 1024 / 1024);
98 }
99
100 Ok(())
101}
102
103fn delete(
104 key: Option<&str>,
105 repo: Option<&str>,
106 all: bool,
107 key_prefix: Option<&str>,
108 ref_: Option<&str>,
109 hostname: Option<&str>,
110) -> anyhow::Result<()> {
111 let spec = match repo {
112 Some(s) => repository::parse_repo_spec(s).context("invalid repository spec")?,
113 None => repository::detect_remote().ok_or_else(|| {
114 anyhow::anyhow!("could not detect repository; specify OWNER/REPO with --repo")
115 })?,
116 };
117
118 let host = hostname.unwrap_or("github.com");
119 let client = Client::new(host).context("failed to create HTTP client")?;
120
121 let mut path = format!("/repos/{}/{}/actions/caches", spec.owner, spec.repo);
122
123 if let Some(k) = key {
124 let _ = write!(path, "?key={k}");
125 } else if let Some(prefix) = key_prefix {
126 let _ = write!(path, "?key={prefix}");
127 }
128
129 if let Some(r) = ref_ {
130 let sep = if path.contains('?') { "&" } else { "?" };
131 let _ = write!(path, "{sep}ref={r}");
132 }
133
134 let response = client
135 .request("DELETE", &path, &[], None)
136 .context("failed to delete caches")?;
137
138 let status = response.status();
139 if !status.is_success() {
140 anyhow::bail!("failed to delete caches: HTTP {status}");
141 }
142
143 let result: serde_json::Value = response.json().context("failed to parse response")?;
144 let count = result["total_count"].as_u64().unwrap_or(0);
145
146 if all {
147 println!("Deleted all caches ({count} total).");
148 } else if let Some(k) = key {
149 println!("Deleted cache '{k}'.");
150 } else if let Some(prefix) = key_prefix {
151 println!("Deleted {count} cache(s) with prefix '{prefix}'.");
152 }
153
154 Ok(())
155}