Skip to main content

spin_plugins/
catalogue.rs

1use crate::{git::GitSource, manifest::PluginManifest};
2use anyhow::Context;
3use semver::Version;
4use std::path::{Path, PathBuf};
5use url::Url;
6
7const SPIN_PLUGINS_REPO: &str = "https://github.com/spinframework/spin-plugins/";
8
9pub(crate) fn plugins_repo_url() -> Result<Url, url::ParseError> {
10    Url::parse(SPIN_PLUGINS_REPO)
11}
12
13/// The local clone of the spin-plugins repo.
14pub struct Catalogue {
15    git_root: PathBuf,
16    manifests_root: PathBuf,
17}
18
19// Name of directory containing the installed manifests
20const LOCAL_CATALOGUE_MANIFESTS_DIRECTORY: &str = "manifests";
21
22impl Catalogue {
23    pub fn new(git_root: PathBuf) -> Self {
24        let manifests_root = git_root.join(LOCAL_CATALOGUE_MANIFESTS_DIRECTORY);
25        Self {
26            git_root,
27            manifests_root,
28        }
29    }
30
31    pub fn manifests(&self) -> anyhow::Result<Vec<PluginManifest>> {
32        // Structure:
33        // CATALOGUE_DIR (spin/plugins/.spin-plugins/manifests)
34        // |- foo
35        // |  |- foo@0.1.2.json
36        // |  |- foo@1.2.3.json
37        // |  |- foo.json
38        // |- bar
39        //    |- bar.json
40        let catalogue_manifests_dir = &self.manifests_root;
41
42        // Catalogue directory doesn't exist so likely nothing has been installed.
43        if !catalogue_manifests_dir.exists() {
44            return Ok(Vec::new());
45        }
46
47        let plugin_dirs = catalogue_manifests_dir
48            .read_dir()
49            .with_context(|| format!("reading manifest catalogue at {catalogue_manifests_dir:?}"))?
50            .filter_map(|d| d.ok())
51            .map(|d| d.path())
52            .filter(|p| p.is_dir());
53        let manifest_paths = plugin_dirs.flat_map(|path| crate::util::json_files_in(&path));
54        let manifests: Vec<_> = manifest_paths
55            .filter_map(|path| crate::util::try_read_manifest_from(&path))
56            .collect();
57        Ok(manifests)
58    }
59
60    /// Get expected path to the manifest of a plugin with a given name
61    /// and version within the spin-plugins repository
62    pub(crate) fn manifest_path(
63        &self,
64        plugin_name: &str,
65        plugin_version: &Option<Version>,
66    ) -> PathBuf {
67        self.manifests_root
68            .join(plugin_name)
69            .join(crate::util::manifest_file_name_version(
70                plugin_name,
71                plugin_version,
72            ))
73    }
74
75    /// Clones or pulls the spin-plugins repo as required. THIS IS NOT SYNCHRONISED
76    /// and should be used only if you know nothing else is updating the working
77    /// copy at the same time: generally, prefer `PluginManager::update()` which
78    /// checks for contention.
79    pub(crate) async fn fetch_from_remote(&self, repo_url: &Url) -> anyhow::Result<()> {
80        let git_root = &self.git_root;
81        let git_source = GitSource::new(repo_url, None, git_root);
82        if accept_as_repo(git_root) {
83            git_source.pull().await?;
84        } else {
85            git_source.clone_repo().await?;
86        }
87        Ok(())
88    }
89
90    pub(crate) async fn ensure_inited(&self, repo_url: &Url) -> anyhow::Result<()> {
91        let git_root = &self.git_root;
92        let git_source = GitSource::new(repo_url, None, git_root);
93        if !accept_as_repo(git_root) {
94            git_source.clone_repo().await?;
95        }
96        Ok(())
97    }
98}
99
100#[cfg(not(test))]
101fn accept_as_repo(git_root: &Path) -> bool {
102    git_root.join(".git").exists()
103}
104
105#[cfg(test)]
106fn accept_as_repo(git_root: &Path) -> bool {
107    git_root.join(".git").exists() || git_root.join("_spin_test_dot_git").exists()
108}