fresh/services/plugins/
embedded.rs1use include_dir::{include_dir, Dir};
10use std::path::PathBuf;
11use std::sync::OnceLock;
12
13static EMBEDDED_PLUGINS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/plugins");
15
16static EXTRACTED_PLUGINS_DIR: OnceLock<PathBuf> = OnceLock::new();
18
19pub fn get_embedded_plugins_dir() -> Option<&'static PathBuf> {
26 EXTRACTED_PLUGINS_DIR.get_or_init(|| match extract_plugins() {
27 Ok(path) => path,
28 Err(e) => {
29 tracing::error!("Failed to extract embedded plugins: {}", e);
30 PathBuf::new()
31 }
32 });
33
34 let path = EXTRACTED_PLUGINS_DIR.get()?;
35 if path.exists()
36 && path
37 .read_dir()
38 .map(|mut d| d.next().is_some())
39 .unwrap_or(false)
40 {
41 Some(path)
42 } else {
43 None
44 }
45}
46
47const PLUGINS_CONTENT_HASH: &str = include_str!(concat!(env!("OUT_DIR"), "/plugins_hash.txt"));
49
50fn get_cache_dir() -> Option<PathBuf> {
52 dirs::cache_dir().map(|p| p.join("fresh").join("embedded-plugins"))
53}
54
55fn extract_plugins() -> Result<PathBuf, std::io::Error> {
57 let cache_base = get_cache_dir().ok_or_else(|| {
58 std::io::Error::new(
59 std::io::ErrorKind::NotFound,
60 "Could not determine cache directory",
61 )
62 })?;
63
64 let content_hash = PLUGINS_CONTENT_HASH.trim();
65 let cache_dir = cache_base.join(content_hash);
66
67 if cache_dir.exists() && cache_dir.read_dir()?.next().is_some() {
69 tracing::info!("Using cached embedded plugins from: {:?}", cache_dir);
70 return Ok(cache_dir);
71 }
72
73 tracing::info!("Extracting embedded plugins to: {:?}", cache_dir);
74
75 if cache_base.exists() {
77 for entry in std::fs::read_dir(&cache_base)? {
78 let entry = entry?;
79 if entry.file_name() != content_hash {
80 #[allow(clippy::let_underscore_must_use)]
82 let _ = trash::delete(entry.path());
83 }
84 }
85 }
86
87 extract_dir_recursive(&EMBEDDED_PLUGINS, &cache_dir)?;
88
89 tracing::info!(
90 "Successfully extracted {} embedded plugin files",
91 count_files(&EMBEDDED_PLUGINS)
92 );
93
94 Ok(cache_dir)
95}
96
97fn extract_dir_recursive(dir: &Dir<'_>, target_path: &std::path::Path) -> std::io::Result<()> {
99 std::fs::create_dir_all(target_path)?;
100
101 for file in dir.files() {
103 let file_path = target_path.join(file.path().file_name().unwrap_or_default());
104 std::fs::write(&file_path, file.contents())?;
105 tracing::debug!("Extracted: {:?}", file_path);
106 }
107
108 for subdir in dir.dirs() {
110 let subdir_name = subdir.path().file_name().unwrap_or_default();
111 let subdir_path = target_path.join(subdir_name);
112 extract_dir_recursive(subdir, &subdir_path)?;
113 }
114
115 Ok(())
116}
117
118fn count_files(dir: &Dir<'_>) -> usize {
120 let mut count = dir.files().count();
121 for subdir in dir.dirs() {
122 count += count_files(subdir);
123 }
124 count
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_embedded_plugins_exist() {
133 assert!(EMBEDDED_PLUGINS.files().count() > 0 || EMBEDDED_PLUGINS.dirs().count() > 0);
135 }
136
137 #[test]
138 fn test_extract_plugins() {
139 let path = get_embedded_plugins_dir();
140 assert!(path.is_some());
141 let path = path.unwrap();
142 assert!(path.exists());
143 assert!(path.is_dir());
144
145 let entries: Vec<_> = std::fs::read_dir(path).unwrap().collect();
147 assert!(!entries.is_empty());
148 }
149}