normalize_package_index/
cache.rs1use std::fs;
4use std::io::Read;
5use std::path::PathBuf;
6use std::time::{Duration, SystemTime};
7
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
10pub struct IndexMeta {
11 pub etag: Option<String>,
12 pub last_modified: Option<String>,
13 pub cached_at: u64, pub url: String,
15}
16
17fn cache_base() -> Option<PathBuf> {
19 let base = if let Ok(cache) = std::env::var("XDG_CACHE_HOME") {
20 PathBuf::from(cache)
21 } else if let Ok(home) = std::env::var("HOME") {
22 PathBuf::from(home).join(".cache")
23 } else if let Ok(home) = std::env::var("USERPROFILE") {
24 PathBuf::from(home).join(".cache")
25 } else {
26 return None;
27 };
28 Some(base.join("moss"))
29}
30
31fn index_cache_dir() -> Option<PathBuf> {
33 Some(cache_base()?.join("indices"))
34}
35
36#[allow(dead_code)]
38pub fn index_cache_key(url: &str) -> String {
39 url.chars()
41 .map(|c| match c {
42 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => c,
43 _ => '_',
44 })
45 .collect()
46}
47
48fn index_paths(ecosystem: &str, name: &str) -> Option<(PathBuf, PathBuf)> {
50 let dir = index_cache_dir()?.join(ecosystem);
51 let data_path = dir.join(format!("{}.data", name));
52 let meta_path = dir.join(format!("{}.meta.json", name));
53 Some((data_path, meta_path))
54}
55
56pub fn read_index_meta(ecosystem: &str, name: &str) -> Option<IndexMeta> {
58 let (_, meta_path) = index_paths(ecosystem, name)?;
59 let content = fs::read_to_string(&meta_path).ok()?;
60 serde_json::from_str(&content).ok()
61}
62
63pub fn read_index(ecosystem: &str, name: &str) -> Option<Vec<u8>> {
65 let (data_path, _) = index_paths(ecosystem, name)?;
66 fs::read(&data_path).ok()
67}
68
69pub fn read_index_if_fresh(ecosystem: &str, name: &str, max_age: Duration) -> Option<Vec<u8>> {
71 let meta = read_index_meta(ecosystem, name)?;
72
73 let now = SystemTime::now()
74 .duration_since(SystemTime::UNIX_EPOCH)
75 .ok()?
76 .as_secs();
77
78 if now - meta.cached_at > max_age.as_secs() {
79 return None; }
81
82 read_index(ecosystem, name)
83}
84
85pub fn write_index(
87 ecosystem: &str,
88 name: &str,
89 data: &[u8],
90 url: &str,
91 etag: Option<&str>,
92 last_modified: Option<&str>,
93) {
94 let Some((data_path, meta_path)) = index_paths(ecosystem, name) else {
95 return;
96 };
97
98 if let Some(parent) = data_path.parent() {
100 let _ = fs::create_dir_all(parent);
101 }
102
103 if fs::write(&data_path, data).is_err() {
105 return;
106 }
107
108 let now = SystemTime::now()
110 .duration_since(SystemTime::UNIX_EPOCH)
111 .map(|d| d.as_secs())
112 .unwrap_or(0);
113
114 let meta = IndexMeta {
115 etag: etag.map(String::from),
116 last_modified: last_modified.map(String::from),
117 cached_at: now,
118 url: url.to_string(),
119 };
120
121 if let Ok(json) = serde_json::to_string_pretty(&meta) {
122 let _ = fs::write(&meta_path, json);
123 }
124}
125
126pub fn fetch_with_cache(
129 ecosystem: &str,
130 name: &str,
131 url: &str,
132 max_age: Duration,
133) -> Result<(Vec<u8>, bool), String> {
134 if let Some(data) = read_index_if_fresh(ecosystem, name, max_age) {
136 return Ok((data, true));
137 }
138
139 let meta = read_index_meta(ecosystem, name);
141
142 let mut request = ureq::get(url);
144
145 if let Some(ref m) = meta {
146 if let Some(ref etag) = m.etag {
147 request = request.set("If-None-Match", etag);
148 }
149 if let Some(ref lm) = m.last_modified {
150 request = request.set("If-Modified-Since", lm);
151 }
152 }
153
154 let response = request.call().map_err(|e| e.to_string())?;
155
156 if response.status() == 304 {
158 if let Some(data) = read_index(ecosystem, name) {
159 if let Some(m) = meta {
161 write_index(
162 ecosystem,
163 name,
164 &data,
165 url,
166 m.etag.as_deref(),
167 m.last_modified.as_deref(),
168 );
169 }
170 return Ok((data, true));
171 }
172 }
173
174 let etag = response.header("ETag").map(String::from);
176 let last_modified = response.header("Last-Modified").map(String::from);
177
178 let mut data = Vec::new();
180 response
181 .into_reader()
182 .read_to_end(&mut data)
183 .map_err(|e| e.to_string())?;
184
185 write_index(
187 ecosystem,
188 name,
189 &data,
190 url,
191 etag.as_deref(),
192 last_modified.as_deref(),
193 );
194
195 Ok((data, false))
196}