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    let path = compute_home_path_optional(p, create_on_missing)?;
123    home_state::set(path);
124    Ok(())
125}
126#[cfg(not(feature = "no-state-check"))]
127pub fn set_home_thread_local(p: &'static PathBuf) {
128    home_state::set_local(p);
129}
130
131pub fn get_home() -> &'static Path {
132    home_state::get().as_ref()
133}
134#[cfg(test)]
135pub fn get_test_home() -> PathBuf {
136    let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
137    dir.join(".test_hyper_scripter")
138}
139
140fn get_anonymous_ids() -> Result<impl Iterator<Item = Result<u32>>> {
141    let dir = get_home().join(ANONYMOUS);
142    if !dir.exists() {
143        log::info!("找不到匿名腳本資料夾,創建之");
144        handle_fs_res(&[&dir], create_dir(&dir))?;
145    }
146
147    let dir = handle_fs_res(&[&dir], read_dir(&dir))?;
148    let iter = dir
149        .map(|entry| -> Result<Option<u32>> {
150            let name = entry?.file_name();
151            let name = name
152                .to_str()
153                .ok_or_else(|| Error::msg("檔案實體為空...?"))?;
154            let pre = if let Some((pre, _)) = name.split_once('.') {
155                pre
156            } else {
157                name
158            };
159            match pre.parse::<u32>() {
160                Ok(id) => Ok(Some(id)),
161                _ => {
162                    log::warn!("匿名腳本名無法轉為整數:{}", name);
163                    Ok(None)
164                }
165            }
166        })
167        .filter_map(|t| match t {
168            Ok(Some(id)) => Some(Ok(id)),
169            Ok(None) => None,
170            Err(e) => Some(Err(e)),
171        });
172    Ok(iter)
173}
174
175pub struct NewAnonymouseIter {
176    existing_ids: HashSet<u32>,
177    amount: u32,
178    cur_id: u32,
179}
180impl Iterator for NewAnonymouseIter {
181    type Item = ScriptName;
182    fn next(&mut self) -> Option<Self::Item> {
183        if self.amount == 0 {
184            return None;
185        }
186        loop {
187            self.cur_id += 1;
188            if !self.existing_ids.contains(&self.cur_id) {
189                self.amount -= 1;
190                return Some(self.cur_id.into_script_name().unwrap());
191            }
192        }
193    }
194}
195pub fn new_anonymous_name(
196    amount: u32,
197    existing: impl Iterator<Item = u32>,
198) -> Result<NewAnonymouseIter> {
199    let mut all_existing = get_anonymous_ids()?
200        .collect::<Result<HashSet<_>>>()
201        .context("無法取得匿名腳本編號")?;
202    for id in existing.into_iter() {
203        all_existing.insert(id);
204    }
205    Ok(NewAnonymouseIter {
206        existing_ids: all_existing,
207        amount,
208        cur_id: 0,
209    })
210}
211
212/// 若 `check_exist` 有值,則會檢查存在性
213/// 需注意:要找已存在的腳本時,允許未知的腳本類型
214/// 此情況下會使用 to_file_path_fallback 方法,即以類型名當作擴展名
215pub fn open_script(
216    name: &ScriptName,
217    ty: &ScriptType,
218    check_exist: Option<bool>,
219) -> Result<PathBuf> {
220    let mut err_in_fallback = None;
221    let script_path = if check_exist == Some(true) {
222        let (p, e) = name.to_file_path_fallback(ty);
223        err_in_fallback = e;
224        p
225    } else {
226        name.to_file_path(ty)?
227    };
228    let script_path = get_home().join(script_path);
229
230    if let Some(should_exist) = check_exist {
231        if !script_path.exists() && should_exist {
232            if let Some(e) = err_in_fallback {
233                return Err(e);
234            }
235            return Err(
236                Error::PathNotFound(vec![script_path]).context("開腳本失敗:應存在卻不存在")
237            );
238        } else if script_path.exists() && !should_exist {
239            return Err(Error::PathExist(script_path).context("開腳本失敗:不應存在卻存在"));
240        }
241    }
242    Ok(script_path)
243}
244
245pub fn get_process_lock_dir() -> Result<PathBuf> {
246    let p = get_home().join(PROCESS_LOCK);
247    if !p.exists() {
248        log::info!("找不到檔案鎖資料夾,創建之");
249        handle_fs_res(&[&p], create_dir_all(&p))?;
250    }
251    Ok(p)
252}
253
254pub fn get_process_lock(run_id: i64) -> Result<PathBuf> {
255    Ok(get_process_lock_dir()?.join(run_id.to_string()))
256}
257
258pub fn get_template_path(ty: &ScriptFullType) -> Result<PathBuf> {
259    let p = get_home().join(TEMPLATE).join(format!("{}{}", ty, HBS_EXT));
260    if let Some(dir) = p.parent() {
261        if !dir.exists() {
262            log::info!("找不到模板資料夾,創建之");
263            handle_fs_res(&[&dir], create_dir_all(&dir))?;
264        }
265    }
266    Ok(p)
267}
268
269pub fn get_sub_types(ty: &ScriptType) -> Result<Vec<ScriptType>> {
270    let dir = get_home().join(TEMPLATE).join(ty.as_ref());
271    if !dir.exists() {
272        log::info!("找不到子類別資料夾,直接回傳");
273        return Ok(vec![]);
274    }
275
276    let mut subs = vec![];
277    for entry in handle_fs_res(&[&dir], read_dir(&dir))? {
278        let name = entry?.file_name();
279        let name = name
280            .to_str()
281            .ok_or_else(|| Error::msg("檔案實體為空...?"))?;
282        if name.ends_with(HBS_EXT) {
283            let name = &name[..name.len() - HBS_EXT.len()];
284            subs.push(name.parse()?);
285        } else {
286            log::warn!("發現非模版檔案 {}", name);
287        }
288    }
289    Ok(subs)
290}
291
292#[cfg(test)]
293mod test {
294    use super::*;
295    impl From<&'static str> for ScriptType {
296        fn from(s: &'static str) -> Self {
297            ScriptType::new_unchecked(s.to_string())
298        }
299    }
300    #[test]
301    fn test_anonymous_ids() {
302        let mut ids = get_anonymous_ids()
303            .unwrap()
304            .collect::<Result<Vec<_>>>()
305            .unwrap();
306        ids.sort();
307        assert_eq!(ids, vec![1, 2, 3, 5]);
308    }
309    #[test]
310    fn test_open_anonymous() {
311        let new_scripts = new_anonymous_name(3, [7].into_iter())
312            .unwrap()
313            .collect::<Vec<_>>();
314        assert_eq!(new_scripts[0], ScriptName::Anonymous(4));
315        assert_eq!(new_scripts[1], ScriptName::Anonymous(6));
316        assert_eq!(new_scripts[2], ScriptName::Anonymous(8));
317
318        let p = open_script(&5.into_script_name().unwrap(), &"js".into(), None).unwrap();
319        assert_eq!(p, get_test_home().join(".anonymous/5.js"));
320    }
321    #[test]
322    fn test_open() {
323        let second_name = "second".to_owned().into_script_name().unwrap();
324        let not_exist = "not-exist".to_owned().into_script_name().unwrap();
325
326        let p = open_script(&second_name, &"rb".into(), Some(false)).unwrap();
327        assert_eq!(p, get_home().join("second.rb"));
328
329        let p = open_script(
330            &".1".to_owned().into_script_name().unwrap(),
331            &"sh".into(),
332            None,
333        )
334        .unwrap();
335        assert_eq!(p, get_test_home().join(".anonymous/1.sh"));
336
337        match open_script(&not_exist, &"sh".into(), Some(true)).unwrap_err() {
338            Error::PathNotFound(name) => assert_eq!(name[0], get_home().join("not-exist.sh")),
339            _ => unreachable!(),
340        }
341
342        // NOTE: 如果是要找已存在的腳本,可以允許為不存在的類型,此情況下直接將類別的名字當作擴展名
343        let err = open_script(&second_name, &"no-such-type".into(), None).unwrap_err();
344        assert!(matches!(err, Error::UnknownType(_)));
345        let err = open_script(&second_name, &"no-such-type".into(), Some(false)).unwrap_err();
346        assert!(matches!(err, Error::UnknownType(_)));
347        let p = open_script(&second_name, &"no-such-type".into(), Some(true)).unwrap();
348        assert_eq!(p, get_home().join("second.no-such-type"));
349        // 用類別名當擴展名仍找不到,當然還是要報錯
350        let err = open_script(&not_exist, &"no-such-type".into(), Some(true)).unwrap_err();
351        assert!(matches!(err, Error::UnknownType(_)));
352    }
353}