use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use crate::error::{Error, Issue, Result, code};
use crate::meta::{Meta, MetaLoad, load};
use crate::util::{self, META_FILE};
pub const HARD_MAX_DEPTH: usize = 128;
pub struct Visit {
pub dir: PathBuf,
pub rel: String,
pub depth: usize,
pub meta: Option<Box<Meta>>,
pub parent: Option<usize>,
pub readable: bool,
}
pub struct Scan {
pub root: PathBuf,
pub visits: Vec<Visit>,
pub issues: Vec<Issue>,
pub by_id: HashMap<String, Vec<usize>>,
pub root_index: Option<usize>,
pub max_depth: usize,
}
impl Scan {
pub fn resolve(&self, id: &str) -> Option<usize> {
self.by_id.get(id).and_then(|v| v.first().copied())
}
pub fn ancestors(&self, mut idx: usize) -> Vec<usize> {
let mut out = vec![idx];
while let Some(p) = self.visits[idx].parent {
out.push(p);
idx = p;
}
out.reverse();
out
}
}
#[derive(Debug, Clone)]
pub struct Bundle {
pub root: PathBuf,
}
impl Bundle {
pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
let root = root.into();
if !root.exists() {
return Err(Error::NotFound(root.display().to_string()));
}
if !root.is_dir() {
return Err(Error::BadArg(format!(
"{} 不是目录(`.str` 是目录 bundle)",
root.display()
)));
}
let root = std::fs::canonicalize(&root).map_err(|e| Error::io(&root, e))?;
Ok(Self { root })
}
pub fn name(&self) -> String {
self.root
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| self.root.display().to_string())
}
pub fn meta_path(&self, dir: &Path) -> PathBuf {
dir.join(META_FILE)
}
pub fn has_meta(&self, dir: &Path) -> bool {
self.meta_path(dir).is_file()
}
pub fn rel(&self, path: &Path) -> String {
util::rel_display(&self.root, path)
}
pub fn read_meta(&self, dir: &Path) -> Result<MetaLoad> {
let p = self.meta_path(dir);
let rel = self.rel(&p);
load(&p, &rel)
}
pub fn list_names(&self, dir: &Path) -> Result<Vec<(String, bool)>> {
let mut out = Vec::new();
let rd = std::fs::read_dir(dir).map_err(|e| Error::io(dir, e))?;
for ent in rd {
let ent = ent.map_err(|e| Error::io(dir, e))?;
let name = ent.file_name().to_string_lossy().to_string();
let is_dir = ent
.file_type()
.map(|t| t.is_dir())
.unwrap_or(false);
out.push((name, is_dir));
}
out.sort();
Ok(out)
}
pub fn child_dirs(&self, dir: &Path) -> Result<Vec<PathBuf>> {
Ok(self
.list_names(dir)?
.into_iter()
.filter(|(_, is_dir)| *is_dir)
.map(|(n, _)| dir.join(n))
.collect())
}
fn contains_meta_deeper(&self, dir: &Path) -> bool {
for entry in walkdir::WalkDir::new(dir)
.max_depth(8)
.into_iter()
.filter_entry(|e| {
if e.depth() == 0 {
return true;
}
let name = e.file_name().to_string_lossy().to_string();
!util::is_os_noise(&name) && !util::is_sub_bundle(&name)
})
.flatten()
{
if entry.depth() == 0 || !entry.file_type().is_dir() {
continue;
}
if entry.path().join(META_FILE).is_file() {
return true;
}
}
false
}
pub fn scan(&self) -> Result<Scan> {
let mut visits: Vec<Visit> = Vec::new();
let mut issues: Vec<Issue> = Vec::new();
let mut by_id: HashMap<String, Vec<usize>> = HashMap::new();
let root_meta = self.meta_path(&self.root);
if !root_meta.is_file() {
issues.push(Issue::error(
code::META_MISSING,
".",
"bundle 根目录缺少 `._meta`",
));
return Ok(Scan {
root: self.root.clone(),
visits,
issues,
by_id,
root_index: None,
max_depth: 0,
});
}
let (root_parsed, mut root_issues) = match self.read_meta(&self.root)? {
MetaLoad::Ok(m, i) => (Some(m), i),
MetaLoad::Failed(i) => (None, i),
};
issues.append(&mut root_issues);
if let Some(m) = &root_parsed {
if let Some(id) = &m.id {
by_id.entry(id.clone()).or_default().push(0);
}
}
visits.push(Visit {
dir: self.root.clone(),
rel: ".".to_string(),
depth: 0,
meta: root_parsed,
parent: None,
readable: true,
});
let mut queue: VecDeque<usize> = VecDeque::from([0usize]);
let mut max_depth = 0usize;
while let Some(cur) = queue.pop_front() {
let dir = visits[cur].dir.clone();
let depth = visits[cur].depth;
if depth >= HARD_MAX_DEPTH {
continue;
}
for child in self.child_dirs(&dir)? {
let name = child
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
if util::is_os_noise(&name) {
continue;
}
if util::is_sub_bundle(&name) {
continue;
}
let rel = self.rel(&child);
if !self.has_meta(&child) {
if !util::is_reserved_name(&name) && self.contains_meta_deeper(&child) {
issues.push(Issue::error(
code::META_MISSING,
rel,
"父目录不是分支(缺少 `._meta`),其内出现 `._meta`,无法建立分支层级",
));
}
continue;
}
let d = depth + 1;
let (parsed, mut iss) = match self.read_meta(&child)? {
MetaLoad::Ok(m, i) => (Some(m), i),
MetaLoad::Failed(i) => (None, i),
};
issues.append(&mut iss);
let idx = visits.len();
if let Some(m) = &parsed {
if let Some(id) = &m.id {
by_id.entry(id.clone()).or_default().push(idx);
}
}
visits.push(Visit {
dir: child.clone(),
rel,
depth: d,
meta: parsed,
parent: Some(cur),
readable: true,
});
max_depth = max_depth.max(d);
queue.push_back(idx);
}
}
Ok(Scan {
root: self.root.clone(),
visits,
issues,
by_id,
root_index: Some(0),
max_depth,
})
}
}