hyper_scripter/
path.rs

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
214/// 若 `check_exist` 有值,則會檢查存在性
215/// 需注意:要找已存在的腳本時,允許未知的腳本類型
216/// 此情況下會使用 to_file_path_fallback 方法,即以類型名當作擴展名
217pub 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(&not_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        // NOTE: 如果是要找已存在的腳本,可以允許為不存在的類型,此情況下直接將類別的名字當作擴展名
345        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        // 用類別名當擴展名仍找不到,當然還是要報錯
352        let err = open_script(&not_exist, &"no-such-type".into(), Some(true)).unwrap_err();
353        assert!(matches!(err, Error::UnknownType(_)));
354    }
355}