use {
crate::Page,
std::{
collections::HashMap,
fs::{self, File},
io::{self, Write},
path::{Path, PathBuf},
},
};
pub type Result<T> = std::result::Result<T, std::io::Error>;
pub trait ReqWithDB {
fn db(&self) -> &DB;
}
impl ReqWithDB for vial::Request {
fn db(&self) -> &DB {
self.state::<DB>()
}
}
pub struct DB {
root: String,
}
unsafe impl Sync for DB {}
unsafe impl Send for DB {}
impl DB {
pub fn new<S: AsRef<str>>(root: S) -> DB {
DB {
root: root.as_ref().to_string(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn len(&self) -> usize {
if let Ok(res) = shell!("ls -R -1 {} | grep '\\.md' | wc -l", self.root) {
res.trim().parse::<usize>().unwrap_or(0)
} else {
0
}
}
pub fn find(&self, name: &str) -> Option<Page> {
let path = self.pathify(name);
self.pages()
.unwrap_or_else(|_| vec![])
.into_iter()
.find(|p| p.path() == path)
}
pub fn exists(&self, name: &str) -> bool {
self.find(name).is_some()
}
pub fn pages(&self) -> Result<Vec<Page>> {
Ok(shell!("find {} -type f -name '*.md' | sort", self.root)?
.trim()
.split('\n')
.filter(|line| !line.trim().is_empty())
.map(|line| Page::new(&self.root, line.trim().replace("//", "/")))
.collect())
}
pub fn names(&self) -> Result<Vec<String>> {
let mut names: Vec<_> = self.pages()?.iter().map(|p| p.name().to_string()).collect();
names.sort();
Ok(names)
}
pub fn titles(&self) -> Result<Vec<String>> {
let mut names: Vec<_> = self.pages()?.iter().map(|p| p.title()).collect();
names.sort();
Ok(names)
}
pub fn recent(&self) -> Result<Vec<Page>> {
if !self.is_git() {
return Ok(vec![]);
}
let out = shell!(
r#"git --git-dir={:?} log --pretty=format: --name-only -n 30 | grep "\.md\$""#,
self.git_dir()
)?;
let mut pages = vec![];
let mut seen = HashMap::new();
for path in out.split("\n") {
if seen.get(path).is_some() || path == ".md" || path.is_empty() {
continue;
} else {
pages.push(Page::new(&self.root, path));
seen.insert(path, true);
}
}
Ok(pages)
}
pub fn tags(&self) -> Result<Vec<String>> {
if !self.is_git() {
return Ok(vec![]);
}
let out = match shell!(
"grep --exclude-dir {:?} -I -E -h -o -r '#(\\w+)' {} | sort | uniq",
self.git_dir(),
self.root
) {
Err(e) => {
eprintln!("EGREP ERROR: {}", e);
return Err(e);
}
Ok(out) => out,
};
Ok(out
.split('\n')
.filter_map(|s| {
if s.is_empty() {
None
} else {
Some(s[1..].to_string())
}
})
.collect::<Vec<_>>())
}
pub fn find_pages_with_tag(&self, tag: &str) -> Result<Vec<Page>> {
let tag = if tag.starts_with('#') {
tag.to_string()
} else {
format!("#{}", tag)
};
let out = shell!(
"grep --exclude-dir {:?} -l -r '{}' {}",
self.git_dir(),
tag,
self.root
)?;
Ok(out
.split("\n")
.filter_map(|line| {
if !line.is_empty() {
Some(Page::new(&self.root, line.split(':').next().unwrap_or("?")))
} else {
None
}
})
.collect::<Vec<_>>())
}
pub fn create(&self, name: &str, body: &str) -> Result<Page> {
let path = self.pathify(name);
if self.exists(&path) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("Already Exists: {}", path),
));
}
if path.contains('/') {
if let Some(dir) = Path::new(&path).parent() {
fs::create_dir_all(&dir.display().to_string())?;
}
}
let mut file = File::create(&path)?;
write!(file, "{}", body)?;
Ok(Page::new(&self.root, path))
}
pub fn update(&self, name: &str, body: &str) -> Result<Page> {
if !self.exists(name) {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Doesn't exist: {}", name),
));
}
let path = self.pathify(name);
let tmp = format!("{}~", path);
let mut file = File::create(&tmp)?;
file.write_all(body.as_bytes())?;
fs::rename(tmp, &path)?;
Ok(Page::new(&self.root, path))
}
pub fn absolute_path(&self, path: &str) -> String {
let path = if path.ends_with(".html") && !path.starts_with("html/") {
format!("html/{}", path)
} else {
path.to_string()
};
format!(
"{}{}",
self.root,
path.trim_start_matches('/').replace("..", ".")
)
}
pub fn is_git(&self) -> bool {
self.git_dir().exists()
}
fn git_dir(&self) -> PathBuf {
Path::new(&format!("{}.git", self.root)).to_path_buf()
}
fn pathify(&self, path: &str) -> String {
if path.ends_with(".md") {
path.into()
} else {
self.absolute_path(&format!("{}.md", Self::title_to_name(path)))
}
}
pub fn title_to_name(title: &str) -> String {
title
.trim()
.replace(" ", "_")
.chars()
.filter(|&c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-' || c == '/')
.collect::<String>()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_len() {
let db = DB::new("./wiki/");
assert_eq!(6, db.len());
assert_eq!(false, db.is_empty());
let db = DB::new("./src/");
assert_eq!(0, db.len());
assert_eq!(true, db.is_empty());
}
#[test]
fn test_pages() {
let db = DB::new("./wiki/");
let pages = db.pages().unwrap();
assert_eq!("TODO", pages[0].name());
assert_eq!("TODO", pages[0].title());
assert_eq!("keyboard_shortcuts", pages[1].name());
assert_eq!("Keyboard Shortcuts", pages[1].title());
shell!("mkdir -p ./wiki/empty").unwrap();
let db = DB::new("./wiki/empty");
let pages = db.pages().unwrap();
println!("{:?}", pages);
assert_eq!(0, pages.len());
shell!("rm -rf ./wiki/empty").unwrap();
}
#[test]
fn test_pathify() {
let db = DB::new("./wiki/");
assert_eq!(
"./wiki/keyboard_shortcuts.md",
db.pathify("keyboard shortcuts")
);
assert_eq!("./wiki/Coool.md", db.pathify("Coool!!"));
}
#[test]
fn test_exists() {
let db = DB::new("./wiki/");
let kb = db.find("keyboard shortcuts").unwrap();
assert_eq!("Keyboard Shortcuts", kb.title());
assert!(db.exists("keyboard shortcuts"));
}
#[test]
fn test_absolute() {
let db = DB::new("./wiki/");
assert_eq!("./wiki/rip.gif", db.absolute_path("rip.gif"));
assert_eq!("./wiki/./rip.gif", db.absolute_path("../rip.gif"));
assert_eq!(
"./wiki/long/path/to/img.JPEG",
db.absolute_path("long/path/to/img.JPEG")
);
}
#[test]
fn test_is_git() {
let db = DB::new("./wiki/");
assert!(!db.is_git());
let db = DB::new("./");
assert!(db.is_git());
}
#[test]
fn test_git_dir() {
let db = DB::new("./wiki/");
assert_eq!("./wiki/.git", db.git_dir().to_str().unwrap());
}
}