Skip to main content

spin_templates/
store.rs

1use anyhow::Context;
2use spin_common::data_dir::data_dir;
3use std::path::{Path, PathBuf};
4use std::sync::LazyLock;
5
6use crate::directory::subdirectories;
7
8const ENV_SCOPED_TEMPLATES_DIR: &str = ".envs";
9
10pub(crate) struct TemplateStore {
11    root: PathBuf,
12}
13
14static UNSAFE_CHARACTERS: LazyLock<regex::Regex> =
15    LazyLock::new(|| regex::Regex::new("[^-_a-zA-Z0-9]").expect("Invalid identifier regex"));
16
17impl TemplateStore {
18    pub(crate) fn new(root: impl AsRef<Path>) -> Self {
19        Self {
20            root: root.as_ref().to_owned(),
21        }
22    }
23
24    pub(crate) fn try_default() -> anyhow::Result<Self> {
25        Ok(Self::new(data_dir()?.join("templates")))
26    }
27
28    pub(crate) fn for_environment(env: &str) -> anyhow::Result<Self> {
29        let env_dir = UNSAFE_CHARACTERS.replace_all(env, "_");
30        Ok(Self::new(
31            data_dir()?
32                .join("templates")
33                .join(".envs")
34                .join(env_dir.as_ref()),
35        ))
36    }
37
38    pub(crate) fn get_directory(&self, id: impl AsRef<str>) -> PathBuf {
39        self.root.join(Self::relative_dir(id.as_ref()))
40    }
41
42    pub(crate) fn get_layout(&self, id: impl AsRef<str>) -> Option<TemplateLayout> {
43        let template_dir = self.get_directory(id);
44        if template_dir.exists() {
45            Some(TemplateLayout::new(&template_dir))
46        } else {
47            None
48        }
49    }
50
51    pub(crate) async fn list_layouts(&self) -> anyhow::Result<Vec<TemplateLayout>> {
52        if !self.root.exists() {
53            return Ok(vec![]);
54        }
55
56        let template_dirs = subdirectories(&self.root).with_context(|| {
57            format!(
58                "Failed to read template directories from {}",
59                self.root.display()
60            )
61        })?;
62
63        Ok(template_dirs
64            .iter()
65            .filter(|dp| !Self::is_known_non_template_dir(dp)) // the awkward double negative is because we are ruling out *known* special dirs rather than ruling *in* template dirs
66            .map(TemplateLayout::new)
67            .collect())
68    }
69
70    fn relative_dir(id: &str) -> impl AsRef<Path> {
71        // Using the SHA could generate quite long directory names, which could be a problem on Windows
72        // if the template filenames are also long. Longer term, consider an alternative approach where
73        // we use an index or something for disambiguation, and/or disambiguating only if a clash is
74        // detected, etc.
75        let id_sha256 = spin_common::sha256::hex_digest_from_bytes(id);
76        format!("{}_{}", UNSAFE_CHARACTERS.replace_all(id, "_"), id_sha256)
77    }
78
79    fn is_known_non_template_dir(path: impl AsRef<Path>) -> bool {
80        path.as_ref()
81            .file_name()
82            .is_some_and(|n| n == ENV_SCOPED_TEMPLATES_DIR)
83    }
84}
85
86pub(crate) struct TemplateLayout {
87    template_dir: PathBuf,
88}
89
90const METADATA_DIR_NAME: &str = "metadata";
91const CONTENT_DIR_NAME: &str = "content";
92const SNIPPETS_DIR_NAME: &str = "snippets";
93const PARTIALS_DIR_NAME: &str = "partials";
94
95const MANIFEST_FILE_NAME: &str = "spin-template.toml";
96
97const INSTALLATION_RECORD_FILE_NAME: &str = ".install.toml";
98
99impl TemplateLayout {
100    pub fn new(template_dir: impl AsRef<Path>) -> Self {
101        Self {
102            template_dir: template_dir.as_ref().to_owned(),
103        }
104    }
105
106    pub fn metadata_dir(&self) -> PathBuf {
107        self.template_dir.join(METADATA_DIR_NAME)
108    }
109
110    pub fn manifest_path(&self) -> PathBuf {
111        self.metadata_dir().join(MANIFEST_FILE_NAME)
112    }
113
114    pub fn content_dir(&self) -> PathBuf {
115        self.template_dir.join(CONTENT_DIR_NAME)
116    }
117
118    pub fn snippets_dir(&self) -> PathBuf {
119        self.metadata_dir().join(SNIPPETS_DIR_NAME)
120    }
121
122    pub fn partials_dir(&self) -> PathBuf {
123        self.metadata_dir().join(PARTIALS_DIR_NAME)
124    }
125
126    pub fn installation_record_file(&self) -> PathBuf {
127        self.template_dir.join(INSTALLATION_RECORD_FILE_NAME)
128    }
129}