1use crate::error::{Contextable, Error, Result};
2use crate::script::IntoScriptName;
3use crate::script::{ScriptName, ANONYMOUS};
4use crate::script_type::{ScriptFullType, ScriptType};
5use crate::util::{handle_fs_res, read_file};
6use fxhash::FxHashSet as HashSet;
7use std::fs::{create_dir, create_dir_all, read_dir};
8use std::path::{Component, Path, PathBuf};
9
10pub const HS_REDIRECT: &str = ".hs_redirect";
11pub const HS_PRE_RUN: &str = ".hs_prerun";
12const PROCESS_LOCK: &str = ".hs_process_lock";
13const TEMPLATE: &str = ".hs_templates";
14const HBS_EXT: &str = ".hbs";
15
16macro_rules! hs_home_env {
17 () => {
18 "HYPER_SCRIPTER_HOME"
19 };
20}
21
22crate::local_global_state!(home_state, PathBuf, || { get_test_home() });
23
24#[cfg(not(feature = "hard-home"))]
25fn get_default_home() -> Result<PathBuf> {
26 const ROOT_PATH: &str = "hyper_scripter";
27 use crate::error::SysPath;
28 let home = dirs::config_dir()
29 .ok_or(Error::SysPathNotFound(SysPath::Config))?
30 .join(ROOT_PATH);
31 Ok(home)
32}
33#[cfg(feature = "hard-home")]
34fn get_default_home() -> Result<PathBuf> {
35 let home = env!(
36 hs_home_env!(),
37 concat!("Hardcoded home ", hs_home_env!(), " not provided!",)
38 );
39 Ok(home.into())
40}
41
42fn get_sys_home() -> Result<PathBuf> {
43 let p = match std::env::var(hs_home_env!()) {
44 Ok(p) => {
45 log::debug!("使用環境變數路徑:{}", p);
46 p.into()
47 }
48 Err(std::env::VarError::NotPresent) => get_default_home()?,
49 Err(e) => return Err(e.into()),
50 };
51 Ok(p)
52}
53
54fn join_here_abs<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
55 let path = path.as_ref();
56 if path.is_absolute() {
57 return Ok(path.to_owned());
58 }
59 let here = std::env::current_dir()?;
60 Ok(AsRef::<Path>::as_ref(&here).join(path))
61}
62
63pub fn normalize_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
64 let path = join_here_abs(path)?;
65 let mut components = path.components().peekable();
66 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
67 components.next();
68 PathBuf::from(c.as_os_str())
69 } else {
70 PathBuf::new()
71 };
72
73 for component in components {
74 match component {
75 Component::Prefix(..) => unreachable!(),
76 Component::RootDir => {
77 ret.push(component.as_os_str());
78 }
79 Component::CurDir => {}
80 Component::ParentDir => {
81 ret.pop();
82 }
83 Component::Normal(c) => {
84 ret.push(c);
85 }
86 }
87 }
88 Ok(ret)
89}
90
91fn compute_home_path<T: AsRef<Path>>(p: T, create_on_missing: bool) -> Result<PathBuf> {
92 let path = join_here_abs(p)?;
93 log::debug!("計算路徑:{:?}", path);
94 if !path.exists() {
95 if create_on_missing {
96 log::info!("路徑 {:?} 不存在,嘗試創建之", path);
97 handle_fs_res(&[&path], create_dir(&path))?;
98 } else {
99 return Err(Error::PathNotFound(vec![path]));
100 }
101 } else {
102 let redirect = path.join(HS_REDIRECT);
103 if redirect.is_file() {
104 let redirect = read_file(&redirect)?;
105 let redirect = path.join(redirect.trim());
106 log::info!("重導向至 {:?}", redirect);
107 return compute_home_path(redirect, create_on_missing);
108 }
109 }
110 Ok(path)
111}
112pub fn compute_home_path_optional<T: AsRef<Path>>(
113 p: Option<T>,
114 create_on_missing: bool,
115) -> Result<PathBuf> {
116 match p {
117 Some(p) => compute_home_path(p, create_on_missing),
118 None => compute_home_path(get_sys_home()?, create_on_missing),
119 }
120}
121pub fn set_home<T: AsRef<Path>>(p: Option<T>, create_on_missing: bool) -> Result {
122 if !cfg!(test) {
123 let path = compute_home_path_optional(p, create_on_missing)?;
124 home_state::set(path);
125 }
126 Ok(())
127}
128#[cfg(not(feature = "no-state-check"))]
129pub fn set_home_thread_local(p: &'static PathBuf) {
130 home_state::set_local(p);
131}
132
133pub fn get_home() -> &'static Path {
134 home_state::get().as_ref()
135}
136#[cfg(test)]
137pub fn get_test_home() -> PathBuf {
138 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
139 dir.join(".test_hyper_scripter")
140}
141
142fn get_anonymous_ids() -> Result<impl Iterator<Item = Result<u32>>> {
143 let dir = get_home().join(ANONYMOUS);
144 if !dir.exists() {
145 log::info!("找不到匿名腳本資料夾,創建之");
146 handle_fs_res(&[&dir], create_dir(&dir))?;
147 }
148
149 let dir = handle_fs_res(&[&dir], read_dir(&dir))?;
150 let iter = dir
151 .map(|entry| -> Result<Option<u32>> {
152 let name = entry?.file_name();
153 let name = name
154 .to_str()
155 .ok_or_else(|| Error::msg("檔案實體為空...?"))?;
156 let pre = if let Some((pre, _)) = name.split_once('.') {
157 pre
158 } else {
159 name
160 };
161 match pre.parse::<u32>() {
162 Ok(id) => Ok(Some(id)),
163 _ => {
164 log::warn!("匿名腳本名無法轉為整數:{}", name);
165 Ok(None)
166 }
167 }
168 })
169 .filter_map(|t| match t {
170 Ok(Some(id)) => Some(Ok(id)),
171 Ok(None) => None,
172 Err(e) => Some(Err(e)),
173 });
174 Ok(iter)
175}
176
177pub struct NewAnonymouseIter {
178 existing_ids: HashSet<u32>,
179 amount: u32,
180 cur_id: u32,
181}
182impl Iterator for NewAnonymouseIter {
183 type Item = ScriptName;
184 fn next(&mut self) -> Option<Self::Item> {
185 if self.amount == 0 {
186 return None;
187 }
188 loop {
189 self.cur_id += 1;
190 if !self.existing_ids.contains(&self.cur_id) {
191 self.amount -= 1;
192 return Some(self.cur_id.into_script_name().unwrap());
193 }
194 }
195 }
196}
197pub fn new_anonymous_name(
198 amount: u32,
199 existing: impl Iterator<Item = u32>,
200) -> Result<NewAnonymouseIter> {
201 let mut all_existing = get_anonymous_ids()?
202 .collect::<Result<HashSet<_>>>()
203 .context("無法取得匿名腳本編號")?;
204 for id in existing.into_iter() {
205 all_existing.insert(id);
206 }
207 Ok(NewAnonymouseIter {
208 existing_ids: all_existing,
209 amount,
210 cur_id: 0,
211 })
212}
213
214pub fn open_script(
218 name: &ScriptName,
219 ty: &ScriptType,
220 check_exist: Option<bool>,
221) -> Result<PathBuf> {
222 let mut err_in_fallback = None;
223 let script_path = if check_exist == Some(true) {
224 let (p, e) = name.to_file_path_fallback(ty);
225 err_in_fallback = e;
226 p
227 } else {
228 name.to_file_path(ty)?
229 };
230 let script_path = get_home().join(script_path);
231
232 if let Some(should_exist) = check_exist {
233 if !script_path.exists() && should_exist {
234 if let Some(e) = err_in_fallback {
235 return Err(e);
236 }
237 return Err(
238 Error::PathNotFound(vec![script_path]).context("開腳本失敗:應存在卻不存在")
239 );
240 } else if script_path.exists() && !should_exist {
241 return Err(Error::PathExist(script_path).context("開腳本失敗:不應存在卻存在"));
242 }
243 }
244 Ok(script_path)
245}
246
247pub fn get_process_lock_dir() -> Result<PathBuf> {
248 let p = get_home().join(PROCESS_LOCK);
249 if !p.exists() {
250 log::info!("找不到檔案鎖資料夾,創建之");
251 handle_fs_res(&[&p], create_dir_all(&p))?;
252 }
253 Ok(p)
254}
255
256pub fn get_process_lock(run_id: i64) -> Result<PathBuf> {
257 Ok(get_process_lock_dir()?.join(run_id.to_string()))
258}
259
260pub fn get_template_path(ty: &ScriptFullType) -> Result<PathBuf> {
261 let p = get_home().join(TEMPLATE).join(format!("{}{}", ty, HBS_EXT));
262 if let Some(dir) = p.parent() {
263 if !dir.exists() {
264 log::info!("找不到模板資料夾,創建之");
265 handle_fs_res(&[&dir], create_dir_all(&dir))?;
266 }
267 }
268 Ok(p)
269}
270
271pub fn get_sub_types(ty: &ScriptType) -> Result<Vec<ScriptType>> {
272 let dir = get_home().join(TEMPLATE).join(ty.as_ref());
273 if !dir.exists() {
274 log::info!("找不到子類別資料夾,直接回傳");
275 return Ok(vec![]);
276 }
277
278 let mut subs = vec![];
279 for entry in handle_fs_res(&[&dir], read_dir(&dir))? {
280 let name = entry?.file_name();
281 let name = name
282 .to_str()
283 .ok_or_else(|| Error::msg("檔案實體為空...?"))?;
284 if name.ends_with(HBS_EXT) {
285 let name = &name[..name.len() - HBS_EXT.len()];
286 subs.push(name.parse()?);
287 } else {
288 log::warn!("發現非模版檔案 {}", name);
289 }
290 }
291 Ok(subs)
292}
293
294#[cfg(test)]
295mod test {
296 use super::*;
297 impl From<&'static str> for ScriptType {
298 fn from(s: &'static str) -> Self {
299 ScriptType::new_unchecked(s.to_string())
300 }
301 }
302 #[test]
303 fn test_anonymous_ids() {
304 let mut ids = get_anonymous_ids()
305 .unwrap()
306 .collect::<Result<Vec<_>>>()
307 .unwrap();
308 ids.sort();
309 assert_eq!(ids, vec![1, 2, 3, 5]);
310 }
311 #[test]
312 fn test_open_anonymous() {
313 let new_scripts = new_anonymous_name(3, [7].into_iter())
314 .unwrap()
315 .collect::<Vec<_>>();
316 assert_eq!(new_scripts[0], ScriptName::Anonymous(4));
317 assert_eq!(new_scripts[1], ScriptName::Anonymous(6));
318 assert_eq!(new_scripts[2], ScriptName::Anonymous(8));
319
320 let p = open_script(&5.into_script_name().unwrap(), &"js".into(), None).unwrap();
321 assert_eq!(p, get_test_home().join(".anonymous/5.js"));
322 }
323 #[test]
324 fn test_open() {
325 let second_name = "second".to_owned().into_script_name().unwrap();
326 let not_exist = "not-exist".to_owned().into_script_name().unwrap();
327
328 let p = open_script(&second_name, &"rb".into(), Some(false)).unwrap();
329 assert_eq!(p, get_home().join("second.rb"));
330
331 let p = open_script(
332 &".1".to_owned().into_script_name().unwrap(),
333 &"sh".into(),
334 None,
335 )
336 .unwrap();
337 assert_eq!(p, get_test_home().join(".anonymous/1.sh"));
338
339 match open_script(¬_exist, &"sh".into(), Some(true)).unwrap_err() {
340 Error::PathNotFound(name) => assert_eq!(name[0], get_home().join("not-exist.sh")),
341 _ => unreachable!(),
342 }
343
344 let err = open_script(&second_name, &"no-such-type".into(), None).unwrap_err();
346 assert!(matches!(err, Error::UnknownType(_)));
347 let err = open_script(&second_name, &"no-such-type".into(), Some(false)).unwrap_err();
348 assert!(matches!(err, Error::UnknownType(_)));
349 let p = open_script(&second_name, &"no-such-type".into(), Some(true)).unwrap();
350 assert_eq!(p, get_home().join("second.no-such-type"));
351 let err = open_script(¬_exist, &"no-such-type".into(), Some(true)).unwrap_err();
353 assert!(matches!(err, Error::UnknownType(_)));
354 }
355}