Compare commits

..

2 Commits

Author SHA1 Message Date
7b300987b7 refactor: split cli and logging to separate crates
All checks were successful
Test the running changes / Test (push) Successful in 42s
2025-11-18 23:41:45 +02:00
33bfff2e98 bugfix, implemented verbose logging cli option
Some checks failed
Test the running changes / Test (push) Failing after 38s
2025-11-17 00:48:23 +02:00
21 changed files with 229 additions and 160 deletions

View File

@@ -1,5 +0,0 @@
[registry]
default = "gitea"
[registries.gitea]
index = "sparse+https://git.jlux.dev/api/packages/scrac/cargo/"

1
.gitignore vendored
View File

@@ -3,4 +3,3 @@ web/
target/
result
.cargo/credentials.toml

17
Cargo.lock generated
View File

@@ -14,9 +14,22 @@ name = "fstools"
version = "0.1.0"
[[package]]
name = "stdsrv"
name = "gravel_cli"
version = "0.1.0"
dependencies = [
"cracked_md",
"fstools",
"slogger",
"stdsrv",
]
[[package]]
name = "slogger"
version = "0.1.0"
[[package]]
name = "stdsrv"
version = "0.1.0"
dependencies = [
"fstools",
"slogger",
]

View File

@@ -3,6 +3,7 @@ members = [
"stdsrv",
"cracked_md",
"fstools",
"gravel_cli", "slogger",
]
resolver = "3"

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2024"
[dependencies]
fstools = { version = "0.1.0", path = "../fstools" }
fstools = { path = "../fstools" }

View File

@@ -1,7 +1,7 @@
//! A "Markdown" parser and HTML generator. Part of a static site generator `marksmith-rs`.
//! Not following any standards, only vibes.
#![deny(unused_imports)]
#![deny(dead_code, unused_imports)]
#![allow(clippy::needless_pass_by_value)]
use fstools::crawl_fs;
@@ -107,6 +107,7 @@ pub enum Error {
InDirIsNotDir,
OutDirIsNotEmpty,
OutDirIsNotDir,
OutDirFileOverwriteWithoutForce,
OutDirFileDeleteNotAllowed,
OutDirDirectoryInPlaceOfFile,
FileRead,
@@ -162,8 +163,12 @@ pub fn generate(indir: &PathBuf, outdir: &PathBuf, force: bool) -> Result<()> {
// check if path exists
if newpath.exists() {
// remove if is file and if force, otherwise error
if newpath.is_file() && force {
fs::remove_file(&newpath).map_err(|_e| Error::OutDirFileDeleteNotAllowed)?;
if newpath.is_file() {
if force {
fs::remove_file(&newpath).map_err(|_e| Error::OutDirFileDeleteNotAllowed)?;
} else {
Err(Error::OutDirFileOverwriteWithoutForce)?;
}
} else {
Err(Error::OutDirDirectoryInPlaceOfFile)?;
}

14
gravel_cli/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "gravel_cli"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "gravel"
path = "src/main.rs"
[dependencies]
cracked_md = { path = "../cracked_md" }
stdsrv = { path = "../stdsrv" }
slogger = { path = "../slogger" }

84
gravel_cli/src/args.rs Normal file
View File

@@ -0,0 +1,84 @@
//! Simple and program specific command line argument parsing solution.
// todo: refactor to <command> <subcommand> [<options>]
use slogger::{LOG_LEVEL, Level};
use crate::error::Error;
use std::net::Ipv4Addr;
use std::path::PathBuf;
pub struct ProgramArgs {
pub outdir: PathBuf,
pub indir: PathBuf,
pub generate: bool,
pub force: bool,
pub addr: Ipv4Addr,
pub port: u16,
pub verbose: bool,
}
impl Default for ProgramArgs {
fn default() -> Self {
Self {
indir: PathBuf::from("./web"),
outdir: PathBuf::from("./html"),
generate: false,
force: false,
addr: Ipv4Addr::UNSPECIFIED,
port: 8080,
verbose: false,
}
}
}
impl TryFrom<std::env::Args> for ProgramArgs {
type Error = crate::error::Error;
fn try_from(mut value: std::env::Args) -> Result<Self, Self::Error> {
let mut a = Self::default();
let _ = value.next(); // ignore executable path
while let Some(v) = value.next() {
match v.as_str() {
"-i" => {
a.indir = value
.next()
.ok_or(Error::CommandLineArgsParse(
"Expected input directory after option `-i`".to_string(),
))?
.into();
}
"-a" => {
let addr_string = value.next().ok_or(Error::CommandLineArgsParse(
"Expected listener IPv4 address after option `-a`".to_string(),
))?;
a.addr = Ipv4Addr::parse_ascii(addr_string.as_bytes()).map_err(|_e| {
Error::CommandLineArgsParse(
"Invalid IPv4 address after option `-a`".to_string(),
)
})?;
}
"-p" => {
let port_string = value.next().ok_or(Error::CommandLineArgsParse(
"Expected listener port after option `-p`".to_string(),
))?;
a.port = port_string.parse().map_err(|_e| {
Error::CommandLineArgsParse(
"Invalid 16-bit port number after option `-p`".to_string(),
)
})?;
}
"-g" => a.generate = true,
"-f" => a.force = true,
"-v" => {
a.verbose = true;
LOG_LEVEL.get_or_init(|| Level::Debug);
}
_ => {
a.outdir = v.into();
}
}
}
LOG_LEVEL.get_or_init(|| Level::Info);
Ok(a)
}
}

32
gravel_cli/src/error.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::fmt::Display;
#[derive(Debug)]
pub enum Error {
Server(stdsrv::error::Error),
MdParse(cracked_md::Error),
CommandLineArgsParse(String),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Server(e) => e.fmt(f),
Error::MdParse(e) => e.fmt(f),
Error::CommandLineArgsParse(s) => write!(f, "{s}"),
}
}
}
impl std::error::Error for Error {}
impl From<cracked_md::Error> for Error {
fn from(value: cracked_md::Error) -> Self {
Self::MdParse(value)
}
}
impl From<stdsrv::error::Error> for Error {
fn from(value: stdsrv::error::Error) -> Self {
Self::Server(value)
}
}

17
gravel_cli/src/main.rs Normal file
View File

@@ -0,0 +1,17 @@
#![feature(addr_parse_ascii, never_type)]
use args::ProgramArgs;
use cracked_md::generate;
use error::Error;
//use slogger::{LOG_LEVEL, Level};
use stdsrv::serve;
mod args;
mod error;
fn main() -> Result<!, Error> {
let args = ProgramArgs::try_from(std::env::args())?;
generate(&args.indir, &args.outdir, args.force)?;
serve(args.addr, args.port, args.outdir)?;
}

View File

@@ -5,6 +5,5 @@ targets = [
]
components = [
"clippy",
"rustfmt",
"rust-analyzer"
"rustfmt"
]

6
slogger/Cargo.toml Normal file
View File

@@ -0,0 +1,6 @@
[package]
name = "slogger"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -1,7 +1,9 @@
use std::fmt::Display;
use std::{fmt::Display, sync::OnceLock};
pub static LOG_LEVEL: OnceLock<Level> = OnceLock::new();
#[derive(Debug)]
#[allow(dead_code)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Error,
Warn,
@@ -25,18 +27,20 @@ impl Display for Level {
}
/// A logging macro. Takes a [`Level`] and a formatted string.
///
/// [`Level`]: ./logger/enum.Level.html
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {{
println!(
"{} {}:{}:{}: {}",
$level,
std::module_path!(),
std::file!(),
std::line!(),
format!($($arg)*)
);
if &$level <= $crate::LOG_LEVEL.get().unwrap_or(&$crate::Level::Info) {
println!(
"{} {}:{}:{}: {}",
$level,
std::module_path!(),
std::file!(),
std::line!(),
format!($($arg)*)
);
}
}};
}
// todo: implement clean verbose/short logging

View File

@@ -3,11 +3,7 @@ name = "stdsrv"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "gravel"
path = "src/main.rs"
# local dependencies
[dependencies]
cracked_md = { version = "0.1.0", path = "../cracked_md" }
fstools = { version = "0.1.0", path = "../fstools" }
fstools = { path = "../fstools" }
slogger = { path = "../slogger" }

View File

@@ -1,60 +0,0 @@
//! Simple and program specific command line argument parsing solution.
use std::path::PathBuf;
use crate::error::Error;
use crate::error::ErrorKind;
pub struct ProgramArgs {
pub outdir: PathBuf,
pub indir: PathBuf,
pub generate: bool,
pub force: bool,
pub addr: String,
}
impl Default for ProgramArgs {
fn default() -> Self {
Self {
indir: PathBuf::from("./web"),
outdir: PathBuf::from("./html"),
generate: false,
force: false,
addr: "0.0.0.0:8080".to_string(),
}
}
}
impl TryFrom<std::env::Args> for ProgramArgs {
type Error = crate::error::Error;
fn try_from(mut value: std::env::Args) -> Result<Self, Self::Error> {
let mut a = Self::default();
let _ = value.next(); // ignore executable path
while let Some(v) = value.next() {
match v.as_str() {
"-i" => {
a.indir = value
.next()
.ok_or(Error::new(
ErrorKind::CommandLineArgsParse,
"Expected input directory after option `-i`",
))?
.into();
}
"-a" => {
a.addr = value.next().ok_or(Error::new(
ErrorKind::CommandLineArgsParse,
"Expected listener address after option `-a`",
))?;
}
"-g" => a.generate = true,
"-f" => a.force = true,
_ => {
a.outdir = v.into();
}
}
}
Ok(a)
}
}

View File

@@ -27,6 +27,7 @@ pub struct Error {
}
impl Error {
#[must_use]
pub fn new(kind: ErrorKind, msg: &str) -> Self {
Self {
kind,

View File

@@ -1,8 +1,8 @@
//! A simple server implementation that just responds with the contents of the file requested in
//! the provided directory.
use std::fs;
use std::path::PathBuf;
use std::{fs, path::Path};
use crate::{
error::{Error, ErrorKind, Result},
@@ -16,7 +16,7 @@ pub struct FileServer {
}
impl FileServer {
pub fn new(root: &PathBuf) -> Result<FileServer> {
pub fn new(root: &Path) -> Result<FileServer> {
if !root.is_dir() {
return Err(Error::new(
ErrorKind::DirNotFound,

View File

@@ -1,43 +0,0 @@
use crate::error::ErrorKind;
use crate::log;
use crate::logger::Level;
use std::io::Read;
use std::net::{TcpListener, TcpStream};
use crate::{error::Error, request::HttpRequest};
pub struct HttpStream {
tcp_listener: TcpListener,
}
impl HttpStream {
pub fn new(addr: &str) -> Self {
let tcp_listener = TcpListener::bind(addr)
.unwrap_or_else(|e| panic!("Failed to bind on address `{}`: {}", addr, e));
log!(Level::Info, "Listening on `{}`", addr);
Self { tcp_listener }
}
}
impl Iterator for HttpStream {
type Item = (HttpRequest, TcpStream);
fn next(&mut self) -> Option<Self::Item> {
// safe to unwrap, because Incoming never returns None
let mut stream = self.tcp_listener.incoming().next().unwrap().ok()?;
let mut buf = [0; 1024];
let _read_bytes = stream
.read(&mut buf)
.or(Err(Error::new(
ErrorKind::StreamReadFailed,
"Reading from TCP stream failed",
)))
.ok()?;
Some((
String::from_utf8_lossy(&buf[..]).trim().try_into().ok()?,
stream,
))
}
}

View File

@@ -1,34 +1,38 @@
//! A simple web server with 0 dependencies (other than Rust's stdlib).
//! Documentation is a work in progress, go see my webpage at [jlux.dev](https://jlux.dev).
#![feature(never_type)]
#![allow(dead_code)]
use std::{
io::{BufReader, BufWriter},
net::TcpListener,
process,
net::{Ipv4Addr, TcpListener},
path::PathBuf,
};
use args::ProgramArgs;
use cracked_md::generate;
use error::Error;
use fileserver::FileServer;
use logger::Level;
use request::HttpRequest;
use responder::Responder;
use slogger::{Level, log};
mod args;
mod error;
pub mod error;
mod fileserver;
mod http_header;
//mod http_stream;
mod logger;
mod request;
mod responder;
mod response;
/// Entrypoint to the program.
fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Opens a file server on a specified address and port which serves all files in dir.
///
/// # Errors
/// Errors that come up while serving files. Look at [`Error`].
///
/// # Panics
/// Never. Added to allow compiler to check for ! type.
pub fn serve(addr: Ipv4Addr, port: u16, dir: PathBuf) -> Result<!, Error> {
/*
let args: ProgramArgs = std::env::args().try_into()?;
if args.generate {
match generate(&args.indir, &args.outdir, args.force) {
Ok(()) => log!(
@@ -52,18 +56,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
let listener = TcpListener::bind(&args.addr)?;
log!(Level::Info, "Listening on addr `{}`", &args.addr);
*/
let listener = TcpListener::bind((addr, port))?;
log!(Level::Info, "Listening on addr `{}:{}`", addr, port);
// todo: refactor this
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let outdir = args.outdir.clone();
let outdir = dir.clone();
std::thread::spawn(move || {
log!(Level::Debug, "TcpStream handler spawned");
let mut reader = BufReader::new(&stream);
let mut writer = BufWriter::new(&stream);
let server = match FileServer::new(&outdir) {
let server = match FileServer::new(outdir.as_path()) {
Ok(s) => s,
Err(_e) => return,
};
@@ -83,5 +89,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
Ok(())
panic!("Code shouldn't get to here");
}

View File

@@ -2,8 +2,7 @@
use crate::error::{Error, ErrorKind, Result};
use crate::http_header::HttpHeaders;
use crate::log;
use crate::logger::Level;
use slogger::{Level, log};
use std::fmt::Display;
use std::io::{BufRead, BufReader};
use std::net::TcpStream;

View File

@@ -2,8 +2,7 @@
use crate::error::Result;
use crate::http_header::HttpHeaders;
use crate::log;
use crate::logger::Level;
use slogger::{Level, log};
use std::{fmt::Display, io::Write};
/// Macro for generating Http status codes (AI generated).
@@ -119,7 +118,9 @@ impl HttpResponse {
let _ = std::io::Read::read(stream, &mut [0u8; 1]);
*/
log!(Level::Info, "\n{}", &self);
// todo better verbose tracking
log!(Level::Info, "{} {}", self.version, self.status);
log!(Level::Debug, "\n{}", &self);
Ok(())
}