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)) .map(TemplateLayout::new)
67 .collect())
68 }
69
70 fn relative_dir(id: &str) -> impl AsRef<Path> {
71 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}