1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::fmt::Debug;
4use std::fs::File;
5use std::path::Path;
6use std::rc::Rc;
7use std::{fs, path::PathBuf};
8
9use crate::callback::Callback;
10#[cfg(feature = "library")]
11use crate::net_backend::DownloadError;
12use crate::net_backend::{DownloadBackend, DownloadBackendWriter};
13use crate::package::RemoteName;
14use crate::{backend::Error, package::PackageError, PackageName};
15use crate::{DOWNLOAD_DIR, PACKAGES_REMOTE_DIR};
16use serde_derive::{Deserialize, Serialize};
17pub struct RepoManager {
19 pub remotes: Vec<RemoteName>,
21 pub locals: Vec<RemoteName>,
23 pub remote_map: BTreeMap<RemoteName, RemotePath>,
25 pub download_path: PathBuf,
26 pub download_backend: Rc<Box<dyn DownloadBackend>>,
27
28 pub callback: Rc<RefCell<dyn Callback>>,
29}
30
31impl Debug for RepoManager {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("RepoManager")
34 .field("remotes", &self.remotes)
35 .field("locals", &self.locals)
36 .field("remote_map", &self.remote_map)
37 .field("download_path", &self.download_path)
38 .finish()
39 }
40}
41
42impl Clone for RepoManager {
43 fn clone(&self) -> Self {
44 Self {
45 remotes: self.remotes.clone(),
46 locals: self.locals.clone(),
47 remote_map: self.remote_map.clone(),
48 download_path: self.download_path.clone(),
49 download_backend: self.download_backend.clone(),
50 callback: self.callback.clone(),
51 }
52 }
53}
54
55pub type RepoPublicKey = [u8; 32];
57
58#[derive(Clone, Debug, Deserialize, Serialize)]
59
60pub struct RepoPublicKeyFile {
62 #[serde(
63 serialize_with = "hex::serialize",
64 deserialize_with = "hex::deserialize"
65 )]
66 pub pkey: RepoPublicKey,
67}
68
69impl RepoPublicKeyFile {
70 pub fn new(pubkey: RepoPublicKey) -> Self {
71 Self { pkey: pubkey }
72 }
73
74 pub fn open(file: impl AsRef<Path>) -> Result<RepoPublicKeyFile, Error> {
75 let content = fs::read_to_string(file.as_ref()).map_err(Error::IO)?;
76 toml::from_str(&content).map_err(|_| {
77 Error::ContentIsNotValidUnicode(file.as_ref().to_string_lossy().to_string())
78 })
79 }
80
81 pub fn save(&self, file: impl AsRef<Path>) -> Result<(), Error> {
82 fs::write(file, toml::to_string(&self).unwrap()).map_err(Error::IO)
83 }
84}
85
86#[derive(Clone, Debug)]
87pub struct RemotePath {
88 pub path: String,
90 pub pubpath: String,
92 pub name: RemoteName,
94 pub pubkey: Option<RepoPublicKey>,
96}
97
98impl RemotePath {
99 pub fn is_local(&self) -> bool {
100 self.pubpath.is_empty()
101 }
102}
103
104const PUB_TOML: &str = "id_ed25519.pub.toml";
105
106impl RepoManager {
107 pub fn new(
108 callback: Rc<RefCell<dyn Callback>>,
109 download_backend: Box<dyn DownloadBackend>,
110 ) -> Self {
111 Self {
112 remotes: Vec::new(),
113 locals: Vec::new(),
114 download_path: DOWNLOAD_DIR.into(),
115 download_backend: Rc::new(download_backend),
116 callback: callback,
117 remote_map: BTreeMap::new(),
118 }
119 }
120
121 pub fn set_download_path(&mut self, path: PathBuf) {
123 self.download_path = path;
124 }
125
126 pub fn update_remotes(&mut self, target: &str, install_path: &Path) -> Result<(), Error> {
128 self.remotes = Vec::new();
129 self.locals = Vec::new();
130 self.remote_map = BTreeMap::new();
131
132 let repos_path = install_path.join(PACKAGES_REMOTE_DIR);
133 let mut repo_files = Vec::new();
134 for entry_res in fs::read_dir(&repos_path)? {
135 let entry = entry_res?;
136 let path = entry.path();
137 if path.is_file() {
138 repo_files.push(path);
139 }
140 }
141 repo_files.sort();
142 for repo_file in repo_files {
143 let data = fs::read_to_string(repo_file)?;
144 for line in data.lines() {
145 if !line.starts_with('#') {
146 self.add_remote(line.trim(), target)?;
147 }
148 }
149 }
150 let local_pub_path = install_path.join("pkg");
152 let _ = self.add_local("installer_key", "", target, &local_pub_path);
153 Ok(())
154 }
155
156 fn extract_host(path: &str) -> Option<&str> {
157 path.split("://")
158 .nth(1)?
159 .split('/')
160 .next()?
161 .split(':')
162 .next()
163 }
164
165 pub fn add_remote(&mut self, url: &str, target: &str) -> Result<(), Error> {
167 let host = Self::extract_host(url)
168 .ok_or_else(|| Error::RepoPathInvalid(url.into()))?
169 .to_string();
170
171 if self
172 .remote_map
173 .insert(
174 host.clone(),
175 RemotePath {
176 path: format!("{}/{}", url, target),
177 pubpath: format!("{}/{}", url, PUB_TOML),
178 name: host.clone(),
179 pubkey: None,
180 },
181 )
182 .is_none()
183 {
184 self.remotes.push(host);
185 };
186
187 Ok(())
188 }
189
190 pub fn add_local(
192 &mut self,
193 host: &str,
194 path: &str,
195 target: &str,
196 pubkey_dir: &Path,
197 ) -> Result<(), Error> {
198 let pubkey_path = pubkey_dir.join(PUB_TOML);
199 if !pubkey_path.is_file() {
200 return Err(Error::RepoPathInvalid(
201 pubkey_path.to_string_lossy().to_string(),
202 ));
203 }
204 let pubkey = RepoPublicKeyFile::open(pubkey_path).map_err(Error::from)?;
206 if self
207 .remote_map
208 .insert(
209 host.into(),
210 RemotePath {
211 path: if path.is_empty() {
212 path.into()
213 } else {
214 format!("{}/{}", path, target)
215 },
216 pubpath: "".into(),
218 name: host.into(),
219 pubkey: Some(pubkey.pkey),
220 },
221 )
222 .is_none()
223 {
224 self.locals.push(host.into());
225 };
226 Ok(())
227 }
228
229 fn sync_toml(&self, package_name: &PackageName) -> Result<(String, RemoteName), Error> {
231 let file = format!("{package_name}.toml");
232 if let Some((r, path)) = self.local_search(&file)? {
233 let toml = fs::read_to_string(path)?;
234 return Ok((toml, r));
235 }
236 let mut writer = DownloadBackendWriter::ToBuf(Vec::new());
237 match self.download(&file, None, &mut writer) {
238 Ok(r) => {
239 let text = writer.to_inner_buf();
240 let toml = String::from_utf8(text)
241 .map_err(|_| Error::ContentIsNotValidUnicode(file.into()))?;
242 Ok((toml, r))
243 }
244 Err(Error::ValidRepoNotFound) => {
245 Err(PackageError::PackageNotFound(package_name.to_owned()).into())
246 }
247 Err(e) => Err(e),
248 }
249 }
250
251 fn sync_pkgar(
253 &self,
254 package_name: &PackageName,
255 len_hint: u64,
256 dst_path: PathBuf,
257 ) -> Result<(PathBuf, RemoteName), Error> {
258 let file = format!("{package_name}.pkgar");
259 if let Some((r, path)) = self.local_search(&file)? {
260 return Ok((path, r));
261 }
262 let mut writer = DownloadBackendWriter::ToFile(File::create(&dst_path)?);
263 match self.download(&file, Some(len_hint), &mut writer) {
264 Ok(r) => Ok((dst_path, r)),
265 Err(Error::ValidRepoNotFound) => {
266 Err(PackageError::PackageNotFound(package_name.to_owned()).into())
267 }
268 Err(e) => Err(e),
269 }
270 }
271
272 pub fn get_local_path(&self, remote: &RemoteName, file: &str, ext: &str) -> PathBuf {
273 self.download_path.join(format!("{}_{file}.{ext}", remote))
274 }
275
276 pub fn sync_keys(&mut self) -> Result<(), Error> {
278 let download_dir = &self.download_path;
279 if !download_dir.is_dir() {
280 fs::create_dir_all(download_dir)?;
281 }
282 for (_, remote) in self.remote_map.iter_mut() {
283 if remote.pubkey.is_some() {
284 continue;
285 }
286 if remote.pubkey.is_none() {
288 let local_keypath = download_dir.join(format!("pub_key_{}.toml", remote.name));
289 if !local_keypath.exists() {
290 self.download_backend.download_to_file(
291 &remote.pubpath,
292 None,
293 &local_keypath,
294 self.callback.clone(),
295 )?;
296 }
297 let pubkey = RepoPublicKeyFile::open(local_keypath)?;
298 remote.pubkey = Some(pubkey.pkey);
299 }
300 }
301
302 Ok(())
303 }
304
305 pub fn download(
307 &self,
308 file: &str,
309 len: Option<u64>,
310 mut dest: &mut DownloadBackendWriter,
311 ) -> Result<RemoteName, Error> {
312 if !self.download_path.exists() {
313 fs::create_dir_all(self.download_path.clone())?;
314 }
315
316 for rname in self.remotes.iter() {
317 let Some(remote) = self.remote_map.get(rname) else {
318 continue;
319 };
320 if remote.path == "" {
321 continue;
323 }
324
325 let remote_path = format!("{}/{}", remote.path, file);
326 let res =
327 self.download_backend
328 .download(&remote_path, len, &mut dest, self.callback.clone());
329 match res {
330 Ok(_) => return Ok(rname.into()),
331 #[cfg(feature = "library")]
332 Err(DownloadError::HttpStatus(_)) => continue,
333 Err(e) => {
334 return Err(Error::Download(e));
335 }
336 };
337 }
338
339 Err(Error::ValidRepoNotFound)
340 }
341
342 pub fn local_search(&self, file: &str) -> Result<Option<(RemoteName, PathBuf)>, Error> {
344 if !self.download_path.exists() {
345 fs::create_dir_all(self.download_path.clone())?;
346 }
347
348 for rname in self.locals.iter() {
349 let Some(remote) = self.remote_map.get(rname) else {
350 continue;
351 };
352 if remote.path == "" {
353 continue;
355 }
356
357 let remote_path = Path::new(&remote.path).join(file);
358 match remote_path.metadata() {
359 Ok(e) => {
360 if e.is_file() {
361 return Ok(Some((rname.into(), remote_path)));
362 } else {
363 continue;
364 }
365 }
366 Err(err) => {
367 if err.kind() == std::io::ErrorKind::NotFound {
368 continue;
369 } else {
370 return Err(Error::IO(err));
371 }
372 }
373 }
374 }
375
376 Ok(None)
377 }
378
379 pub fn get_package_pkgar(
381 &self,
382 package: &PackageName,
383 len_hint: u64,
384 ) -> Result<(PathBuf, &RemotePath), Error> {
385 let local_path = self.get_local_path(&"".to_string(), package.as_str(), "pkgar");
386 let (local_path, remote) = self.sync_pkgar(&package, len_hint, local_path)?;
387 if let Some(r) = self.remote_map.get(&remote) {
388 if r.is_local() {
389 return Ok((local_path, r));
390 }
391 let new_local_path = self.get_local_path(&r.name, package.as_str(), "pkgar");
392 if new_local_path != local_path {
393 fs::rename(&local_path, &new_local_path)?;
394 }
395 Ok((new_local_path, r))
396 } else {
397 Err(Error::RepoCacheNotFound(package.clone()))
399 }
400 }
401
402 pub fn get_package_toml(&self, package: &PackageName) -> Result<(String, RemoteName), Error> {
404 self.callback.borrow_mut().fetch_package_name(&package);
405 self.sync_toml(package)
406 }
407}