Initail version

This commit is contained in:
Dawid Ciężarkiewicz 2020-05-01 01:11:21 -07:00 committed by Dawid Ciężarkiewicz
parent bc4c300ce4
commit cb3ade0cdb
10 changed files with 1822 additions and 56 deletions

1269
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,14 @@
[package]
name = "rust-bin-template" # CHANGEME
name = "tagwiki"
version = "0.1.0"
authors = ["Dawid Ciężarkiewicz <dpc@dpc.pw>"] # CHANGEME
authors = ["Dawid Ciężarkiewicz <dpc@dpc.pw>"]
edition = "2018"
readme = "README.md"
description = "A template for Rust programs"
# CHANGEME
documentation = "https://docs.rs/rust-bin-template"
repository = "https://github.com/dpc/rust-bin-template"
homepage = "https://github.com/dpc/rust-bin-template"
documentation = "https://docs.rs/tagwiki"
repository = "https://github.com/dpc/tagwiki"
homepage = "https://github.com/dpc/tagwiki"
keywords = ["template", "cli", "bin"]
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"]}
log = "0.4"
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 = "*"

View File

@ -1,10 +1,9 @@
<!-- CHANGEME below -->
<p align="center">
<a href="https://travis-ci.org/dpc/rust-bin-template">
<img src="https://img.shields.io/travis/dpc/rust-bin-template/master.svg?style=flat-square" alt="Travis CI Build Status">
<a href="https://travis-ci.org/dpc/tagwiki">
<img src="https://img.shields.io/travis/dpc/tagwiki/master.svg?style=flat-square" alt="Travis CI Build Status">
</a>
<a href="https://crates.io/crates/rust-bin-template">
<img src="http://meritbadge.herokuapp.com/rust-bin-template?style=flat-square" alt="crates.io">
<a href="https://crates.io/crates/tagwiki">
<img src="http://meritbadge.herokuapp.com/tagwiki?style=flat-square" alt="crates.io">
</a>
<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">
@ -16,26 +15,7 @@
</p>
<!-- CHANGEME -->
# rust-bin-template
# tagwiki
This is a template meant to be used as a starting point for
Rust programs.
A tag-addressable wiki.
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.

View File

@ -4,8 +4,11 @@ use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, StructOpt, Clone)]
#[structopt(about = "Rust application template")] // CHANGEME
#[structopt(about = "TagWiki")]
#[structopt(global_setting = structopt::clap::AppSettings::ColoredHelp)]
pub struct Opts {
pub path: PathBuf,
#[structopt(long = "port", default_value = "3030")]
pub port: u16,
}

116
src/index.rs Normal file
View File

@ -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()
}
}

View File

@ -1,26 +1,45 @@
// CHANGEME
//! rust-bin-template
//! tagwiki
use anyhow::{Context, Result};
use log::debug;
use std::path::Path;
use anyhow::Result;
use log::info;
use structopt::StructOpt;
use warp::{path::FullPath, Filter};
/// Command line options
mod cli;
/// Page
mod page;
fn open(path: &Path) -> Result<()> {
let _f =
std::fs::File::open(path).with_context(|| format!("Failed to open: {}", path.display()))?;
mod index;
/// 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(())
}
fn main() -> Result<()> {
env_logger::init();
debug!("Parsing command line arguments");
let opts = cli::Opts::from_args();
open(&opts.path)?;
start(&opts)?;
Ok(())
}

165
src/page.rs Normal file
View File

@ -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(())
}

91
src/page/store.rs Normal file
View File

@ -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()))
}
}

131
src/page/store/fs.rs Normal file
View File

@ -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()))
}
}

9
src/util.rs Normal file
View File

@ -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()
}