Skip to main content

pkg/
repo_manager.rs

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};
17/// Remote package management
18pub struct RepoManager {
19    /// http sources
20    pub remotes: Vec<RemoteName>,
21    /// file sources
22    pub locals: Vec<RemoteName>,
23    /// detailed http + file sources
24    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
55/// same as pkgar_core::PublicKey
56pub type RepoPublicKey = [u8; 32];
57
58#[derive(Clone, Debug, Deserialize, Serialize)]
59
60/// same as pkgar_keys::PublicKeyFile
61pub 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    /// URL/Path to packages
89    pub path: String,
90    /// URL to public key
91    pub pubpath: String,
92    /// Unique ID
93    pub name: RemoteName,
94    /// Embedded public key, lazily loaded
95    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    /// override from default
122    pub fn set_download_path(&mut self, path: PathBuf) {
123        self.download_path = path;
124    }
125
126    /// override from existing callback
127    pub fn set_callback(&mut self, callback: Rc<RefCell<dyn Callback>>) {
128        self.callback = callback;
129    }
130
131    /// read [install_path]/etc/pkg.d with specified target. Will reset existing remotes / locals list.
132    pub fn update_remotes(&mut self, target: &str, install_path: &Path) -> Result<(), Error> {
133        self.remotes = Vec::new();
134        self.locals = Vec::new();
135        self.remote_map = BTreeMap::new();
136
137        let repos_path = install_path.join(PACKAGES_REMOTE_DIR);
138        let mut repo_files = Vec::new();
139        for entry_res in fs::read_dir(&repos_path)? {
140            let entry = entry_res?;
141            let path = entry.path();
142            if path.is_file() {
143                repo_files.push(path);
144            }
145        }
146        repo_files.sort();
147        for repo_file in repo_files {
148            let data = fs::read_to_string(repo_file)?;
149            for line in data.lines() {
150                if !line.starts_with('#') {
151                    self.add_remote(line.trim(), target)?;
152                }
153            }
154        }
155        // optional local path
156        let local_pub_path = install_path.join("pkg");
157        let _ = self.add_local("installer_key", "", target, &local_pub_path);
158        Ok(())
159    }
160
161    fn extract_host(path: &str) -> Option<&str> {
162        path.split("://")
163            .nth(1)?
164            .split('/')
165            .next()?
166            .split(':')
167            .next()
168    }
169
170    /// Add a remote target. The domain url will be used as a host (unique identifier).
171    pub fn add_remote(&mut self, url: &str, target: &str) -> Result<(), Error> {
172        let host = Self::extract_host(url)
173            .ok_or_else(|| Error::RepoPathInvalid(url.into()))?
174            .to_string();
175
176        if self
177            .remote_map
178            .insert(
179                host.clone(),
180                RemotePath {
181                    path: format!("{}/{}", url, target),
182                    pubpath: format!("{}/{}", url, PUB_TOML),
183                    name: host.clone(),
184                    pubkey: None,
185                },
186            )
187            .is_none()
188        {
189            self.remotes.push(host);
190        };
191
192        Ok(())
193    }
194
195    /// Add a local directory target. Specify a host as a unique identifier.
196    pub fn add_local(
197        &mut self,
198        host: &str,
199        path: &str,
200        target: &str,
201        pubkey_dir: &Path,
202    ) -> Result<(), Error> {
203        let pubkey_path = pubkey_dir.join(PUB_TOML);
204        if !pubkey_path.is_file() {
205            return Err(Error::RepoPathInvalid(
206                pubkey_path.to_string_lossy().to_string(),
207            ));
208        }
209        // load to check for failure early
210        let pubkey = RepoPublicKeyFile::open(pubkey_path).map_err(Error::from)?;
211        if self
212            .remote_map
213            .insert(
214                host.into(),
215                RemotePath {
216                    path: if path.is_empty() {
217                        path.into()
218                    } else {
219                        format!("{}/{}", path, target)
220                    },
221                    // signifies local repository
222                    pubpath: "".into(),
223                    name: host.into(),
224                    pubkey: Some(pubkey.pkey),
225                },
226            )
227            .is_none()
228        {
229            self.locals.push(host.into());
230        };
231        Ok(())
232    }
233
234    /// Download a toml file. Wrapper to local_search() + download().
235    fn sync_toml(&self, package_name: &PackageName) -> Result<(String, RemoteName), Error> {
236        let file = format!("{package_name}.toml");
237        if let Some((r, path)) = self.local_search(&file)? {
238            let toml = fs::read_to_string(path)?;
239            return Ok((toml, r));
240        }
241        let mut writer = DownloadBackendWriter::ToBuf(Vec::new());
242        match self.download(&file, None, &mut writer) {
243            Ok(r) => {
244                let text = writer.to_inner_buf();
245                let toml = String::from_utf8(text)
246                    .map_err(|_| Error::ContentIsNotValidUnicode(file.into()))?;
247                Ok((toml, r))
248            }
249            Err(Error::ValidRepoNotFound) => {
250                Err(PackageError::PackageNotFound(package_name.to_owned()).into())
251            }
252            Err(e) => Err(e),
253        }
254    }
255
256    /// Download a pkgar file to specified path. Wrapper to local_search() + download().
257    fn sync_pkgar(
258        &self,
259        package_name: &PackageName,
260        len_hint: u64,
261        dst_path: PathBuf,
262    ) -> Result<(PathBuf, RemoteName), Error> {
263        let file = format!("{package_name}.pkgar");
264        if let Some((r, path)) = self.local_search(&file)? {
265            return Ok((path, r));
266        }
267        let mut writer = DownloadBackendWriter::ToFile(File::create(&dst_path)?);
268        match self.download(&file, Some(len_hint), &mut writer) {
269            Ok(r) => Ok((dst_path, r)),
270            Err(Error::ValidRepoNotFound) => {
271                Err(PackageError::PackageNotFound(package_name.to_owned()).into())
272            }
273            Err(e) => Err(e),
274        }
275    }
276
277    pub fn get_local_path(&self, remote: &RemoteName, file: &str, ext: &str) -> PathBuf {
278        self.download_path.join(format!("{}_{file}.{ext}", remote))
279    }
280
281    /// Downloads all keys
282    pub fn sync_keys(&mut self) -> Result<(), Error> {
283        let download_dir = &self.download_path;
284        if !download_dir.is_dir() {
285            fs::create_dir_all(download_dir)?;
286        }
287        for (_, remote) in self.remote_map.iter_mut() {
288            if remote.pubkey.is_some() {
289                continue;
290            }
291            // download key if not exists
292            if remote.pubkey.is_none() {
293                let local_keypath = download_dir.join(format!("pub_key_{}.toml", remote.name));
294                if !local_keypath.exists() {
295                    self.download_backend.download_to_file(
296                        &remote.pubpath,
297                        None,
298                        &local_keypath,
299                        self.callback.clone(),
300                    )?;
301                }
302                let pubkey = RepoPublicKeyFile::open(local_keypath)?;
303                remote.pubkey = Some(pubkey.pkey);
304            }
305        }
306
307        Ok(())
308    }
309
310    /// Download to dest and report which remotes it's downloaded from.
311    pub fn download(
312        &self,
313        file: &str,
314        len: Option<u64>,
315        mut dest: &mut DownloadBackendWriter,
316    ) -> Result<RemoteName, Error> {
317        if !self.download_path.exists() {
318            fs::create_dir_all(self.download_path.clone())?;
319        }
320
321        for rname in self.remotes.iter() {
322            let Some(remote) = self.remote_map.get(rname) else {
323                continue;
324            };
325            if remote.path == "" {
326                // installer repository
327                continue;
328            }
329
330            let remote_path = format!("{}/{}", remote.path, file);
331            let res =
332                self.download_backend
333                    .download(&remote_path, len, &mut dest, self.callback.clone());
334            match res {
335                Ok(_) => return Ok(rname.into()),
336                #[cfg(feature = "library")]
337                Err(DownloadError::HttpStatus(_)) => continue,
338                Err(e) => {
339                    return Err(Error::Download(e));
340                }
341            };
342        }
343
344        Err(Error::ValidRepoNotFound)
345    }
346
347    /// Locate and return path and report which locals it's downloaded from.
348    pub fn local_search(&self, file: &str) -> Result<Option<(RemoteName, PathBuf)>, Error> {
349        if !self.download_path.exists() {
350            fs::create_dir_all(self.download_path.clone())?;
351        }
352
353        for rname in self.locals.iter() {
354            let Some(remote) = self.remote_map.get(rname) else {
355                continue;
356            };
357            if remote.path == "" {
358                // installer repository
359                continue;
360            }
361
362            let remote_path = Path::new(&remote.path).join(file);
363            match remote_path.metadata() {
364                Ok(e) => {
365                    if e.is_file() {
366                        return Ok(Some((rname.into(), remote_path)));
367                    } else {
368                        continue;
369                    }
370                }
371                Err(err) => {
372                    if err.kind() == std::io::ErrorKind::NotFound {
373                        continue;
374                    } else {
375                        return Err(Error::IO(err));
376                    }
377                }
378            }
379        }
380
381        Ok(None)
382    }
383
384    /// Download a pkgar file to the download path. Wrapper to sync_pkgar().
385    pub fn get_package_pkgar(
386        &self,
387        package: &PackageName,
388        len_hint: u64,
389    ) -> Result<(PathBuf, &RemotePath), Error> {
390        let local_path = self.get_local_path(&"".to_string(), package.as_str(), "pkgar");
391        let (local_path, remote) = self.sync_pkgar(&package, len_hint, local_path)?;
392        if let Some(r) = self.remote_map.get(&remote) {
393            if r.is_local() {
394                return Ok((local_path, r));
395            }
396            let new_local_path = self.get_local_path(&r.name, package.as_str(), "pkgar");
397            if new_local_path != local_path {
398                fs::rename(&local_path, &new_local_path)?;
399            }
400            Ok((new_local_path, r))
401        } else {
402            // the pubkey cache is failing to download?
403            Err(Error::RepoCacheNotFound(package.clone()))
404        }
405    }
406
407    /// Fetch a toml file. Wrapper to sync_toml() with notifies fetch callback.
408    pub fn get_package_toml(&self, package: &PackageName) -> Result<(String, RemoteName), Error> {
409        self.callback.borrow_mut().fetch_package_name(&package);
410        self.sync_toml(package)
411    }
412}