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
13pub const PLUGIN_MANIFESTS_DIRECTORY_NAME: &str = "manifests";
15const INSTALLATION_RECORD_FILE_NAME: &str = ".install.json";
16
17pub 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 pub fn get_plugins_directory(&self) -> &Path {
33 &self.root
34 }
35
36 pub fn plugin_subdirectory_path(&self, plugin_name: &str) -> PathBuf {
38 self.root.join(plugin_name)
39 }
40
41 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 pub fn catalogue_manifests(&self) -> Result<Vec<PluginManifest>> {
78 let catalogue_dir =
87 crate::lookup::spin_plugins_repo_manifest_dir(self.get_plugins_directory());
88
89 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 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 let tar_gz = File::open(plugin_file_name)?;
160 let tar = GzDecoder::new(tar_gz);
162 let mut archive = Archive::new(tar);
164 archive.set_preserve_permissions(true);
165 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
174pub fn manifest_file_name(plugin_name: &str) -> String {
176 format!("{plugin_name}.json")
177}