use configparser::ini::Ini;
use glob::{glob_with, MatchOptions};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
static SIDECAR_FILENAME: &str = ".meta";
pub(crate) struct FileOptions {
databases_read: BTreeMap<PathBuf, SystemTime>,
file_meta: BTreeMap<PathBuf, PresetMeta>,
default: PresetMeta,
}
#[derive(Clone, Debug)]
pub(crate) enum PresetMeta {
Parameters(String),
FullMime(String),
FullHeader(u8, String),
}
impl FileOptions {
pub(crate) fn new(default: PresetMeta) -> Self {
Self {
databases_read: BTreeMap::new(),
file_meta: BTreeMap::new(),
default,
}
}
fn update(&mut self, file: &Path) {
let mut db = if super::ARGS.central_config {
super::ARGS.content_dir.clone()
} else {
file.parent().expect("no parent directory").to_path_buf()
};
db.push(SIDECAR_FILENAME);
let should_read = if let Ok(metadata) = db.metadata() {
if !metadata.is_file() {
false
} else if let (Ok(modified), Some(last_read)) =
(metadata.modified(), self.databases_read.get(&db))
{
&modified >= last_read
} else {
true
}
} else {
false
};
if should_read {
self.read_database(&db);
}
}
fn read_database(&mut self, db: &PathBuf) {
log::trace!("reading database {:?}", db);
let mut ini = Ini::new_cs();
ini.set_default_section("mime");
let map = ini
.load(db.to_str().expect("config path not UTF-8"))
.and_then(|mut sections| {
sections
.remove("mime")
.ok_or_else(|| "no \"mime\" or default section".to_string())
});
self.databases_read
.insert(db.to_path_buf(), SystemTime::now());
let files = match map {
Ok(section) => section,
Err(err) => {
log::error!("invalid config file {:?}: {}", db, err);
return;
}
};
for (rel_path, header) in files {
let header = header.unwrap_or_default();
let mut path = db.clone();
path.pop();
path.push(rel_path);
let preset = if header.is_empty() || header.starts_with(';') {
PresetMeta::Parameters(header.to_string())
} else if matches!(header.chars().next(), Some('1'..='6')) {
if header.len() < 3
|| !header.chars().nth(1).unwrap().is_ascii_digit()
|| !header.chars().nth(2).unwrap().is_whitespace()
{
log::error!("Line for {:?} starts like a full header line, but it is incorrect; ignoring it.", path);
return;
}
let separator = header.chars().nth(2).unwrap();
if separator != ' ' {
log::warn!("Full Header line for {:?} has an invalid character, treating {:?} as a space.", path, separator);
}
let status = header
.chars()
.take(2)
.collect::<String>()
.parse::<u8>()
.unwrap();
let meta = header.chars().skip(3).collect::<String>();
PresetMeta::FullHeader(status, meta)
} else {
PresetMeta::FullMime(header.to_string())
};
let glob_options = MatchOptions {
case_sensitive: true,
require_literal_separator: true,
require_literal_leading_dot: !crate::ARGS.serve_secret,
};
let paths = if let Some(path) = path.to_str() {
match glob_with(path, glob_options) {
Ok(paths) => paths.collect::<Vec<_>>(),
Err(err) => {
log::error!("incorrect glob pattern in {:?}: {}", path, err);
continue;
}
}
} else {
log::error!("path is not UTF-8: {:?}", path);
continue;
};
if paths.is_empty() {
self.file_meta.insert(path, preset);
} else {
for glob_result in paths {
match glob_result {
Ok(path) if path.is_dir() => { }
Ok(path) => {
self.file_meta.insert(path, preset.clone());
}
Err(err) => {
log::warn!("could not process glob path: {}", err);
continue;
}
};
}
}
}
}
pub fn get(&mut self, file: &Path) -> PresetMeta {
self.update(file);
self.file_meta.get(file).unwrap_or(&self.default).clone()
}
pub fn exists(&mut self, file: &Path) -> bool {
self.update(file);
self.file_meta.contains_key(file)
}
}