diff --git a/CHANGELOG.md b/CHANGELOG.md index 601e46f..dcbd97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.0] - 2026-02-10 + +### Added +- **Human-readable size formats**: Configuration options `max_item_size` and `disk_cache_max_size` now support human-readable formats like "10M", "1G", "10MiB", "1GiB", etc. + - Supports both decimal (MB, GB) and binary (MiB, GiB) units + - Backward compatible with numeric byte values +- **Command-line options for cache settings**: + - `--forward-headers` / `--no-forward-headers`: Enable/disable X-Forwarded-* header forwarding + - `--disk-cache` / `--no-disk-cache`: Enable/disable disk-based cache + - `--disk-cache-path `: Specify disk cache directory + - `--disk-cache-max-size `: Set maximum disk cache size with human-readable format support +- **Environment variables for cache settings**: + - `FORWARD_HEADERS_ENABLED`: Enable/disable X-Forwarded-* header forwarding (true/false) + - `DISK_CACHE_ENABLED`: Enable/disable disk-based cache (true/false) + - `DISK_CACHE_PATH`: Specify disk cache directory path + - `DISK_CACHE_MAX_SIZE`: Set maximum disk cache size with human-readable format support + +### Changed +- Bumped version from 0.1.2 to 0.2.0 + ## [0.1.2] - 2026-02-10 ### Added @@ -45,7 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial implementation -[Unreleased]: https://github.com/BlockG-ws/akkoproxy/compare/v0.1.2...HEAD +[Unreleased]: https://github.com/BlockG-ws/akkoproxy/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/BlockG-ws/akkoproxy/compare/v0.1.2...v0.2.0 [0.1.2]: https://github.com/BlockG-ws/akkoproxy/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/BlockG-ws/akkoproxy/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/BlockG-ws/akkoproxy/releases/tag/v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index 705e001..6c0a67a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,11 +19,12 @@ dependencies = [ [[package]] name = "akkoproxy" -version = "0.1.2" +version = "0.2.0" dependencies = [ "anyhow", "axum", "bytes", + "bytesize", "clap", "futures", "hex", @@ -434,6 +435,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + [[package]] name = "cargo-lock" version = "8.0.3" diff --git a/Cargo.toml b/Cargo.toml index 30a7073..dc086e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "akkoproxy" -version = "0.1.2" +version = "0.2.0" edition = "2021" authors = ["Akkoproxy Contributors"] description = "A fast caching and optimization media proxy for Akkoma/Pleroma" @@ -27,6 +27,7 @@ libavif = { version = "0.12", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8" +bytesize = "2.3" # Caching moka = { version = "0.12", features = ["future"] } diff --git a/config.example.toml b/config.example.toml index 6416d8c..7349833 100644 --- a/config.example.toml +++ b/config.example.toml @@ -49,8 +49,10 @@ max_capacity = 10000 # Time to live for cached items in seconds (default: 3600) ttl = 3600 -# Maximum size of a cached item in bytes (default: 10485760, 10MB) -max_item_size = 10485760 +# Maximum size of a cached item in bytes (default: 10485760, 10MiB) +# Supports human-readable formats: "10M", "10MB", "10MiB", "1G", "1GB", "1GiB", etc. +# Note: M/MB uses base 10 (1000^2), MiB uses base 2 (1024^2) +max_item_size = "10MiB" # Enable disk-based cache (default: false) # When enabled, cached media will be stored on disk for persistence across restarts @@ -60,7 +62,9 @@ disk_cache_enabled = false disk_cache_path = "./cache" # Maximum disk cache size in bytes (default: 1073741824, 1GB) -disk_cache_max_size = 1073741824 +# Supports human-readable formats: "10M", "10MB", "10MiB", "1G", "1GB", "1GiB", etc. +# Note: G/GB uses base 10 (1000^3), GiB uses base 2 (1024^3) +disk_cache_max_size = "1GiB" [image] # Enable AVIF conversion (default: true) diff --git a/src/config.rs b/src/config.rs index 92eb300..d08bc00 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,32 @@ use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::fs; use std::net::SocketAddr; use std::path::Path; +/// Deserialize a size that can be either a number (bytes) or a human-readable string like "10M", "1G" +fn deserialize_size<'de, D>(deserializer: D) -> std::result::Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum SizeValue { + Numeric(u64), + String(String), + } + + match SizeValue::deserialize(deserializer)? { + SizeValue::Numeric(n) => Ok(n), + SizeValue::String(s) => { + // Try to parse as a human-readable size using bytesize + s.parse::() + .map(|bs| bs.as_u64()) + .map_err(serde::de::Error::custom) + } + } +} + /// Application configuration #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { @@ -77,7 +100,7 @@ pub struct CacheConfig { pub ttl: u64, /// Maximum size of a cached item in bytes - #[serde(default = "default_max_item_size")] + #[serde(default = "default_max_item_size", deserialize_with = "deserialize_size")] pub max_item_size: u64, /// Enable disk-based cache (default: false) @@ -89,7 +112,7 @@ pub struct CacheConfig { pub disk_cache_path: String, /// Maximum disk cache size in bytes (default: 1GB) - #[serde(default = "default_disk_cache_max_size")] + #[serde(default = "default_disk_cache_max_size", deserialize_with = "deserialize_size")] pub disk_cache_max_size: u64, } @@ -260,4 +283,68 @@ mod tests { assert!(config.image.enable_avif); assert!(config.image.enable_webp); } + + #[test] + fn test_parse_size_numeric() { + let toml = r#" + [upstream] + url = "https://example.com" + + [cache] + max_item_size = 10485760 + disk_cache_max_size = 1073741824 + "#; + + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.cache.max_item_size, 10485760); + assert_eq!(config.cache.disk_cache_max_size, 1073741824); + } + + #[test] + fn test_parse_size_human_readable() { + let toml = r#" + [upstream] + url = "https://example.com" + + [cache] + max_item_size = "10MiB" + disk_cache_max_size = "1GiB" + "#; + + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.cache.max_item_size, 10 * 1024 * 1024); + assert_eq!(config.cache.disk_cache_max_size, 1024 * 1024 * 1024); + } + + #[test] + fn test_parse_size_various_formats() { + let toml = r#" + [upstream] + url = "https://example.com" + + [cache] + max_item_size = "5MB" + disk_cache_max_size = "2GB" + "#; + + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.cache.max_item_size, 5 * 1000 * 1000); + assert_eq!(config.cache.disk_cache_max_size, 2 * 1000 * 1000 * 1000); + } + + #[test] + fn test_parse_size_kilobytes() { + let toml = r#" + [upstream] + url = "https://example.com" + + [cache] + max_item_size = "512KiB" + disk_cache_max_size = "100KB" + "#; + + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.cache.max_item_size, 512 * 1024); + assert_eq!(config.cache.disk_cache_max_size, 100 * 1000); + } } diff --git a/src/main.rs b/src/main.rs index 342e9d3..ca68587 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,30 @@ struct Cli { /// Preserve all headers from upstream when responding #[arg(long)] preserve_headers: bool, + + /// Enable forwarding of X-Forwarded-* headers to upstream + #[arg(long)] + forward_headers: bool, + + /// Disable forwarding of X-Forwarded-* headers to upstream + #[arg(long, conflicts_with = "forward_headers")] + no_forward_headers: bool, + + /// Enable disk-based cache + #[arg(long)] + disk_cache: bool, + + /// Disable disk-based cache + #[arg(long, conflicts_with = "disk_cache")] + no_disk_cache: bool, + + /// Path to disk cache directory + #[arg(long, value_name = "PATH")] + disk_cache_path: Option, + + /// Maximum disk cache size (e.g., "1G", "500M", "1GiB") + #[arg(long, value_name = "SIZE")] + disk_cache_max_size: Option, } #[tokio::main] @@ -108,6 +132,14 @@ async fn main() -> Result<()> { Ok(()) } +/// Parse a human-readable size string into bytes +fn parse_size(size_str: &str, context: &str) -> Result { + let size = size_str + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid {} '{}': {}", context, size_str, e))?; + Ok(size.as_u64()) +} + /// Load configuration with priority: env > cmdline options > config file fn load_config(cli: &Cli) -> Result { // Priority 3 (lowest): Load from config file if it exists @@ -156,6 +188,26 @@ fn load_config(cli: &Cli) -> Result { config.server.preserve_upstream_headers = true; } + if cli.forward_headers { + config.server.forward_headers_enabled = true; + } else if cli.no_forward_headers { + config.server.forward_headers_enabled = false; + } + + if cli.disk_cache { + config.cache.disk_cache_enabled = true; + } else if cli.no_disk_cache { + config.cache.disk_cache_enabled = false; + } + + if let Some(ref path) = cli.disk_cache_path { + config.cache.disk_cache_path = path.to_string_lossy().to_string(); + } + + if let Some(ref size_str) = cli.disk_cache_max_size { + config.cache.disk_cache_max_size = parse_size(size_str, "disk cache max size")?; + } + // Priority 1 (highest): Apply environment variables if let Ok(upstream_url) = std::env::var("UPSTREAM_URL") { info!("Overriding upstream URL from environment: {}", upstream_url); @@ -176,6 +228,31 @@ fn load_config(cli: &Cli) -> Result { } } + if let Ok(forward_headers) = std::env::var("FORWARD_HEADERS_ENABLED") { + if let Ok(value) = forward_headers.parse::() { + info!("Overriding forward_headers_enabled from environment: {}", value); + config.server.forward_headers_enabled = value; + } + } + + if let Ok(disk_cache) = std::env::var("DISK_CACHE_ENABLED") { + if let Ok(value) = disk_cache.parse::() { + info!("Overriding disk_cache_enabled from environment: {}", value); + config.cache.disk_cache_enabled = value; + } + } + + if let Ok(path) = std::env::var("DISK_CACHE_PATH") { + info!("Overriding disk_cache_path from environment: {}", path); + config.cache.disk_cache_path = path; + } + + if let Ok(size_str) = std::env::var("DISK_CACHE_MAX_SIZE") { + let size = parse_size(&size_str, "DISK_CACHE_MAX_SIZE")?; + info!("Overriding disk_cache_max_size from environment: {}", bytesize::ByteSize(size)); + config.cache.disk_cache_max_size = size; + } + // Validate that we have an upstream URL if config.upstream.url.is_empty() { anyhow::bail!(