feat: Read site repository state
Add a `read` method to Site, Post, and Page that reads the current state of the site repositor/post/page off of the disk. Change to using btree collections for deterministic ordering, and make Site, Post, and Page `Eq` for testability purposes.
This commit is contained in:
parent
fc817f1375
commit
8bdfcaa1d8
|
@ -389,6 +389,15 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -512,6 +521,15 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-lifetimes"
|
name = "io-lifetimes"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
@ -519,7 +537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3"
|
checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -531,7 +549,7 @@ dependencies = [
|
||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -849,6 +867,15 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
|
@ -890,7 +917,16 @@ dependencies = [
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys",
|
"windows-sys 0.45.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -997,8 +1033,10 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_dhall",
|
"serde_dhall",
|
||||||
"snafu",
|
"snafu",
|
||||||
|
"tempfile",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1018,6 +1056,19 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"fastrand",
|
||||||
|
"redox_syscall",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.42.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -1229,6 +1280,16 @@ version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.10.0+wasi-snapshot-preview1"
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
@ -1320,6 +1381,21 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.42.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
|
|
|
@ -17,3 +17,7 @@ serde_dhall = { version = "0.12.1", default-features = false }
|
||||||
snafu = "0.7.4"
|
snafu = "0.7.4"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
|
walkdir = "2.3.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.4.0"
|
||||||
|
|
352
src/site.rs
352
src/site.rs
|
@ -1,14 +1,15 @@
|
||||||
//! Management of on-disk layout of the source of a site
|
//! Management of on-disk layout of the source of a site
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{BTreeMap, BTreeSet},
|
||||||
fs::{create_dir_all, File},
|
fs::{create_dir_all, File},
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use snafu::{ensure, ResultExt, Snafu};
|
use snafu::{ensure, ResultExt, Snafu};
|
||||||
use tracing::{debug, info, info_span};
|
use tracing::{debug, info, info_span, warn};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use self::{config::Config, page::Page, post::Post};
|
use self::{config::Config, page::Page, post::Post};
|
||||||
|
|
||||||
|
@ -16,108 +17,37 @@ pub mod config;
|
||||||
pub mod page;
|
pub mod page;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
|
|
||||||
/// Error encountered interacting with a [`Site`]
|
|
||||||
#[derive(Snafu, Debug)]
|
|
||||||
#[snafu(visibility(pub(crate)))]
|
|
||||||
pub enum SiteError {
|
|
||||||
/// Error checking to see if a path exists
|
|
||||||
#[snafu(display("Error checking if path exists: {:?}", path))]
|
|
||||||
ExistanceCheck {
|
|
||||||
/// Path being checked
|
|
||||||
path: PathBuf,
|
|
||||||
/// Underlying error
|
|
||||||
source: std::io::Error,
|
|
||||||
},
|
|
||||||
/// Error creating a directory
|
|
||||||
#[snafu(display("Error creating directory:: {:?}", path))]
|
|
||||||
CreateDirectory {
|
|
||||||
/// Path being checked
|
|
||||||
path: PathBuf,
|
|
||||||
/// Underlying error
|
|
||||||
source: std::io::Error,
|
|
||||||
},
|
|
||||||
/// Site repository dir was not a directory
|
|
||||||
#[snafu(display("Site repository path was not a directory: {:?}", path))]
|
|
||||||
NotADirectory {
|
|
||||||
/// Path being checked
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
/// Failed to reify the configuration
|
|
||||||
ConfigReify {
|
|
||||||
/// The underlying error
|
|
||||||
#[snafu(source(from(serde_dhall::Error, Box::new)))]
|
|
||||||
source: Box<serde_dhall::Error>,
|
|
||||||
},
|
|
||||||
/// Failed to write out the configuration
|
|
||||||
WriteConfig {
|
|
||||||
/// The path we tried to write the config to
|
|
||||||
path: PathBuf,
|
|
||||||
/// The underlying error
|
|
||||||
source: std::io::Error,
|
|
||||||
},
|
|
||||||
/// Failed to write out a page stub
|
|
||||||
WritePageStub {
|
|
||||||
/// The path we tried to write the page stub to
|
|
||||||
path: PathBuf,
|
|
||||||
/// The underlying error
|
|
||||||
source: std::io::Error,
|
|
||||||
},
|
|
||||||
/// Error writing out a page
|
|
||||||
#[snafu(display("Error writing out page {:?}", page))]
|
|
||||||
PageWrite {
|
|
||||||
/// The page we were trying to write out
|
|
||||||
page: PathBuf,
|
|
||||||
/// The underlying error
|
|
||||||
#[snafu(source(from(SiteError, Box::new)))]
|
|
||||||
source: Box<SiteError>,
|
|
||||||
},
|
|
||||||
/// Failed to write out a post stub
|
|
||||||
WritePostStub {
|
|
||||||
/// The path we tried to write the page stub to
|
|
||||||
path: PathBuf,
|
|
||||||
/// The underlying error
|
|
||||||
source: std::io::Error,
|
|
||||||
},
|
|
||||||
/// Error writing out a post
|
|
||||||
#[snafu(display("Error writing out post {:?}", post))]
|
|
||||||
PostWrite {
|
|
||||||
/// The page we were trying to write out
|
|
||||||
post: PathBuf,
|
|
||||||
/// The underlying error
|
|
||||||
#[snafu(source(from(SiteError, Box::new)))]
|
|
||||||
source: Box<SiteError>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of the on-disk structure of a site
|
/// Representation of the on-disk structure of a site
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct Site {
|
pub struct Site {
|
||||||
/// Top level configuration
|
/// Top level configuration
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
/// Non-post static pages
|
/// Non-post static pages
|
||||||
pub pages: HashMap<PathBuf, Page>,
|
pub pages: BTreeMap<PathBuf, Page>,
|
||||||
/// Posts
|
/// Posts
|
||||||
pub posts: HashMap<PathBuf, Post>,
|
pub posts: BTreeMap<PathBuf, Post>,
|
||||||
/// Static content
|
/// Static content
|
||||||
///
|
///
|
||||||
/// Path is relative to `statics` directory
|
/// Path is relative to `statics` directory
|
||||||
pub statics: Vec<PathBuf>,
|
pub statics: BTreeSet<PathBuf>,
|
||||||
/// Stylesheets
|
/// Stylesheets
|
||||||
pub styles: Vec<PathBuf>,
|
///
|
||||||
|
/// Path is relative to the styles directory
|
||||||
|
pub styles: BTreeSet<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Site {
|
impl Default for Site {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut pages = HashMap::new();
|
let mut pages = BTreeMap::new();
|
||||||
pages.insert("index".into(), Page::default());
|
pages.insert("index".into(), Page::default());
|
||||||
let mut posts = HashMap::new();
|
let mut posts = BTreeMap::new();
|
||||||
posts.insert("new-blog-who-this".into(), Post::default());
|
posts.insert("new-blog-who-this".into(), Post::default());
|
||||||
Self {
|
Self {
|
||||||
config: Config::default(),
|
config: Config::default(),
|
||||||
pages,
|
pages,
|
||||||
posts,
|
posts,
|
||||||
statics: Vec::new(),
|
statics: BTreeSet::new(),
|
||||||
styles: Vec::new(),
|
styles: BTreeSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,7 +59,7 @@ impl Site {
|
||||||
///
|
///
|
||||||
/// Will return an error if serialization fails, the user does not have write permissions for
|
/// Will return an error if serialization fails, the user does not have write permissions for
|
||||||
/// the directory, or any other IO errors occur while writing out the config.
|
/// the directory, or any other IO errors occur while writing out the config.
|
||||||
pub fn write(&self, site_dir: impl AsRef<Path>) -> Result<(), SiteError> {
|
pub fn write(&mut self, site_dir: impl AsRef<Path>) -> Result<(), SiteError> {
|
||||||
let site_dir = site_dir.as_ref();
|
let site_dir = site_dir.as_ref();
|
||||||
// Setup logging
|
// Setup logging
|
||||||
let span = info_span!("Site::write", ?site_dir);
|
let span = info_span!("Site::write", ?site_dir);
|
||||||
|
@ -185,6 +115,8 @@ impl Site {
|
||||||
// Touch a .gitkeep
|
// Touch a .gitkeep
|
||||||
let git_keep = statics_dir.join(".gitkeep");
|
let git_keep = statics_dir.join(".gitkeep");
|
||||||
File::create(&git_keep).context(CreateDirectorySnafu { path: &git_keep })?;
|
File::create(&git_keep).context(CreateDirectorySnafu { path: &git_keep })?;
|
||||||
|
// Add the .gitkeep to the state
|
||||||
|
self.statics.insert(PathBuf::from(".gitkeep"));
|
||||||
}
|
}
|
||||||
// Create the gitignore
|
// Create the gitignore
|
||||||
let git_ignore_path = site_dir.join(".gitignore");
|
let git_ignore_path = site_dir.join(".gitignore");
|
||||||
|
@ -223,6 +155,7 @@ impl Site {
|
||||||
.context(WriteConfigSnafu {
|
.context(WriteConfigSnafu {
|
||||||
path: &default_path,
|
path: &default_path,
|
||||||
})?;
|
})?;
|
||||||
|
self.styles.insert(PathBuf::from("default.css"));
|
||||||
// index
|
// index
|
||||||
let index_path = styles_dir.join("index.css");
|
let index_path = styles_dir.join("index.css");
|
||||||
debug!(?index_path, "Copying over index.css");
|
debug!(?index_path, "Copying over index.css");
|
||||||
|
@ -231,6 +164,7 @@ impl Site {
|
||||||
index
|
index
|
||||||
.write_all(include_bytes!("../assets/css/index.css"))
|
.write_all(include_bytes!("../assets/css/index.css"))
|
||||||
.context(WriteConfigSnafu { path: &index_path })?;
|
.context(WriteConfigSnafu { path: &index_path })?;
|
||||||
|
self.styles.insert(PathBuf::from("index.css"));
|
||||||
// post
|
// post
|
||||||
let post_path = styles_dir.join("post.css");
|
let post_path = styles_dir.join("post.css");
|
||||||
debug!(?post_path, "Copying over post.css");
|
debug!(?post_path, "Copying over post.css");
|
||||||
|
@ -238,8 +172,256 @@ impl Site {
|
||||||
File::create(&post_path).context(WriteConfigSnafu { path: &post_path })?;
|
File::create(&post_path).context(WriteConfigSnafu { path: &post_path })?;
|
||||||
post.write_all(include_bytes!("../assets/css/post.css"))
|
post.write_all(include_bytes!("../assets/css/post.css"))
|
||||||
.context(WriteConfigSnafu { path: &post_path })?;
|
.context(WriteConfigSnafu { path: &post_path })?;
|
||||||
|
self.styles.insert(PathBuf::from("post.css"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads out the configuration state for a `Site` from the provided directory
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return an error if the state of the given site is invalid, or of any other IO errors
|
||||||
|
/// occur while reading in the state
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub fn read(site_dir: impl AsRef<Path>) -> Result<Site, SiteError> {
|
||||||
|
let site_dir = site_dir.as_ref();
|
||||||
|
// Setup the logging
|
||||||
|
let span = info_span!("Site::read", ?site_dir);
|
||||||
|
let _enter = span.enter();
|
||||||
|
info!("Reading site repository state");
|
||||||
|
|
||||||
|
// Read the configuration file
|
||||||
|
let config_path = site_dir.join("config.dhall");
|
||||||
|
debug!(?config_path, "Reading main site configuration file");
|
||||||
|
let config: Config = serde_dhall::from_file(&config_path)
|
||||||
|
.parse()
|
||||||
|
.context(ConfigReadSnafu { path: &config_path })?;
|
||||||
|
|
||||||
|
// Read in the statics
|
||||||
|
let mut statics = BTreeSet::new();
|
||||||
|
let statics_dir = site_dir.join("statics");
|
||||||
|
let statics_iter = WalkDir::new(&statics_dir)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(std::result::Result::ok)
|
||||||
|
.filter(|e| e.path().is_file());
|
||||||
|
for static_path in statics_iter {
|
||||||
|
statics.insert(
|
||||||
|
static_path
|
||||||
|
.path()
|
||||||
|
.strip_prefix(&statics_dir)
|
||||||
|
.expect("Invalid static?")
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read in the styles
|
||||||
|
let mut styles = BTreeSet::new();
|
||||||
|
let styles_dir = site_dir.join("styles");
|
||||||
|
let styles_iter = WalkDir::new(&styles_dir)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(std::result::Result::ok)
|
||||||
|
.filter(|e| e.path().is_file());
|
||||||
|
for style_path in styles_iter {
|
||||||
|
styles.insert(
|
||||||
|
style_path
|
||||||
|
.path()
|
||||||
|
.strip_prefix(&styles_dir)
|
||||||
|
.expect("Invalid style?")
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read in the pages
|
||||||
|
let mut pages = BTreeMap::new();
|
||||||
|
let pages_dir = site_dir.join("pages");
|
||||||
|
debug!(?pages_dir, "Reading in pages");
|
||||||
|
for dir in
|
||||||
|
std::fs::read_dir(&pages_dir).context(EnumerateDirectorySnafu { path: &pages_dir })?
|
||||||
|
{
|
||||||
|
let dir = dir.context(EnumerateDirectorySnafu { path: &pages_dir })?;
|
||||||
|
let dir_path = dir.path();
|
||||||
|
// Make sure it is a directory
|
||||||
|
if dir
|
||||||
|
.metadata()
|
||||||
|
.context(EnumerateDirectorySnafu { path: &pages_dir })?
|
||||||
|
.is_dir()
|
||||||
|
{
|
||||||
|
// Extract the final component
|
||||||
|
if let Some(page_dir) = dir_path.file_name() {
|
||||||
|
let page =
|
||||||
|
Page::read(site_dir, page_dir).context(PageReadSnafu { page: page_dir })?;
|
||||||
|
pages.insert(PathBuf::from(page_dir), page);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
?dir_path,
|
||||||
|
"Path in pages directory did not have a final component? ignorning"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
?dir_path,
|
||||||
|
"Item in pages directory was not a directory, ignoring"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read in the posts
|
||||||
|
let mut posts = BTreeMap::new();
|
||||||
|
let posts_dir = site_dir.join("posts");
|
||||||
|
debug!(?posts_dir, "Reading in posts");
|
||||||
|
for dir in
|
||||||
|
std::fs::read_dir(&posts_dir).context(EnumerateDirectorySnafu { path: &posts_dir })?
|
||||||
|
{
|
||||||
|
let dir = dir.context(EnumerateDirectorySnafu { path: &posts_dir })?;
|
||||||
|
let dir_path = dir.path();
|
||||||
|
// Make sure it is a directory
|
||||||
|
if dir
|
||||||
|
.metadata()
|
||||||
|
.context(EnumerateDirectorySnafu { path: &posts_dir })?
|
||||||
|
.is_dir()
|
||||||
|
{
|
||||||
|
// Extract the final component
|
||||||
|
if let Some(post_dir) = dir_path.file_name() {
|
||||||
|
let post =
|
||||||
|
Post::read(site_dir, post_dir).context(PostReadSnafu { post: post_dir })?;
|
||||||
|
posts.insert(PathBuf::from(post_dir), post);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
?dir_path,
|
||||||
|
"Path in posts directory did not have a final component? ignorning"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
?dir_path,
|
||||||
|
"Item in posts directory was not a directory, ignoring"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Site {
|
||||||
|
config,
|
||||||
|
pages,
|
||||||
|
posts,
|
||||||
|
statics,
|
||||||
|
styles,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Error encountered interacting with a [`Site`]
|
||||||
|
#[derive(Snafu, Debug)]
|
||||||
|
#[snafu(visibility(pub(crate)))]
|
||||||
|
pub enum SiteError {
|
||||||
|
/// Error checking to see if a path exists
|
||||||
|
#[snafu(display("Error checking if path exists: {:?}", path))]
|
||||||
|
ExistanceCheck {
|
||||||
|
/// Path being checked
|
||||||
|
path: PathBuf,
|
||||||
|
/// Underlying error
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
/// Error creating a directory
|
||||||
|
#[snafu(display("Error creating directory:: {:?}", path))]
|
||||||
|
CreateDirectory {
|
||||||
|
/// Path being checked
|
||||||
|
path: PathBuf,
|
||||||
|
/// Underlying error
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
/// Site repository dir was not a directory
|
||||||
|
#[snafu(display("Site repository path was not a directory: {:?}", path))]
|
||||||
|
NotADirectory {
|
||||||
|
/// Path being checked
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
/// Failed to enumerate a directory
|
||||||
|
#[snafu(display("Failed to enumerate directory: {:?}", path))]
|
||||||
|
EnumerateDirectory {
|
||||||
|
/// The directory being enumerated
|
||||||
|
path: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
/// Failed to reify the configuration
|
||||||
|
ConfigReify {
|
||||||
|
/// The underlying error
|
||||||
|
#[snafu(source(from(serde_dhall::Error, Box::new)))]
|
||||||
|
source: Box<serde_dhall::Error>,
|
||||||
|
},
|
||||||
|
/// Failed to read the configuration
|
||||||
|
#[snafu(display("Failed to read configuration at: {:?}", path))]
|
||||||
|
ConfigRead {
|
||||||
|
/// The path of the configuration file being read
|
||||||
|
path: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
#[snafu(source(from(serde_dhall::Error, Box::new)))]
|
||||||
|
source: Box<serde_dhall::Error>,
|
||||||
|
},
|
||||||
|
/// Failed to write out the configuration
|
||||||
|
WriteConfig {
|
||||||
|
/// The path we tried to write the config to
|
||||||
|
path: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
/// Failed to write out a page stub
|
||||||
|
WritePageStub {
|
||||||
|
/// The path we tried to write the page stub to
|
||||||
|
path: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
/// Error writing out a page
|
||||||
|
#[snafu(display("Error writing out page {:?}", page))]
|
||||||
|
PageWrite {
|
||||||
|
/// The page we were trying to write out
|
||||||
|
page: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
#[snafu(source(from(SiteError, Box::new)))]
|
||||||
|
source: Box<SiteError>,
|
||||||
|
},
|
||||||
|
/// Error reading in a page
|
||||||
|
#[snafu(display("Error reading in page {:?}", page))]
|
||||||
|
PageRead {
|
||||||
|
/// The page we were trying to write out
|
||||||
|
page: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
#[snafu(source(from(SiteError, Box::new)))]
|
||||||
|
source: Box<SiteError>,
|
||||||
|
},
|
||||||
|
/// Failed to write out a post stub
|
||||||
|
WritePostStub {
|
||||||
|
/// The path we tried to write the page stub to
|
||||||
|
path: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
/// Error writing out a post
|
||||||
|
#[snafu(display("Error writing out post {:?}", post))]
|
||||||
|
PostWrite {
|
||||||
|
/// The post we were trying to write out
|
||||||
|
post: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
#[snafu(source(from(SiteError, Box::new)))]
|
||||||
|
source: Box<SiteError>,
|
||||||
|
},
|
||||||
|
/// Error reading in a post
|
||||||
|
#[snafu(display("Error reading in post {:?}", post))]
|
||||||
|
PostRead {
|
||||||
|
/// The post we were trying to write out
|
||||||
|
post: PathBuf,
|
||||||
|
/// The underlying error
|
||||||
|
#[snafu(source(from(SiteError, Box::new)))]
|
||||||
|
source: Box<SiteError>,
|
||||||
|
},
|
||||||
|
/// Duplicate configuration files
|
||||||
|
#[snafu(display("Duplicate or missing configuration files in {:?}: {:?}", dir, configs))]
|
||||||
|
DuplicateConfigs {
|
||||||
|
/// The directory being checked for configs
|
||||||
|
dir: PathBuf,
|
||||||
|
/// The list of possible canidates
|
||||||
|
configs: Vec<PathBuf>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Description of the domain name and related settings for a site
|
/// Description of the domain name and related settings for a site
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
pub struct Domain {
|
pub struct Domain {
|
||||||
/// The domain name itself
|
/// The domain name itself
|
||||||
pub domain_name: String,
|
pub domain_name: String,
|
||||||
|
@ -23,7 +23,7 @@ impl Default for Domain {
|
||||||
/// Top level configuration for a site
|
/// Top level configuration for a site
|
||||||
///
|
///
|
||||||
/// Describes the file located at `site/config.dhall`
|
/// Describes the file located at `site/config.dhall`
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Doman name settings
|
/// Doman name settings
|
||||||
pub domain: Domain,
|
pub domain: Domain,
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
//! Management of a page
|
//! Management of a page
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::{create_dir_all, File},
|
borrow::Cow,
|
||||||
|
ffi::OsStr,
|
||||||
|
fs::{create_dir_all, read_dir, File},
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use snafu::{ensure, ResultExt};
|
use snafu::{ensure, ResultExt};
|
||||||
use tracing::{debug, info, info_span};
|
use tracing::{debug, info, info_span, warn};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ConfigReifySnafu, CreateDirectorySnafu, ExistanceCheckSnafu, NotADirectorySnafu, SiteError,
|
ConfigReadSnafu, ConfigReifySnafu, CreateDirectorySnafu, DuplicateConfigsSnafu,
|
||||||
WriteConfigSnafu, WritePageStubSnafu,
|
EnumerateDirectorySnafu, ExistanceCheckSnafu, NotADirectorySnafu, SiteError, WriteConfigSnafu,
|
||||||
|
WritePageStubSnafu,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Representation of the configuration for a page
|
/// Representation of the configuration for a page
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
pub struct PageConfig {
|
pub struct PageConfig {
|
||||||
/// Title of the page
|
/// Title of the page
|
||||||
title: String,
|
title: String,
|
||||||
|
@ -37,7 +40,7 @@ impl Default for PageConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Representation of a page
|
/// Representation of a page
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
/// Configuration for the page
|
/// Configuration for the page
|
||||||
pub config: PageConfig,
|
pub config: PageConfig,
|
||||||
|
@ -119,4 +122,86 @@ impl Page {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads in the configuration for a `Page`, and makes sure the file for the page exists
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return an error if deserialization fails, the user does not have read permissions for
|
||||||
|
/// the directory, or any other IO errors occur while reading in the config.
|
||||||
|
pub fn read(
|
||||||
|
site_path: impl AsRef<Path>,
|
||||||
|
page_dir: impl AsRef<Path>,
|
||||||
|
) -> Result<Page, SiteError> {
|
||||||
|
let page_dir = site_path.as_ref().join("pages").join(page_dir.as_ref());
|
||||||
|
// Setup logging
|
||||||
|
let span = info_span!("Page::read", ?page_dir);
|
||||||
|
let _enter = span.enter();
|
||||||
|
// Get the list of files
|
||||||
|
let mut file_names = Vec::new();
|
||||||
|
for file in read_dir(&page_dir).context(EnumerateDirectorySnafu { path: &page_dir })? {
|
||||||
|
match file {
|
||||||
|
Ok(file) => {
|
||||||
|
let file_path = file.path();
|
||||||
|
if file_path.is_file() {
|
||||||
|
if let Some(file_path) = file_path.file_name() {
|
||||||
|
file_names.push(PathBuf::from(file_path));
|
||||||
|
} else {
|
||||||
|
warn!(?file_path, "File path has no last part?");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(?file_path, "Page has illegal subdirectory!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => warn!(?error, "Error enumerating page directory"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Find the dhall file
|
||||||
|
let mut dhall_canidates = file_names
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.extension().map(OsStr::to_string_lossy) == Some(Cow::Borrowed("dhall")))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ensure!(
|
||||||
|
dhall_canidates.len() == 1,
|
||||||
|
DuplicateConfigsSnafu {
|
||||||
|
dir: &page_dir,
|
||||||
|
configs: dhall_canidates.clone()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let config_path = dhall_canidates.remove(0);
|
||||||
|
debug!(?config_path, "Reading config");
|
||||||
|
let real_config_path = page_dir.join(config_path);
|
||||||
|
let config: PageConfig =
|
||||||
|
serde_dhall::from_file(&real_config_path)
|
||||||
|
.parse()
|
||||||
|
.context(ConfigReadSnafu {
|
||||||
|
path: &real_config_path,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Find the djot file
|
||||||
|
let mut djot_canidates = file_names
|
||||||
|
.iter()
|
||||||
|
.filter(|e| {
|
||||||
|
e.extension()
|
||||||
|
.map(OsStr::to_string_lossy)
|
||||||
|
.map_or(false, |e| e == "djot" || e == "md")
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ensure!(
|
||||||
|
djot_canidates.len() == 1,
|
||||||
|
DuplicateConfigsSnafu {
|
||||||
|
dir: &page_dir,
|
||||||
|
configs: djot_canidates.clone()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let djot_path = djot_canidates.remove(0);
|
||||||
|
debug!(?djot_path, "Found djot file");
|
||||||
|
|
||||||
|
Ok(Page {
|
||||||
|
config,
|
||||||
|
file: djot_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
//! Management of a post
|
//! Management of a post
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::{create_dir_all, File},
|
borrow::Cow,
|
||||||
|
ffi::OsStr,
|
||||||
|
fs::{create_dir_all, read_dir, File},
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
@ -9,15 +11,16 @@ use std::{
|
||||||
use chrono::{Local, NaiveDate};
|
use chrono::{Local, NaiveDate};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use snafu::{ensure, ResultExt};
|
use snafu::{ensure, ResultExt};
|
||||||
use tracing::{debug, info, span, Level};
|
use tracing::{debug, info, info_span, span, warn, Level};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ConfigReifySnafu, CreateDirectorySnafu, ExistanceCheckSnafu, NotADirectorySnafu, SiteError,
|
ConfigReadSnafu, ConfigReifySnafu, CreateDirectorySnafu, DuplicateConfigsSnafu,
|
||||||
WriteConfigSnafu, WritePostStubSnafu,
|
EnumerateDirectorySnafu, ExistanceCheckSnafu, NotADirectorySnafu, SiteError, WriteConfigSnafu,
|
||||||
|
WritePostStubSnafu,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Representation of the configuration for a post
|
/// Representation of the configuration for a post
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
pub struct PostConfig {
|
pub struct PostConfig {
|
||||||
/// Title of the post
|
/// Title of the post
|
||||||
title: String,
|
title: String,
|
||||||
|
@ -42,7 +45,7 @@ impl Default for PostConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Representation of a post
|
/// Representation of a post
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
/// Configuration for the post
|
/// Configuration for the post
|
||||||
pub config: PostConfig,
|
pub config: PostConfig,
|
||||||
|
@ -124,4 +127,86 @@ impl Post {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads in the configuration for a `Post`, and makes sure the file for the post exists
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return an error if deserialization fails, the user does not have read permissions for
|
||||||
|
/// the directory, or any other IO errors occur while reading in the config.
|
||||||
|
pub fn read(
|
||||||
|
site_path: impl AsRef<Path>,
|
||||||
|
post_dir: impl AsRef<Path>,
|
||||||
|
) -> Result<Post, SiteError> {
|
||||||
|
let post_dir = site_path.as_ref().join("posts").join(post_dir.as_ref());
|
||||||
|
// Setup logging
|
||||||
|
let span = info_span!("Post::read", ?post_dir);
|
||||||
|
let _enter = span.enter();
|
||||||
|
// Get the list of files
|
||||||
|
let mut file_names = Vec::new();
|
||||||
|
for file in read_dir(&post_dir).context(EnumerateDirectorySnafu { path: &post_dir })? {
|
||||||
|
match file {
|
||||||
|
Ok(file) => {
|
||||||
|
let file_path = file.path();
|
||||||
|
if file_path.is_file() {
|
||||||
|
if let Some(file_path) = file_path.file_name() {
|
||||||
|
file_names.push(PathBuf::from(file_path));
|
||||||
|
} else {
|
||||||
|
warn!(?file_path, "File path has no last part?");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(?file_path, "Post has illegal subdirectory!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => warn!(?error, "Error enumerating post directory"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Find the dhall file
|
||||||
|
let mut dhall_canidates = file_names
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.extension().map(OsStr::to_string_lossy) == Some(Cow::Borrowed("dhall")))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ensure!(
|
||||||
|
dhall_canidates.len() == 1,
|
||||||
|
DuplicateConfigsSnafu {
|
||||||
|
dir: &post_dir,
|
||||||
|
configs: dhall_canidates.clone()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let config_path = dhall_canidates.remove(0);
|
||||||
|
debug!(?config_path, "Reading config");
|
||||||
|
let real_config_path = post_dir.join(config_path);
|
||||||
|
let config: PostConfig =
|
||||||
|
serde_dhall::from_file(&real_config_path)
|
||||||
|
.parse()
|
||||||
|
.context(ConfigReadSnafu {
|
||||||
|
path: &real_config_path,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Find the djot file
|
||||||
|
let mut djot_canidates = file_names
|
||||||
|
.iter()
|
||||||
|
.filter(|e| {
|
||||||
|
e.extension()
|
||||||
|
.map(OsStr::to_string_lossy)
|
||||||
|
.map_or(false, |e| e == "djot" || e == "md")
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ensure!(
|
||||||
|
djot_canidates.len() == 1,
|
||||||
|
DuplicateConfigsSnafu {
|
||||||
|
dir: &post_dir,
|
||||||
|
configs: djot_canidates.clone()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let djot_path = djot_canidates.remove(0);
|
||||||
|
debug!(?djot_path, "Found djot file");
|
||||||
|
|
||||||
|
Ok(Post {
|
||||||
|
config,
|
||||||
|
file: djot_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
use color_eyre::eyre::{ensure, Context, Result};
|
||||||
|
use stranger_site_gen::site::Site;
|
||||||
|
|
||||||
|
/// Test that serializing out a fresh site and reading it back in gives back the same [`Site`]
|
||||||
|
#[test]
|
||||||
|
fn write_read_default() -> Result<()> {
|
||||||
|
// Get a temporary directory
|
||||||
|
let tempdir = tempfile::tempdir().context("Failed to get temporary directory")?;
|
||||||
|
let tempdir_path = tempdir.path();
|
||||||
|
// Generate our site
|
||||||
|
let mut site_input = Site::default();
|
||||||
|
// Write it out
|
||||||
|
site_input
|
||||||
|
.write(tempdir_path)
|
||||||
|
.context("Failed to write out site")?;
|
||||||
|
// Read it back in
|
||||||
|
let site_output = Site::read(tempdir_path).context("Failed to read in site")?;
|
||||||
|
// Make sure the results are equal
|
||||||
|
ensure!(site_input == site_output, "Sites were not equal");
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue