spin_plugins/
store.rs

1use anyhow::{Context, Result};
2use flate2::read::GzDecoder;
3use spin_common::data_dir::data_dir;
4use std::{
5    ffi::OsStr,
6    fs::{self, File},
7    path::{Path, PathBuf},
8};
9use tar::Archive;
10
11use crate::{error::*, manifest::PluginManifest};
12
13/// Directory where the manifests of installed plugins are stored.
14pub const PLUGIN_MANIFESTS_DIRECTORY_NAME: &str = "manifests";
15const INSTALLATION_RECORD_FILE_NAME: &str = ".install.json";
16
17/// Houses utilities for getting the path to Spin plugin directories.
18pub struct PluginStore {
19    root: PathBuf,
20}
21
22impl PluginStore {
23    pub fn new(root: impl Into<PathBuf>) -> Self {
24        Self { root: root.into() }
25    }
26
27    pub fn try_default() -> Result<Self> {
28        Ok(Self::new(data_dir()?.join("plugins")))
29    }
30
31    /// Gets the path to where Spin plugin are installed.
32    pub fn get_plugins_directory(&self) -> &Path {
33        &self.root
34    }
35
36    /// Get the path to the subdirectory of an installed plugin.
37    pub fn plugin_subdirectory_path(&self, plugin_name: &str) -> PathBuf {
38        self.root.join(plugin_name)
39    }
40
41    /// Get the path to the manifests directory which contains the plugin manifests
42    /// of all installed Spin plugins.
43    pub fn installed_manifests_directory(&self) -> PathBuf {
44        self.root.join(PLUGIN_MANIFESTS_DIRECTORY_NAME)
45    }
46
47    pub fn installed_manifest_path(&self, plugin_name: &str) -> PathBuf {
48        self.installed_manifests_directory()
49            .join(manifest_file_name(plugin_name))
50    }
51
52    pub fn installed_binary_path(&self, plugin_name: &str) -> PathBuf {
53        let mut binary = self.root.join(plugin_name).join(plugin_name);
54        if cfg!(target_os = "windows") {
55            binary.set_extension("exe");
56        }
57        binary
58    }
59
60    pub fn installation_record_file(&self, plugin_name: &str) -> PathBuf {
61        self.root
62            .join(plugin_name)
63            .join(INSTALLATION_RECORD_FILE_NAME)
64    }
65
66    pub fn installed_manifests(&self) -> Result<Vec<PluginManifest>> {
67        let manifests_dir = self.installed_manifests_directory();
68        let manifest_paths = Self::json_files_in(&manifests_dir);
69        let manifests = manifest_paths
70            .iter()
71            .filter_map(|path| Self::try_read_manifest_from(path))
72            .collect();
73        Ok(manifests)
74    }
75
76    // TODO: report errors on individuals
77    pub fn catalogue_manifests(&self) -> Result<Vec<PluginManifest>> {
78        // Structure:
79        // CATALOGUE_DIR (spin/plugins/.spin-plugins/manifests)
80        // |- foo
81        // |  |- foo@0.1.2.json
82        // |  |- foo@1.2.3.json
83        // |  |- foo.json
84        // |- bar
85        //    |- bar.json
86        let catalogue_dir =
87            crate::lookup::spin_plugins_repo_manifest_dir(self.get_plugins_directory());
88
89        // Catalogue directory doesn't exist so likely nothing has been installed.
90        if !catalogue_dir.exists() {
91            return Ok(Vec::new());
92        }
93
94        let plugin_dirs = catalogue_dir
95            .read_dir()
96            .context("reading manifest catalogue at {catalogue_dir:?}")?
97            .filter_map(|d| d.ok())
98            .map(|d| d.path())
99            .filter(|p| p.is_dir());
100        let manifest_paths = plugin_dirs.flat_map(|path| Self::json_files_in(&path));
101        let manifests: Vec<_> = manifest_paths
102            .filter_map(|path| Self::try_read_manifest_from(&path))
103            .collect();
104        Ok(manifests)
105    }
106
107    fn try_read_manifest_from(manifest_path: &Path) -> Option<PluginManifest> {
108        let manifest_file = File::open(manifest_path).ok()?;
109        serde_json::from_reader(manifest_file).ok()
110    }
111
112    fn json_files_in(dir: &Path) -> Vec<PathBuf> {
113        let json_ext = Some(OsStr::new("json"));
114        match dir.read_dir() {
115            Err(_) => vec![],
116            Ok(rd) => rd
117                .filter_map(|de| de.ok())
118                .map(|de| de.path())
119                .filter(|p| p.is_file() && p.extension() == json_ext)
120                .collect(),
121        }
122    }
123
124    /// Returns the PluginManifest for an installed plugin with a given name.
125    /// Looks up and parses the JSON plugin manifest file into object form.
126    pub fn read_plugin_manifest(&self, plugin_name: &str) -> PluginLookupResult<PluginManifest> {
127        let manifest_path = self.installed_manifest_path(plugin_name);
128        tracing::info!("Reading plugin manifest from {}", manifest_path.display());
129        let manifest_file = File::open(manifest_path.clone()).map_err(|e| {
130            Error::NotFound(NotFoundError::new(
131                Some(plugin_name.to_string()),
132                manifest_path.display().to_string(),
133                e.to_string(),
134            ))
135        })?;
136        let manifest = serde_json::from_reader(manifest_file).map_err(|e| {
137            Error::InvalidManifest(InvalidManifestError::new(
138                Some(plugin_name.to_string()),
139                manifest_path.display().to_string(),
140                e.to_string(),
141            ))
142        })?;
143        Ok(manifest)
144    }
145
146    pub(crate) fn add_manifest(&self, plugin_manifest: &PluginManifest) -> Result<()> {
147        let manifests_dir = self.installed_manifests_directory();
148        std::fs::create_dir_all(manifests_dir)?;
149        serde_json::to_writer(
150            &File::create(self.installed_manifest_path(&plugin_manifest.name()))?,
151            plugin_manifest,
152        )?;
153        tracing::trace!("Added manifest for {}", &plugin_manifest.name());
154        Ok(())
155    }
156
157    pub(crate) fn untar_plugin(&self, plugin_file_name: &PathBuf, plugin_name: &str) -> Result<()> {
158        // Get handle to file
159        let tar_gz = File::open(plugin_file_name)?;
160        // Unzip file
161        let tar = GzDecoder::new(tar_gz);
162        // Get plugin from tarball
163        let mut archive = Archive::new(tar);
164        archive.set_preserve_permissions(true);
165        // Create subdirectory in plugins directory for this plugin
166        let plugin_sub_dir = self.plugin_subdirectory_path(plugin_name);
167        fs::remove_dir_all(&plugin_sub_dir).ok();
168        fs::create_dir_all(&plugin_sub_dir)?;
169        archive.unpack(&plugin_sub_dir)?;
170        Ok(())
171    }
172}
173
174/// Given a plugin name, returns the expected file name for the installed manifest
175pub fn manifest_file_name(plugin_name: &str) -> String {
176    format!("{plugin_name}.json")
177}