mirror of https://github.com/dpc/tagwiki
Initail version
This commit is contained in:
parent
bc4c300ce4
commit
cb3ade0cdb
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
|
@ -1,15 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "rust-bin-template" # CHANGEME
|
name = "tagwiki"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Dawid Ciężarkiewicz <dpc@dpc.pw>"] # CHANGEME
|
authors = ["Dawid Ciężarkiewicz <dpc@dpc.pw>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
description = "A template for Rust programs"
|
description = "A template for Rust programs"
|
||||||
|
|
||||||
# CHANGEME
|
documentation = "https://docs.rs/tagwiki"
|
||||||
documentation = "https://docs.rs/rust-bin-template"
|
repository = "https://github.com/dpc/tagwiki"
|
||||||
repository = "https://github.com/dpc/rust-bin-template"
|
homepage = "https://github.com/dpc/tagwiki"
|
||||||
homepage = "https://github.com/dpc/rust-bin-template"
|
|
||||||
keywords = ["template", "cli", "bin"]
|
keywords = ["template", "cli", "bin"]
|
||||||
license = "MPL-2.0 OR MIT OR Apache-2.0"
|
license = "MPL-2.0 OR MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
@ -20,3 +19,13 @@ structopt = "0.3"
|
||||||
env_logger = { version = "0.7.1", default-features = false, features = ["humantime"]}
|
env_logger = { version = "0.7.1", default-features = false, features = ["humantime"]}
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
anyhow = "1.0.26"
|
anyhow = "1.0.26"
|
||||||
|
pulldown-cmark = "0.7.1"
|
||||||
|
tokio = { version = "0.2", features = ["macros"] }
|
||||||
|
warp = "0.2"
|
||||||
|
rand = "0.6"
|
||||||
|
regex = "1.3.7"
|
||||||
|
lazy_static = "*"
|
||||||
|
blake2 = "*"
|
||||||
|
digest = "*"
|
||||||
|
hex = "*"
|
||||||
|
walkdir = "*"
|
||||||
|
|
32
README.md
32
README.md
|
@ -1,10 +1,9 @@
|
||||||
<!-- CHANGEME below -->
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://travis-ci.org/dpc/rust-bin-template">
|
<a href="https://travis-ci.org/dpc/tagwiki">
|
||||||
<img src="https://img.shields.io/travis/dpc/rust-bin-template/master.svg?style=flat-square" alt="Travis CI Build Status">
|
<img src="https://img.shields.io/travis/dpc/tagwiki/master.svg?style=flat-square" alt="Travis CI Build Status">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://crates.io/crates/rust-bin-template">
|
<a href="https://crates.io/crates/tagwiki">
|
||||||
<img src="http://meritbadge.herokuapp.com/rust-bin-template?style=flat-square" alt="crates.io">
|
<img src="http://meritbadge.herokuapp.com/tagwiki?style=flat-square" alt="crates.io">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://matrix.to/#/!VLOvTiaFrBYAYplQFW:mozilla.org">
|
<a href="https://matrix.to/#/!VLOvTiaFrBYAYplQFW:mozilla.org">
|
||||||
<img src="https://img.shields.io/matrix/rust:mozilla.org.svg?server_fqdn=matrix.org&style=flat-square" alt="#rust matrix channel">
|
<img src="https://img.shields.io/matrix/rust:mozilla.org.svg?server_fqdn=matrix.org&style=flat-square" alt="#rust matrix channel">
|
||||||
|
@ -16,26 +15,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<!-- CHANGEME -->
|
# tagwiki
|
||||||
# rust-bin-template
|
|
||||||
|
|
||||||
This is a template meant to be used as a starting point for
|
A tag-addressable wiki.
|
||||||
Rust programs.
|
|
||||||
|
|
||||||
After cloning, grep for `CHANGEME` parts and replace them.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Build-in common functionality:
|
|
||||||
* Command line options handling (`structopt`)
|
|
||||||
* Error management with (`anyhow`)
|
|
||||||
* Logging (`env_logger`)
|
|
||||||
* [Automatic binary releases](//github.com/dpc/rust-bin-template/releases)
|
|
||||||
* Built by Travis CI for every release tag
|
|
||||||
* Easy and fast to set-up
|
|
||||||
* Generated for: Linux, Mac, Windows
|
|
||||||
* Other small features
|
|
||||||
|
|
||||||
### License
|
|
||||||
|
|
||||||
You are free to change the LICENSE of this project.
|
|
||||||
|
|
|
@ -4,8 +4,11 @@ use std::path::PathBuf;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
#[derive(Debug, StructOpt, Clone)]
|
#[derive(Debug, StructOpt, Clone)]
|
||||||
#[structopt(about = "Rust application template")] // CHANGEME
|
#[structopt(about = "TagWiki")]
|
||||||
#[structopt(global_setting = structopt::clap::AppSettings::ColoredHelp)]
|
#[structopt(global_setting = structopt::clap::AppSettings::ColoredHelp)]
|
||||||
pub struct Opts {
|
pub struct Opts {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
#[structopt(long = "port", default_value = "3030")]
|
||||||
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
use crate::page;
|
||||||
|
|
||||||
|
use crate::page::{Id, Tag};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Index<T> {
|
||||||
|
// tag -> page_ids
|
||||||
|
page_ids_by_tag: HashMap<String, HashSet<Id>>,
|
||||||
|
tags_by_page_id: HashMap<Id, Vec<Tag>>,
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FindResults {
|
||||||
|
matching_pages: Vec<Id>,
|
||||||
|
matching_tags: Vec<page::Tag>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindResults {
|
||||||
|
fn empty() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Index<T> {
|
||||||
|
fn find(&self, tags: &[&Tag]) -> FindResults {
|
||||||
|
let mut matching_pages = vec![];
|
||||||
|
let mut matching_tags = vec![];
|
||||||
|
for tag in tags {
|
||||||
|
if matching_tags.is_empty() {
|
||||||
|
if let Some(ids) = self.page_ids_by_tag.get(tag.as_str()) {
|
||||||
|
matching_pages = ids.iter().map(|id| id.to_owned()).collect();
|
||||||
|
} else {
|
||||||
|
return FindResults::empty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(ids) = self.page_ids_by_tag.get(tag.as_str()) {
|
||||||
|
let new_matching_pages: Vec<_> = matching_pages
|
||||||
|
.iter()
|
||||||
|
.filter(|id| ids.contains(id.as_str()))
|
||||||
|
.map(|id| id.to_owned())
|
||||||
|
.collect();
|
||||||
|
if new_matching_pages.is_empty() {
|
||||||
|
return FindResults {
|
||||||
|
matching_pages,
|
||||||
|
matching_tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
matching_pages = new_matching_pages;
|
||||||
|
matching_tags.push(tag.to_string());
|
||||||
|
} else {
|
||||||
|
return FindResults {
|
||||||
|
matching_pages,
|
||||||
|
matching_tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FindResults {
|
||||||
|
matching_pages,
|
||||||
|
matching_tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_data_for_page(&mut self, id: Id) {
|
||||||
|
for tag in self
|
||||||
|
.tags_by_page_id
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| vec![])
|
||||||
|
{
|
||||||
|
self.page_ids_by_tag
|
||||||
|
.get_mut(&tag)
|
||||||
|
.map(|set| set.remove(&id));
|
||||||
|
}
|
||||||
|
self.tags_by_page_id.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> page::StoreMut for Index<T>
|
||||||
|
where
|
||||||
|
T: page::StoreMut,
|
||||||
|
{
|
||||||
|
fn get(&mut self, id: Id) -> Result<page::Parsed> {
|
||||||
|
self.inner.get(id)
|
||||||
|
}
|
||||||
|
fn put(&mut self, page: &page::Parsed) -> Result<()> {
|
||||||
|
self.inner.put( page)?;
|
||||||
|
|
||||||
|
if let Some(_tags) = self.tags_by_page_id.get(&page.headers.id) {
|
||||||
|
self.clean_data_for_page(page.headers.id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in &page.tags {
|
||||||
|
self.page_ids_by_tag
|
||||||
|
.get_mut(tag)
|
||||||
|
.map(|set| set.insert(page.headers.id.clone()));
|
||||||
|
self.tags_by_page_id
|
||||||
|
.insert(page.headers.id.clone(), page.tags.clone());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn delete(&mut self, id: Id) -> Result<()> {
|
||||||
|
self.inner.delete(id.clone())?;
|
||||||
|
self.clean_data_for_page(id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter<'s>(&'s mut self) -> Result<Box<dyn Iterator<Item = Id> + 's>> {
|
||||||
|
self.inner.iter()
|
||||||
|
}
|
||||||
|
}
|
39
src/main.rs
39
src/main.rs
|
@ -1,26 +1,45 @@
|
||||||
// CHANGEME
|
//! tagwiki
|
||||||
//! rust-bin-template
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use log::debug;
|
use log::info;
|
||||||
use std::path::Path;
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
use warp::{path::FullPath, Filter};
|
||||||
|
|
||||||
/// Command line options
|
/// Command line options
|
||||||
mod cli;
|
mod cli;
|
||||||
|
/// Page
|
||||||
|
mod page;
|
||||||
|
|
||||||
fn open(path: &Path) -> Result<()> {
|
mod index;
|
||||||
let _f =
|
|
||||||
std::fs::File::open(path).with_context(|| format!("Failed to open: {}", path.display()))?;
|
/// Utils
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
async fn handler(path: FullPath) -> Result<String, std::convert::Infallible> {
|
||||||
|
let tags: Vec<_> = path
|
||||||
|
.as_str()
|
||||||
|
.split('/')
|
||||||
|
.map(|t| t.trim())
|
||||||
|
.filter(|t| t != &"")
|
||||||
|
.collect();
|
||||||
|
Ok(format!("Path: {:?}", tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(opts: &cli::Opts) -> Result<()> {
|
||||||
|
let handler = warp::path::full().and_then(handler);
|
||||||
|
let serve = warp::serve(handler).run(([127, 0, 0, 1], opts.port));
|
||||||
|
info!("Listening on port {}", opts.port);
|
||||||
|
|
||||||
|
tokio::runtime::Runtime::new().unwrap().block_on(serve);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
debug!("Parsing command line arguments");
|
|
||||||
let opts = cli::Opts::from_args();
|
let opts = cli::Opts::from_args();
|
||||||
|
|
||||||
open(&opts.path)?;
|
start(&opts)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
mod store;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
pub use store::{InMemoryStore, Store, StoreMut};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use digest::Digest;
|
||||||
|
|
||||||
|
pub type Id = String;
|
||||||
|
pub type Tag = String;
|
||||||
|
|
||||||
|
const TAGWIKI_PAGE_ID_KEY: &str = "tagwiki-page-id";
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Source(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Parsed {
|
||||||
|
pub source: Source,
|
||||||
|
pub html: String,
|
||||||
|
pub headers: Headers,
|
||||||
|
pub tags: Vec<Tag>,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_headers_and_body(source: &Source) -> (&str, &str) {
|
||||||
|
lazy_static! {
|
||||||
|
static ref RE: regex::Regex =
|
||||||
|
regex::RegexBuilder::new(r"\A[[:space:]]*<!--+(.*)--+>(.*)\z")
|
||||||
|
.multi_line(true)
|
||||||
|
.dot_matches_new_line(true)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cap) = RE.captures_iter(&source.0).next() {
|
||||||
|
(
|
||||||
|
cap.get(1).expect("be there").as_str(),
|
||||||
|
cap.get(2).expect("be there").as_str(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
("", &source.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Headers {
|
||||||
|
pub id: String,
|
||||||
|
pub all: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Headers {
|
||||||
|
fn parse(headers_str: &str, source: &Source) -> Headers {
|
||||||
|
let mut id = None;
|
||||||
|
|
||||||
|
for line in headers_str.lines() {
|
||||||
|
match line.split(":").collect::<Vec<_>>().as_slice() {
|
||||||
|
[key, value] => {
|
||||||
|
let key = key.trim();
|
||||||
|
let value = value.trim();
|
||||||
|
match key {
|
||||||
|
TAGWIKI_PAGE_ID_KEY => {
|
||||||
|
id = Some(value.to_owned());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match id {
|
||||||
|
Some(id) => Self {
|
||||||
|
id,
|
||||||
|
all: headers_str.to_owned(),
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let mut hasher = blake2::Blake2b::new();
|
||||||
|
hasher.input(&source.0);
|
||||||
|
let res = hasher.result();
|
||||||
|
let id = hex::encode(&res.as_slice()[0..16]);
|
||||||
|
|
||||||
|
let mut all = String::new();
|
||||||
|
all.push_str(TAGWIKI_PAGE_ID_KEY);
|
||||||
|
all.push_str(": ");
|
||||||
|
all.push_str(&id);
|
||||||
|
all.push_str("\n");
|
||||||
|
all.push_str(headers_str);
|
||||||
|
|
||||||
|
Self { id, all }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parsed {
|
||||||
|
fn from_markdown(source: Source) -> Parsed {
|
||||||
|
let (headers, body) = split_headers_and_body(&source);
|
||||||
|
let headers = Headers::parse(headers, &source);
|
||||||
|
|
||||||
|
let parser = pulldown_cmark::Parser::new(body);
|
||||||
|
let mut html_output = String::new();
|
||||||
|
pulldown_cmark::html::push_html(&mut html_output, parser);
|
||||||
|
|
||||||
|
Parsed {
|
||||||
|
headers,
|
||||||
|
html: html_output,
|
||||||
|
source,
|
||||||
|
tags: vec!["TODO".into()],
|
||||||
|
title: "TODO".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_headers_and_body_test() -> Result<()> {
|
||||||
|
let s = Source(
|
||||||
|
r#"
|
||||||
|
|
||||||
|
|
||||||
|
<!------- a: b
|
||||||
|
c: d -->banana"#
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let (headers, body) = split_headers_and_body(&s);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
headers,
|
||||||
|
r#" a: b
|
||||||
|
c: d "#
|
||||||
|
);
|
||||||
|
assert_eq!(body, "banana");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_markdown_metadata_test() -> Result<()> {
|
||||||
|
let page = Parsed::from_markdown(Source(
|
||||||
|
r#"
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
a: b
|
||||||
|
tagwiki-page-id: xyz
|
||||||
|
|
||||||
|
foo-bar: bar
|
||||||
|
-->
|
||||||
|
bar
|
||||||
|
<!---
|
||||||
|
tagwiki-id: 123
|
||||||
|
-->
|
||||||
|
"#
|
||||||
|
.to_owned(),
|
||||||
|
));
|
||||||
|
|
||||||
|
println!("{:#?}", page);
|
||||||
|
assert_eq!(page.headers.id, "xyz");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_to_store(_store: &impl Store, source: Source) -> Result<()> {
|
||||||
|
let _page = Parsed::from_markdown(source);
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
use crate::page::{self, Id};
|
||||||
|
use anyhow::{format_err, Result};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
// use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
pub mod fs;
|
||||||
|
|
||||||
|
pub trait Store {
|
||||||
|
fn get(&self, id: Id) -> Result<page::Parsed>;
|
||||||
|
fn put(&self, page: &page::Parsed) -> Result<()>;
|
||||||
|
fn delete(&self, id: Id) -> Result<()>;
|
||||||
|
fn iter<'s>(&'s self) -> Result<Box<dyn Iterator<Item = Id> + 's>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StoreMut {
|
||||||
|
fn get(&mut self, id: Id) -> Result<page::Parsed>;
|
||||||
|
fn put(&mut self, page: &page::Parsed) -> Result<()>;
|
||||||
|
fn delete(&mut self, id: Id) -> Result<()>;
|
||||||
|
fn iter<'s>(&'s mut self) -> Result<Box<dyn Iterator<Item = Id> + 's>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StoreMut for T
|
||||||
|
where
|
||||||
|
T: Store,
|
||||||
|
{
|
||||||
|
fn get(&mut self, id: Id) -> Result<page::Parsed> {
|
||||||
|
Store::get(self, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put(&mut self, page: &page::Parsed) -> Result<()> {
|
||||||
|
Store::put(self, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&mut self, id: Id) -> Result<()> {
|
||||||
|
Store::delete(self, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter<'s>(&'s mut self) -> Result<Box<dyn Iterator<Item = Id> + 's>> {
|
||||||
|
Store::iter(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl Store for Arc<Mutex<InMemoryStore>> {}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct InMemoryStore {
|
||||||
|
page_by_id: HashMap<Id, page::Parsed>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InMemoryStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
fn inner(&self) -> Result<std::sync::MutexGuard<InMemoryStoreInner>> {
|
||||||
|
self.inner
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format_err!("Lock failed {}", e))
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoreMut for InMemoryStore {
|
||||||
|
fn get(&mut self, id: Id) -> Result<page::Parsed> {
|
||||||
|
Ok(self
|
||||||
|
.page_by_id
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| format_err!("Not found"))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put(&mut self, page: &page::Parsed) -> Result<()> {
|
||||||
|
*self
|
||||||
|
.page_by_id
|
||||||
|
.get_mut(&page.headers.id)
|
||||||
|
.ok_or_else(|| format_err!("Not found"))? = page.clone();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&mut self, id: Id) -> Result<()> {
|
||||||
|
self.page_by_id
|
||||||
|
.remove(&id)
|
||||||
|
.ok_or_else(|| format_err!("Not found"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn iter<'s>(&'s mut self) -> Result<Box<dyn Iterator<Item = Id> + 's>> {
|
||||||
|
Ok(Box::new(self.page_by_id.keys().cloned()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
use crate::page::{self, Id};
|
||||||
|
use anyhow::{format_err, Result};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct FsStore {
|
||||||
|
root_path: PathBuf,
|
||||||
|
id_to_path: HashMap<Id, PathBuf>,
|
||||||
|
path_to_page: HashMap<PathBuf, page::Parsed>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FsStore {
|
||||||
|
pub fn new(root_path: PathBuf) -> Result<Self> {
|
||||||
|
let mut s = Self {
|
||||||
|
root_path,
|
||||||
|
..Self::default()
|
||||||
|
};
|
||||||
|
for entry in walkdir::WalkDir::new(&s.root_path) {
|
||||||
|
match Self::try_reading_page_from_entry(&s.root_path, entry) {
|
||||||
|
Ok(Some((page, path))) => {
|
||||||
|
s.id_to_path.insert(page.headers.id.clone(), path.clone());
|
||||||
|
s.path_to_page.insert(path, page);
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error reading pages: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title_to_new_rel_path(&self, title: &str) -> PathBuf {
|
||||||
|
let mut last_char_was_alphanum = false;
|
||||||
|
let mut path_str = String::new();
|
||||||
|
for ch in title.chars() {
|
||||||
|
let is_alphanum = ch.is_alphanumeric();
|
||||||
|
|
||||||
|
match (is_alphanum, last_char_was_alphanum) {
|
||||||
|
(true, _) => {
|
||||||
|
path_str.push(ch);
|
||||||
|
}
|
||||||
|
(false, true) => {
|
||||||
|
path_str.push('-');
|
||||||
|
}
|
||||||
|
(false, false) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
last_char_was_alphanum = is_alphanum;
|
||||||
|
}
|
||||||
|
|
||||||
|
let initial_title = path_str.clone();
|
||||||
|
let mut path = PathBuf::from(&initial_title);
|
||||||
|
let mut i = 1;
|
||||||
|
while let Some(_) = self.path_to_page.get(&path) {
|
||||||
|
path = PathBuf::from(format!("{}-{}", &initial_title, i));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_reading_page_from_entry(
|
||||||
|
root_path: &Path,
|
||||||
|
entry: walkdir::Result<walkdir::DirEntry>,
|
||||||
|
) -> Result<Option<(page::Parsed, PathBuf)>> {
|
||||||
|
let entry = entry?;
|
||||||
|
if !entry.file_type().is_file() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.path().extension() != Some(&OsString::from("md")) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = std::fs::File::open(PathBuf::from(root_path).join(entry.path()))?;
|
||||||
|
let mut reader = std::io::BufReader::new(file);
|
||||||
|
let mut source = page::Source::default();
|
||||||
|
reader.read_to_string(&mut source.0)?;
|
||||||
|
|
||||||
|
Ok(Some((
|
||||||
|
page::Parsed::from_markdown(source),
|
||||||
|
entry.path().to_owned(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_page_to_file(&self, _rel_path: &Path, _page: &page::Parsed) -> Result<()> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl page::StoreMut for FsStore {
|
||||||
|
fn get(&mut self, id: Id) -> Result<page::Parsed> {
|
||||||
|
self.id_to_path
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|path| self.path_to_page.get(path).cloned())
|
||||||
|
.ok_or_else(|| format_err!("Not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put(&mut self, page: &page::Parsed) -> Result<()> {
|
||||||
|
let path = if let Some(path) = self.id_to_path.get(&page.headers.id) {
|
||||||
|
path.clone()
|
||||||
|
} else {
|
||||||
|
self.title_to_new_rel_path(&page.title)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.write_page_to_file(&path, &page)?;
|
||||||
|
self.id_to_path
|
||||||
|
.insert(page.headers.id.clone(), path.clone());
|
||||||
|
self.path_to_page.insert(path, page.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&mut self, id: Id) -> Result<()> {
|
||||||
|
let path = self
|
||||||
|
.id_to_path
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| format_err!("Not found"))?;
|
||||||
|
self.path_to_page.remove(&path);
|
||||||
|
self.id_to_path.remove(&id);
|
||||||
|
std::fs::remove_file(self.root_path.join(path))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter<'s>(&'s mut self) -> Result<Box<dyn Iterator<Item = Id> + 's>> {
|
||||||
|
Ok(Box::new(self.id_to_path.keys().cloned()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
use rand::distributions::Alphanumeric;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
pub fn random_string(len: usize) -> String {
|
||||||
|
rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(len)
|
||||||
|
.collect()
|
||||||
|
}
|
Loading…
Reference in New Issue