1use super::outputs::CacheCrateOutput;
7use anyhow::{Context, Result, bail};
8use std::fs;
9use std::path::Path;
10
11pub fn copy_directory_contents(src: &Path, dest: &Path) -> Result<()> {
16 if !src.exists() {
17 bail!("Source directory does not exist: {}", src.display());
18 }
19
20 if !dest.exists() {
21 fs::create_dir_all(dest)
22 .with_context(|| format!("Failed to create directory: {}", dest.display()))?;
23 }
24
25 for entry in
26 fs::read_dir(src).with_context(|| format!("Failed to read directory: {}", src.display()))?
27 {
28 let entry = entry?;
29 let path = entry.path();
30 let name = entry.file_name();
31 let dest_path = dest.join(&name);
32
33 if path.is_dir() {
34 if name == ".git" || name == ".svn" || name == ".hg" || name == "target" {
36 continue;
37 }
38 copy_directory_contents(&path, &dest_path)?;
39 } else {
40 fs::copy(&path, &dest_path).with_context(|| {
41 format!(
42 "Failed to copy file from {} to {}",
43 path.display(),
44 dest_path.display()
45 )
46 })?;
47 }
48 }
49
50 Ok(())
51}
52
53pub fn format_bytes(bytes: u64) -> String {
55 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
56 if bytes == 0 {
57 return "0 B".to_string();
58 }
59
60 let base = 1024_f64;
61 let exponent = (bytes as f64).ln() / base.ln();
62 let exponent = exponent.floor() as usize;
63
64 let unit = UNITS.get(exponent).unwrap_or(&"TB");
65 let size = bytes as f64 / base.powi(exponent as i32);
66
67 if size.fract() == 0.0 {
68 format!("{size:.0} {unit}")
69 } else {
70 format!("{size:.2} {unit}")
71 }
72}
73
74pub type CacheResponse = CacheCrateOutput;
76
77impl CacheResponse {
78 pub fn success(crate_name: impl Into<String>, version: impl Into<String>) -> Self {
80 let crate_name = crate_name.into();
81 let version = version.into();
82 Self::Success {
83 message: format!("Successfully cached {crate_name}-{version}"),
84 crate_name,
85 version,
86 members: None,
87 results: None,
88 updated: None,
89 }
90 }
91
92 pub fn success_updated(crate_name: impl Into<String>, version: impl Into<String>) -> Self {
94 let crate_name = crate_name.into();
95 let version = version.into();
96 Self::Success {
97 message: format!("Successfully updated {crate_name}-{version}"),
98 crate_name,
99 version,
100 members: None,
101 results: None,
102 updated: Some(true),
103 }
104 }
105
106 pub fn members_success(
108 crate_name: impl Into<String>,
109 version: impl Into<String>,
110 members: Vec<String>,
111 results: Vec<String>,
112 updated: bool,
113 ) -> Self {
114 let count = results.len();
115 let message = if updated {
116 format!("Successfully updated {count} workspace members")
117 } else {
118 format!("Successfully cached {count} workspace members")
119 };
120
121 Self::Success {
122 message,
123 crate_name: crate_name.into(),
124 version: version.into(),
125 members: Some(members),
126 results: Some(results),
127 updated: if updated { Some(true) } else { None },
128 }
129 }
130
131 pub fn members_partial(
133 crate_name: impl Into<String>,
134 version: impl Into<String>,
135 members: Vec<String>,
136 results: Vec<String>,
137 errors: Vec<String>,
138 updated: bool,
139 ) -> Self {
140 let message = if updated {
141 format!(
142 "Updated {} members with {} errors",
143 results.len(),
144 errors.len()
145 )
146 } else {
147 format!(
148 "Cached {} members with {} errors",
149 results.len(),
150 errors.len()
151 )
152 };
153
154 Self::PartialSuccess {
155 message,
156 crate_name: crate_name.into(),
157 version: version.into(),
158 members,
159 results,
160 errors,
161 updated: if updated { Some(true) } else { None },
162 }
163 }
164
165 pub fn workspace_detected(
167 crate_name: impl Into<String>,
168 version: impl Into<String>,
169 members: Vec<String>,
170 source_type: &str,
171 updated: bool,
172 ) -> Self {
173 let crate_name = crate_name.into();
174 let version = version.into();
175 let example_members = members.get(0..2.min(members.len())).unwrap_or(&[]).to_vec();
176
177 Self::WorkspaceDetected {
178 message: "This is a workspace crate. Please specify which members to cache using the 'members' parameter.".to_string(),
179 crate_name: crate_name.clone(),
180 version: version.clone(),
181 workspace_members: members,
182 example_usage: format!(
183 "cache_crate_from_{source_type}(crate_name=\"{crate_name}\", version=\"{version}\", members={example_members:?})"
184 ),
185 updated: if updated { Some(true) } else { None },
186 }
187 }
188
189 pub fn error(message: impl Into<String>) -> Self {
191 Self::Error {
192 error: message.into(),
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use tempfile::TempDir;
201
202 #[test]
203 fn test_format_bytes() {
204 assert_eq!(format_bytes(0), "0 B");
205 assert_eq!(format_bytes(512), "512 B");
206 assert_eq!(format_bytes(1024), "1 KB");
207 assert_eq!(format_bytes(1536), "1.50 KB");
208 assert_eq!(format_bytes(1048576), "1 MB");
209 assert_eq!(format_bytes(1073741824), "1 GB");
210 }
211
212 #[test]
213 fn test_copy_directory_contents() -> Result<()> {
214 let temp_dir = TempDir::new()?;
215 let src_dir = temp_dir.path().join("src");
216 let dest_dir = temp_dir.path().join("dest");
217
218 fs::create_dir_all(&src_dir)?;
220 fs::write(src_dir.join("file1.txt"), "content1")?;
221
222 let sub_dir = src_dir.join("subdir");
223 fs::create_dir_all(&sub_dir)?;
224 fs::write(sub_dir.join("file2.txt"), "content2")?;
225
226 let git_dir = src_dir.join(".git");
228 fs::create_dir_all(&git_dir)?;
229 fs::write(git_dir.join("config"), "git config")?;
230
231 copy_directory_contents(&src_dir, &dest_dir)?;
233
234 assert!(dest_dir.join("file1.txt").exists());
236 assert!(dest_dir.join("subdir").exists());
237 assert!(dest_dir.join("subdir/file2.txt").exists());
238 assert!(!dest_dir.join(".git").exists());
239
240 Ok(())
241 }
242
243 #[test]
244 fn test_cache_response() {
245 let response = CacheResponse::success("test-crate", "1.0.0");
247 let json_str = response.to_json();
248 let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
249 assert_eq!(json["status"], "success");
250 assert_eq!(json["message"], "Successfully cached test-crate-1.0.0");
251 assert_eq!(json["crate"], "test-crate");
252 assert_eq!(json["version"], "1.0.0");
253
254 let error = CacheResponse::error("Something went wrong");
256 let json_str = error.to_json();
257 let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
258 assert_eq!(json["status"], "error");
259 assert_eq!(json["error"], "Something went wrong");
260
261 let workspace = CacheResponse::workspace_detected(
263 "test-crate",
264 "1.0.0",
265 vec!["crate-a".to_string(), "crate-b".to_string()],
266 "cratesio",
267 false,
268 );
269 let json_str = workspace.to_json();
270 let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
271 assert_eq!(json["status"], "workspace_detected");
272 assert_eq!(json["crate"], "test-crate");
273 assert_eq!(
274 json["workspace_members"],
275 serde_json::json!(["crate-a", "crate-b"])
276 );
277
278 let members = CacheResponse::members_success(
280 "test-crate",
281 "1.0.0",
282 vec!["member1".to_string()],
283 vec!["Successfully cached member: member1".to_string()],
284 false,
285 );
286 let json_str = members.to_json();
287 let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
288 assert_eq!(json["status"], "success");
289 assert!(json.get("updated").is_none());
290
291 let members_updated = CacheResponse::members_success(
293 "test-crate",
294 "1.0.0",
295 vec!["member1".to_string()],
296 vec!["Successfully cached member: member1".to_string()],
297 true,
298 );
299 let json_str = members_updated.to_json();
300 let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
301 assert!(json["updated"].as_bool().unwrap());
302 }
303}