use crate::error::{Contextable, Error, Result};
use crate::script::IntoScriptName;
use crate::script::{ScriptName, ANONYMOUS};
use crate::script_type::{AsScriptFullTypeRef, ScriptType};
use crate::state::State;
use crate::util::{handle_fs_res, read_file};
use fxhash::FxHashSet as HashSet;
use std::fs::{create_dir, create_dir_all, read_dir};
use std::path::{Component, Path, PathBuf};
pub const HS_REDIRECT: &str = ".hs_redirect";
pub const HS_PRE_RUN: &str = ".hs_prerun";
const TEMPLATE: &str = ".hs_templates";
const HBS_EXT: &str = ".hbs";
macro_rules! hs_home_env {
() => {
"HYPER_SCRIPTER_HOME"
};
}
static PATH: State<PathBuf> = State::new();
#[cfg(not(feature = "hard-home"))]
fn get_default_home() -> Result<PathBuf> {
const ROOT_PATH: &str = "hyper_scripter";
use crate::error::SysPath;
let home = dirs::config_dir()
.ok_or(Error::SysPathNotFound(SysPath::Config))?
.join(ROOT_PATH);
Ok(home)
}
#[cfg(feature = "hard-home")]
fn get_default_home() -> Result<PathBuf> {
let home = env!(
hs_home_env!(),
concat!("Hardcoded home ", hs_home_env!(), " not provided!",)
);
Ok(home.into())
}
fn get_sys_home() -> Result<PathBuf> {
let p = match std::env::var(hs_home_env!()) {
Ok(p) => {
log::debug!("使用環境變數路徑:{}", p);
p.into()
}
Err(std::env::VarError::NotPresent) => get_default_home()?,
Err(e) => return Err(e.into()),
};
Ok(p)
}
fn join_here_abs<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = path.as_ref();
if path.is_absolute() {
return Ok(path.to_owned());
}
let here = std::env::current_dir()?;
Ok(AsRef::<Path>::as_ref(&here).join(path))
}
pub fn normalize_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = join_here_abs(path)?;
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
Ok(ret)
}
fn compute_home_path<T: AsRef<Path>>(p: T, create_on_missing: bool) -> Result<PathBuf> {
let path = join_here_abs(p)?;
log::debug!("計算路徑:{:?}", path);
if !path.exists() {
if create_on_missing {
log::info!("路徑 {:?} 不存在,嘗試創建之", path);
handle_fs_res(&[&path], create_dir(&path))?;
} else {
return Err(Error::PathNotFound(vec![path]));
}
} else {
let redirect = path.join(HS_REDIRECT);
if redirect.is_file() {
let redirect = read_file(&redirect)?;
let redirect = path.join(redirect.trim());
log::info!("重導向至 {:?}", redirect);
return compute_home_path(redirect, create_on_missing);
}
}
Ok(path)
}
pub fn compute_home_path_optional<T: AsRef<Path>>(
p: Option<T>,
create_on_missing: bool,
) -> Result<PathBuf> {
match p {
Some(p) => compute_home_path(p, create_on_missing),
None => compute_home_path(get_sys_home()?, create_on_missing),
}
}
pub fn set_home<T: AsRef<Path>>(p: Option<T>, create_on_missing: bool) -> Result {
let path = compute_home_path_optional(p, create_on_missing)?;
PATH.set(path);
Ok(())
}
#[cfg(not(test))]
pub fn get_home() -> &'static Path {
PATH.get().as_ref()
}
#[cfg(test)]
pub fn get_test_home() -> PathBuf {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.join(".test_hyper_scripter")
}
#[cfg(test)]
pub fn get_home() -> &'static Path {
crate::set_once!(PATH, || { get_test_home() });
PATH.get().as_ref()
}
fn get_anonymous_ids() -> Result<impl Iterator<Item = Result<u32>>> {
let dir = get_home().join(ANONYMOUS);
if !dir.exists() {
log::info!("找不到匿名腳本資料夾,創建之");
handle_fs_res(&[&dir], create_dir(&dir))?;
}
let dir = handle_fs_res(&[&dir], read_dir(&dir))?;
let iter = dir
.map(|entry| -> Result<Option<u32>> {
let name = entry?.file_name();
let name = name
.to_str()
.ok_or_else(|| Error::msg("檔案實體為空...?"))?;
let pre = if let Some((pre, _)) = name.split_once('.') {
pre
} else {
name
};
match pre.parse::<u32>() {
Ok(id) => Ok(Some(id)),
_ => {
log::warn!("匿名腳本名無法轉為整數:{}", name);
Ok(None)
}
}
})
.filter_map(|t| match t {
Ok(Some(id)) => Some(Ok(id)),
Ok(None) => None,
Err(e) => Some(Err(e)),
});
Ok(iter)
}
pub fn new_anonymous_name() -> Result<ScriptName> {
let ids = get_anonymous_ids()?
.collect::<Result<HashSet<_>>>()
.context("無法取得匿名腳本編號")?;
let mut i = 1;
loop {
if !ids.contains(&i) {
return i.into_script_name();
}
i += 1;
}
}
pub fn open_new_anonymous(ty: &ScriptType) -> Result<(ScriptName, PathBuf)> {
let name = new_anonymous_name()?;
let path = open_script(&name, ty, None)?; Ok((name, path))
}
pub fn open_script(
name: &ScriptName,
ty: &ScriptType,
check_exist: Option<bool>,
) -> Result<PathBuf> {
let mut err_in_fallback = None;
let script_path = if check_exist == Some(true) {
let (p, e) = name.to_file_path_fallback(ty);
err_in_fallback = e;
p
} else {
name.to_file_path(ty)?
};
let script_path = get_home().join(script_path);
if let Some(should_exist) = check_exist {
if !script_path.exists() && should_exist {
if let Some(e) = err_in_fallback {
return Err(e);
}
return Err(
Error::PathNotFound(vec![script_path]).context("開腳本失敗:應存在卻不存在")
);
} else if script_path.exists() && !should_exist {
return Err(Error::PathExist(script_path).context("開腳本失敗:不應存在卻存在"));
}
}
Ok(script_path)
}
pub fn get_template_path<T: AsScriptFullTypeRef>(ty: &T) -> Result<PathBuf> {
let p = get_home()
.join(TEMPLATE)
.join(format!("{}{}", ty.display(), HBS_EXT));
if let Some(dir) = p.parent() {
if !dir.exists() {
log::info!("找不到模板資料夾,創建之");
handle_fs_res(&[&dir], create_dir_all(&dir))?;
}
}
Ok(p)
}
pub fn get_sub_types(ty: &ScriptType) -> Result<Vec<ScriptType>> {
let dir = get_home().join(TEMPLATE).join(ty.as_ref());
if !dir.exists() {
log::info!("找不到子類別資料夾,直接回傳");
return Ok(vec![]);
}
let mut subs = vec![];
for entry in handle_fs_res(&[&dir], read_dir(&dir))? {
let name = entry?.file_name();
let name = name
.to_str()
.ok_or_else(|| Error::msg("檔案實體為空...?"))?;
if name.ends_with(HBS_EXT) {
let name = &name[..name.len() - HBS_EXT.len()];
subs.push(name.parse()?);
} else {
log::warn!("發現非模版檔案 {}", name);
}
}
Ok(subs)
}
#[cfg(test)]
mod test {
use super::*;
impl From<&'static str> for ScriptType {
fn from(s: &'static str) -> Self {
ScriptType::new_unchecked(s.to_string())
}
}
#[test]
fn test_anonymous_ids() {
let mut ids = get_anonymous_ids()
.unwrap()
.collect::<Result<Vec<_>>>()
.unwrap();
ids.sort();
assert_eq!(ids, vec![1, 2, 3, 5]);
}
#[test]
fn test_open_anonymous() {
let (name, p) = open_new_anonymous(&"sh".into()).unwrap();
assert_eq!(name, ScriptName::Anonymous(4));
assert_eq!(p, get_test_home().join(".anonymous/4.sh"));
let p = open_script(&5.into_script_name().unwrap(), &"js".into(), None).unwrap();
assert_eq!(p, get_test_home().join(".anonymous/5.js"));
}
#[test]
fn test_open() {
let second_name = "second".to_owned().into_script_name().unwrap();
let not_exist = "not-exist".to_owned().into_script_name().unwrap();
let p = open_script(&second_name, &"rb".into(), Some(false)).unwrap();
assert_eq!(p, get_home().join("second.rb"));
let p = open_script(
&".1".to_owned().into_script_name().unwrap(),
&"sh".into(),
None,
)
.unwrap();
assert_eq!(p, get_test_home().join(".anonymous/1.sh"));
match open_script(¬_exist, &"sh".into(), Some(true)).unwrap_err() {
Error::PathNotFound(name) => assert_eq!(name[0], get_home().join("not-exist.sh")),
_ => unreachable!(),
}
let err = open_script(&second_name, &"no-such-type".into(), None).unwrap_err();
assert!(matches!(err, Error::UnknownType(_)));
let err = open_script(&second_name, &"no-such-type".into(), Some(false)).unwrap_err();
assert!(matches!(err, Error::UnknownType(_)));
let p = open_script(&second_name, &"no-such-type".into(), Some(true)).unwrap();
assert_eq!(p, get_home().join("second.no-such-type"));
let err = open_script(¬_exist, &"no-such-type".into(), Some(true)).unwrap_err();
assert!(matches!(err, Error::UnknownType(_)));
}
}