mirror of
https://github.com/dpc/tagwiki
synced 2025-04-08 08:36:40 +00:00
Initail version
This commit is contained in:
parent
bc4c300ce4
commit
cb3ade0cdb
1269
Cargo.lock
generated
1269
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@ -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 = "*"
|
||||
|
32
README.md
32
README.md
@ -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.
|
||||
|
@ -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
116
src/index.rs
Normal 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()
|
||||
}
|
||||
}
|
39
src/main.rs
39
src/main.rs
@ -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
165
src/page.rs
Normal 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
91
src/page/store.rs
Normal 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
131
src/page/store/fs.rs
Normal 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
9
src/util.rs
Normal 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()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user