diff --git a/Cargo.lock b/Cargo.lock index abdec86..ecf5a3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,7 @@ version = "0.1.0" dependencies = [ "askama", "axum", + "glob", "serde", "tokio", "toml", @@ -240,6 +241,12 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hashbrown" version = "0.14.5" diff --git a/Cargo.toml b/Cargo.toml index 1055450..a81a722 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] askama = "0.12.1" axum = "0.7.5" +glob = "0.3.1" serde = { version = "1.0.210", features = ['derive'] } tokio = "1.40.0" toml = "0.8.19" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..bd12240 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,21 @@ +use serde::Deserialize; +use std::fs::File; +use std::io::Read; + +#[derive(Deserialize)] +pub struct Config { + pub settings: Settings, +} + +#[derive(Deserialize)] +pub struct Settings { + pub base_directory: String, +} + +pub fn read_config() -> Result> { + let mut file = File::open("config.toml")?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let config: Config = toml::from_str(&contents)?; + Ok(config) +} diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..f202939 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,11 @@ +use crate::templates::HtmlTemplate; +use crate::templates::ListTemplate; +use crate::utils::find_files; +use axum::response::IntoResponse; + +pub async fn list_files(base_path: String) -> impl IntoResponse { + let template = ListTemplate { + files: find_files(&base_path).unwrap(), + }; + HtmlTemplate(template) +} diff --git a/src/main.rs b/src/main.rs index 47fdba6..e597b7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,13 @@ -use std::{ - fs::{self, File}, - io::Read, - path::Path, -}; +mod config; +mod handlers; +mod templates; +mod utils; -use axum::{ - http::StatusCode, - response::{Html, IntoResponse}, - routing::get, - Router, -}; - -use serde::Deserialize; - -use askama::Template; - -#[derive(Deserialize)] -struct Config { - settings: Settings, -} - -#[derive(Deserialize)] -struct Settings { - base_directory: String, -} +use crate::config::read_config; +use crate::handlers::list_files; +use axum::routing::get; +use axum::Router; +use tokio::net::TcpListener; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -34,73 +18,8 @@ async fn main() { get(move || list_files(config.settings.base_directory.clone())), ); - let listener = tokio::net::TcpListener::bind("127.0.0.1:3004") - .await - .unwrap(); + let listener = TcpListener::bind("127.0.0.1:3004").await.unwrap(); - println!("Listenening on {}", listener.local_addr().unwrap()); + println!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } - -fn read_config() -> Result> { - let mut file = File::open("config.toml")?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - let config: Config = toml::from_str(&contents)?; - Ok(config) -} - -#[derive(Template)] -#[template(path = "list.html")] -struct ListTemplate { - paths: Vec, -} - -struct HtmlTemplate(T); - -impl IntoResponse for HtmlTemplate -where - T: Template, -{ - fn into_response(self) -> axum::response::Response { - match self.0.render() { - Ok(html) => Html(html).into_response(), - Err(err) => ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to render template. Error: {err}"), - ) - .into_response(), - } - } -} - -async fn list_files(base_path: String) -> impl IntoResponse { - let template = ListTemplate { - paths: find_ora_files(&base_path).unwrap(), - }; - HtmlTemplate(template) -} - -fn find_ora_files(dir: &str) -> Result, std::io::Error> { - let mut ora_files = Vec::new(); - let base_path = Path::new(dir); - - if base_path.is_dir() { - for entry in fs::read_dir(base_path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - if let Ok(mut subdir_files) = find_ora_files(path.to_str().unwrap()) { - ora_files.append(&mut subdir_files); - } - } else if let Some(extension) = path.extension() { - if extension == "ora" { - ora_files.push(path.to_string_lossy().to_string()); - } - } - } - } - - Ok(ora_files) -} diff --git a/src/templates.rs b/src/templates.rs new file mode 100644 index 0000000..a28e402 --- /dev/null +++ b/src/templates.rs @@ -0,0 +1,27 @@ +use askama::Template; +use axum::http::StatusCode; +use axum::response::{Html, IntoResponse}; + +#[derive(Template)] +#[template(path = "list.html")] +pub struct ListTemplate { + pub files: Vec, +} + +pub struct HtmlTemplate(pub T); + +impl IntoResponse for HtmlTemplate +where + T: Template, +{ + fn into_response(self) -> axum::response::Response { + match self.0.render() { + Ok(html) => Html(html).into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to render template. Error: {err}"), + ) + .into_response(), + } + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..13ba6ac --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,110 @@ +use std::fs::{self, File}; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use serde::Deserialize; +use std::collections::HashMap; + +use glob::glob; + +#[derive(Deserialize, Debug)] +struct TagFile { + #[serde(skip_serializing)] + basedir: String, + + #[serde(flatten)] + entries: HashMap, +} + +#[derive(Deserialize, Debug)] +struct FileTags { + tags: Vec, +} + +#[derive(Clone, Debug)] +pub struct FileEntry { + pub path: String, + pub tags: Vec, +} + +fn load_tag_file(path: &PathBuf) -> Result> { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + Ok(TagFile { + basedir: path.parent().unwrap().to_str().unwrap().to_string(), + entries: toml::from_str(&contents)?, + }) +} + +fn match_files(pattern: &str, base_path: &Path) -> Vec { + let pattern = base_path.join(pattern).to_str().unwrap().to_string(); + let mut matched_files = Vec::new(); + for entry in glob(&pattern).expect("Failed to read glob pattern") { + if let Ok(path) = entry { + if path.is_file() { + matched_files.push(path.to_string_lossy().to_string()); + } + } + } + matched_files +} + +fn find_tag_files(dir: &str) -> Result, Box> { + let base_path = Path::new(dir); + let mut tag_files = Vec::new(); + + if base_path.is_dir() { + for entry in fs::read_dir(base_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + if let Ok(mut subdir_files) = find_tag_files(path.to_str().unwrap()) { + tag_files.append(&mut subdir_files); + } + } else if let Some(filename) = path.file_name() { + if filename != ".tags.toml" { + continue; + } + + let tag_file = load_tag_file(&path).unwrap(); + + tag_files.push(tag_file); + } + } + } + + Ok(tag_files) +} + +pub fn find_files(dir: &str) -> Result, Box> { + let mut files = HashMap::new(); + + for tag_file in find_tag_files(dir)? { + for (key, value) in tag_file.entries.into_iter() { + // Create a copy of the list (append needs it to be mutable) and + // ignore .tags.toml files + let matched_files: Vec = match_files(&key, Path::new(&tag_file.basedir)) + .iter_mut() + .filter(|x| !x.ends_with(".tags.toml")) + .map(|x| x.to_owned()) + .collect(); + + for matched_file in matched_files { + files + .entry(matched_file.clone()) + .and_modify(|file_entry: &mut FileEntry| { + file_entry.tags.extend(value.tags.to_owned()); + }) + .or_insert_with(|| FileEntry { + path: matched_file, + tags: value.tags.to_owned(), + }); + } + } + } + + Ok(files.values().cloned().collect()) +} diff --git a/templates/list.html b/templates/list.html index a552cdf..1378269 100644 --- a/templates/list.html +++ b/templates/list.html @@ -1,12 +1,58 @@ + -

Drawing list

-
    - {% for path in paths -%} -
  • {{ path }}
  • +

    Drawing list

    +
      + {% for file in files -%} +
    • +

      {{ file.path }}

      +

        + {% for tag in file.tags -%} +
      • + {{ tag }} +
      • + {% endfor %} +
      +
    • {% endfor %}