libwally/
package_index.rs1use std::collections::HashMap;
2use std::io::{BufReader, ErrorKind, Write};
3use std::path::PathBuf;
4use std::sync::{Arc, Mutex};
5
6use anyhow::{anyhow, Context};
7use fs_err::{create_dir_all, File, OpenOptions};
8use git2::Repository;
9use serde::{Deserialize, Serialize};
10use tempfile::TempDir;
11use url::Url;
12
13use crate::git_util;
14use crate::manifest::Manifest;
15use crate::package_name::PackageName;
16
17#[derive(Debug, Serialize, Deserialize)]
19pub struct PackageIndexConfig {
20 pub api: Url,
21 pub github_oauth_id: Option<String>,
22
23 #[serde(default)]
24 pub fallback_registries: Vec<String>,
25}
26
27pub struct PackageIndex {
28 url: Url,
30
31 path: PathBuf,
33
34 repository: Mutex<Repository>,
37
38 package_cache: Mutex<HashMap<PackageName, Arc<PackageMetadata>>>,
41
42 access_token: Option<String>,
45
46 #[allow(unused)]
50 temp_dir: Option<TempDir>,
51}
52
53impl PackageIndex {
54 pub fn new(index_url: &Url, access_token: Option<String>) -> anyhow::Result<Self> {
55 let path = index_path(index_url)?;
56 let repository = git_util::open_or_clone(access_token.clone(), index_url, &path)?;
57
58 let index = Self {
59 url: index_url.clone(),
60 path,
61 repository: Mutex::new(repository),
62 package_cache: Mutex::new(HashMap::new()),
63 access_token,
64 temp_dir: None,
65 };
66
67 index.update()?;
68 Ok(index)
69 }
70
71 pub fn url(&self) -> &Url {
72 &self.url
73 }
74
75 pub fn path(&self) -> &PathBuf {
76 &self.path
77 }
78
79 pub fn new_temp(index_url: &Url, access_token: Option<String>) -> anyhow::Result<Self> {
80 let temp_dir = tempfile::tempdir()?;
81 let path = temp_dir.path().to_owned();
82 let repository = git_util::open_or_clone(access_token.clone(), index_url, &path)?;
83
84 let index = Self {
85 url: index_url.clone(),
86 path,
87 repository: Mutex::new(repository),
88 package_cache: Mutex::new(HashMap::new()),
89 access_token,
90 temp_dir: Some(temp_dir),
91 };
92
93 index.update()?;
94 Ok(index)
95 }
96
97 pub fn update(&self) -> anyhow::Result<()> {
98 let repository = self.repository.lock().unwrap();
99
100 log::info!(
101 "Updating package index {}...",
102 repository.find_remote("origin")?.url().unwrap()
103 );
104 git_util::update_index(self.access_token.clone(), &repository)
105 .with_context(|| format!("could not update package index"))?;
106
107 Ok(())
108 }
109
110 pub fn config(&self) -> anyhow::Result<PackageIndexConfig> {
111 let config_path = self.path.join("config.json");
112 let contents = fs_err::read_to_string(config_path)?;
113 Ok(serde_json::from_str(&contents)?)
114 }
115
116 pub fn publish(&self, manifest: &Manifest) -> anyhow::Result<()> {
123 let repo = self.repository.lock().unwrap();
124 let package_path = self.package_path(&manifest.package.name);
125
126 create_dir_all(package_path.parent().unwrap())?;
128
129 {
130 let mut file = OpenOptions::new()
131 .append(true)
132 .create(true)
133 .open(&package_path)?;
134
135 let mut entry = serde_json::to_string(&manifest)?;
138 entry.push('\n');
139 file.write_all(entry.as_bytes())?;
140 }
141
142 git_util::commit_and_push(
143 &repo,
144 self.access_token.clone(),
145 &format!("Publish {}", manifest.package_id()),
146 &self.path,
147 &package_path,
148 )?;
149
150 let mut package_cache = self.package_cache.lock().unwrap();
153 package_cache.remove(&manifest.package.name);
154
155 Ok(())
156 }
157
158 pub fn get_package_metadata(&self, name: &PackageName) -> anyhow::Result<Arc<PackageMetadata>> {
160 let mut package_cache = self.package_cache.lock().unwrap();
161
162 if package_cache.contains_key(name) {
163 Ok(Arc::clone(&package_cache[name]))
164 } else {
165 let package_path = self.package_path(name);
166
167 let file = File::open(&package_path)
172 .with_context(|| format!("could not open package {} from index", name))?;
173 let file = BufReader::new(file);
174
175 let manifest_stream: Result<Vec<Manifest>, serde_json::Error> =
180 serde_json::Deserializer::from_reader(file)
181 .into_iter::<Manifest>()
182 .collect();
183
184 let mut versions = manifest_stream
185 .with_context(|| format!("could not parse package index entry for {}", name))?;
186
187 versions.sort_by(|a, b| b.package.version.cmp(&a.package.version));
188
189 let metadata = Arc::new(PackageMetadata { versions });
190 package_cache.insert(name.clone(), Arc::clone(&metadata));
191
192 Ok(metadata)
193 }
194 }
195
196 pub fn get_scope_owners(&self, scope: &str) -> anyhow::Result<Vec<u64>> {
198 let mut path = self.path.clone();
199 path.push(scope);
200 path.push("owners.json");
201
202 match File::open(path) {
203 Ok(file) => serde_json::from_reader(file)
204 .with_context(|| format!("could not parse owner file for scope {}", scope)),
205
206 Err(error) => match error.kind() {
207 ErrorKind::NotFound => Ok(Vec::new()),
208 _ => Err(error)
209 .with_context(|| format!("failed to read owner file for scope {}", scope)),
210 },
211 }
212 }
213
214 pub fn is_scope_owner(&self, scope: &str, user_id: &u64) -> anyhow::Result<bool> {
216 let owners = self.get_scope_owners(scope)?;
217 Ok(owners.iter().any(|owner| owner == user_id))
218 }
219
220 pub fn add_scope_owner(&self, scope: &str, owner_id: &u64) -> anyhow::Result<()> {
224 let repo = self.repository.lock().unwrap();
225 let mut path = self.path.clone();
226
227 path.push(scope);
229 create_dir_all(&path)?;
230 path.push("owners.json");
231
232 {
233 let mut owners = self.get_scope_owners(&scope)?;
234 let mut file = OpenOptions::new().write(true).create(true).open(&path)?;
235
236 owners.push(*owner_id);
237 file.write_all(serde_json::to_string(&owners)?.as_bytes())?;
238 }
239
240 git_util::commit_and_push(
241 &repo,
242 self.access_token.clone(),
243 &format!("Add owner for {}/*", scope),
244 &self.path,
245 &path,
246 )?;
247
248 Ok(())
249 }
250
251 fn package_path(&self, name: &PackageName) -> PathBuf {
252 let mut package_path = self.path.clone();
255 package_path.push(name.scope());
256 package_path.push(name.name());
257 package_path
258 }
259}
260
261#[derive(Default, Serialize)]
262pub struct PackageMetadata {
263 pub versions: Vec<Manifest>,
264}
265
266fn index_path(index_url: &Url) -> anyhow::Result<PathBuf> {
267 let registry_name = match (index_url.domain(), index_url.scheme()) {
268 (Some(domain), _) => domain,
269 (None, "file") => "local-registry",
270 _ => "unknown",
271 };
272
273 let hash = blake3::hash(index_url.to_string().as_bytes());
274 let hash_hex = hex::encode(&hash.as_bytes()[..8]);
275 let ident = format!("{}-{}", registry_name, hash_hex);
276
277 let path = dirs::cache_dir()
278 .ok_or_else(|| anyhow!("could not find cache directory"))?
279 .join("wally")
280 .join("index")
281 .join(ident);
282
283 Ok(path)
284}