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    /// read [install_path]/etc/pkg.d with specified target. Will reset existing remotes / locals list.
127    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        // optional local path
151        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    /// Add a remote target. The domain url will be used as a host (unique identifier).
166    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    /// Add a local directory target. Specify a host as a unique identifier.
191    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        // load to check for failure early
205        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                    // signifies local repository
217                    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    /// Download a toml file. Wrapper to local_search() + download().
230    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    /// Download a pkgar file to specified path. Wrapper to local_search() + download().
252    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    /// Downloads all keys
277    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            // download key if not exists
287            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    /// Download to dest and report which remotes it's downloaded from.
306    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                // installer repository
322                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    /// Locate and return path and report which locals it's downloaded from.
343    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                // installer repository
354                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    /// Download a pkgar file to the download path. Wrapper to sync_pkgar().
380    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            // the pubkey cache is failing to download?
398            Err(Error::RepoCacheNotFound(package.clone()))
399        }
400    }
401
402    /// Fetch a toml file. Wrapper to sync_toml() with notifies fetch callback.
403    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}