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
212pub 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(¬_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 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 let err = open_script(¬_exist, &"no-such-type".into(), Some(true)).unwrap_err();
351 assert!(matches!(err, Error::UnknownType(_)));
352 }
353}