add yml config system + initial routes

This commit is contained in:
qpismont 2023-12-25 13:46:32 +01:00
parent 1edf622e10
commit 02bbc48b15
11 changed files with 2597 additions and 1 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
config.yml
.env

2248
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

28
Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
name = "imgproxy-rs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.35", features = ["full", "tracing"] }
tracing = "0.1"
tracing-subscriber = "0.3"
tokio-stream = "0.1"
tokio-util = "0.7"
axum = { version = "0.7" }
reqwest = { version = "0.11", features = ["stream"] }
anyhow = "1.0"
sentry = "0.32"
dotenvy = "0.15"
thiserror = "1.0"
axum-macros = "0.4"
magic-crypt = "3.1"
async-trait = "0.1"
queryst = "3"
clap = { version = "4.4.10", features = ["derive"] }
serde = "1.0"
serde_yaml = "0.9"
regex = "1.10"
serde_regex = "1.1"

View file

@ -1,2 +1,65 @@
# imgproxy-rs
# Rust Image Proxy
## Overview
This Rust project is a versatile image proxy designed to efficiently handle image requests. Users can customize the caching behavior through a YAML configuration file, choosing between in-memory and on-disk caching based on provided regular expressions.
## Features
- **Image Proxy:** Efficiently fetches and serves images from remote sources.
- **Caching:** Supports both in-memory and on-disk caching for improved performance.
- **Configuration:** Customize caching behavior using a YAML configuration file.
- **Regex-Based Caching:** Specify caching strategy based on a regular expression.
## Getting Started
### Prerequisites
- [Rust](https://www.rust-lang.org/tools/install) must be installed.
### Installation
Clone the repository:
```bash
git clone https://gitea.qpismont.fr/qpismont/imgproxy-rs.git
cd rust-image-proxy
```
Build the project:
```bash
cargo build --release
```
### Configuration
Create a YAML configuration file (config.yml) with the following structure:
```yaml
storages:
- strategy: !Memory
ttl: 12
max_size: 1000
regex: "REGEX_HERE"
- strategy: !Memory
ttl: 32
max_size: 16000
regex: "REGEX_HERE"
- strategy: !Disk
path: "./cache"
max_size: 128000
regex: "REGEX_HERE"
secret_key: "SECRET_KEY_FOR_SECURE_URL"
```
Adjust the regex values and storage configurations as needed.
### Usage
Run the proxy using the following command:
```bash
cargo run --release
```

45
src/config.rs Normal file
View file

@ -0,0 +1,45 @@
use std::str::FromStr;
use anyhow::anyhow;
use serde::{Deserialize, de::DeserializeOwned};
#[derive(Deserialize)]
pub struct MemoryStorageConfig {
pub max_size: usize,
pub ttl: usize
}
#[derive(Deserialize)]
pub struct DiskStorageConfig {
pub path: String,
pub max_size: usize
}
#[derive(Deserialize)]
pub enum StorageStrategyConfig {
Memory(MemoryStorageConfig),
Disk(DiskStorageConfig)
}
#[derive(Deserialize)]
pub struct StorageConfig {
pub strategy: StorageStrategyConfig,
#[serde(with = "serde_regex")]
pub regex: regex::Regex
}
#[derive(Deserialize, Default)]
pub struct YAMLConfig {
pub storages: Vec<StorageConfig>,
pub secret_key: Option<String>
}
pub async fn load() -> YAMLConfig {
tokio::fs::read("config.yml")
.await
.map_err(|err| anyhow!(err))
.and_then(|content| serde_yaml::from_slice::<YAMLConfig>(&content).map_err(|err| anyhow!(err)))
.unwrap_or_default()
}

90
src/main.rs Normal file
View file

@ -0,0 +1,90 @@
use anyhow::anyhow;
use axum::{
http::StatusCode,
routing::get, Router, extract::{Path, State}, response::IntoResponse,
};
use magic_crypt::{new_magic_crypt, MagicCryptTrait};
use tokio_stream::{self as stream, StreamExt};
use tokio_util::{bytes::Bytes};
use tracing::{warn};
use clap::Parser;
use std::{sync::Arc, path::PathBuf};
use crate::storages::StoragePool;
mod config;
mod storages;
mod routes;
mod state;
struct AppState {
secret_key: String,
authorized_commands: Vec<String>
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let config = config::load().await;
let secret_key = config.secret_key.clone();
let storage_pool = StoragePool::from_config(config.storages);
let app_state = Arc::new(state::AppState::new(storage_pool, secret_key));
println!("{}", config.secret_key.clone().unwrap_or_default());
let app = Router::new()
.route("/unsecure/*src", get(routes::handle_unsecure))
.route("/secure/*data", get(routes::handle_secure))
.with_state(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
/*async fn handle_secure(Path(data): Path<String>, State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, MyAnyhow> {
let mc = new_magic_crypt!(&state.secret_key, 256);
let decrypt = mc.decrypt_base64_to_string(data)?;
let split = decrypt.splitn(2, "/").map(String::from).collect::<Vec<String>>();
let opts = split.get(0).ok_or_else(|| anyhow::anyhow!("opts not found"))?;
let src = split.get(1).ok_or_else(|| anyhow::anyhow!("src not found"))?;
let bytes = handle(opts.clone(), src.clone(), state).await?;
Ok(axum::body::Body::from(bytes))
}
async fn handle_unsecure(Path((opts, src)): Path<(String, String)>, State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, MyAnyhow> {
let bytes = handle(opts, src, state).await?;
Ok(axum::body::Body::from(bytes))
}*/
/*async fn handle(_opts: String, src: String) -> anyhow::Result<impl Stream<Item = Result<Bytes, reqwest::Error>>> {
let client = reqwest::Client::new();
let res = client.get(src).send().await?;
let stream = res.bytes_stream();
Ok(stream)
}*/
/*async fn handle(opts: String, src: String, app_state: Arc<AppState>) -> anyhow::Result<Bytes, MyAnyhow> {
let qs_opts = queryst::parse(&opts).map_err(|err| anyhow::anyhow!("{}", err.message))?;
let client = reqwest::Client::new();
let res = client.get(src.clone()).send().await?;
let bytes = res.bytes().await?;
if let Some(storage) = &app_state.storage {
}
Ok(bytes)
}*/

49
src/routes.rs Normal file
View file

@ -0,0 +1,49 @@
use std::{sync::Arc};
use axum::{extract::{Path, State}, response::IntoResponse, http::StatusCode};
use magic_crypt::{new_magic_crypt, MagicCryptTrait};
use tokio_stream::Stream;
use tokio_util::bytes::Bytes;
use crate::state::AppState;
pub async fn handle_secure(Path(data): Path<String>, State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, MyAnyhow> {
let mc = new_magic_crypt!(&state.secret_key.clone().unwrap(), 256);
let decrypt = mc.decrypt_base64_to_string(data)?;
let split = decrypt.splitn(2, "/").map(String::from).collect::<Vec<String>>();
let opts = split.get(0).ok_or_else(|| anyhow::anyhow!("opts not found"))?;
let src = split.get(1).ok_or_else(|| anyhow::anyhow!("src not found"))?;
let bytes = handle(opts.clone(), src.clone()).await?;
Ok(axum::body::Body::from_stream(bytes))
}
pub async fn handle_unsecure(Path((opts, src)): Path<(String, String)>, State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, MyAnyhow> {
let bytes = handle(opts, src).await?;
Ok(axum::body::Body::from_stream(bytes))
}
async fn handle(_opts: String, src: String) -> anyhow::Result<impl Stream<Item = Result<Bytes, reqwest::Error>>> {
let client = reqwest::Client::new();
let res = client.get(src).send().await?;
let stream = res.bytes_stream();
Ok(stream)
}
pub struct MyAnyhow(anyhow::Error);
impl IntoResponse for MyAnyhow {
fn into_response(self) -> axum::response::Response {
(StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response()
}
}
impl <T>From<T> for MyAnyhow where T: Into<anyhow::Error> {
fn from(value: T) -> Self {
Self(value.into())
}
}

12
src/state.rs Normal file
View file

@ -0,0 +1,12 @@
use crate::storages::StoragePool;
pub struct AppState {
pub secret_key: Option<String>,
pub storage_pool: StoragePool
}
impl AppState {
pub fn new(storage_pool: StoragePool, secret_key: Option<String>) -> Self {
Self { secret_key, storage_pool }
}
}

15
src/storages/disk.rs Normal file
View file

@ -0,0 +1,15 @@
use crate::config::DiskStorageConfig;
use super::Storage;
pub struct DiskStorage {
config: DiskStorageConfig
}
impl DiskStorage {
pub fn new(config: DiskStorageConfig) -> Self {
Self { config }
}
}
impl Storage for DiskStorage {}

15
src/storages/memory.rs Normal file
View file

@ -0,0 +1,15 @@
use crate::config::MemoryStorageConfig;
use super::Storage;
pub struct MemoryStorage {
config: MemoryStorageConfig
}
impl MemoryStorage {
pub fn new(config: MemoryStorageConfig) -> Self {
Self { config }
}
}
impl Storage for MemoryStorage {}

28
src/storages/mod.rs Normal file
View file

@ -0,0 +1,28 @@
pub mod memory;
pub mod disk;
use async_trait::async_trait;
use crate::config::StorageConfig;
use self::{memory::MemoryStorage, disk::DiskStorage};
#[async_trait]
pub trait Storage {}
pub struct StoragePool {
storages: Vec<Box<dyn Storage + Sync + Send>>
}
impl StoragePool {
pub fn from_config(items: Vec<StorageConfig>) -> Self {
Self { storages: items.into_iter().map(create_storage).collect() }
}
}
fn create_storage(item: StorageConfig) -> Box<dyn Storage + Sync + Send> {
match item.strategy {
crate::config::StorageMethodConfig::Memory(config) => Box::new(MemoryStorage::new(config)),
crate::config::StorageMethodConfig::Disk(config) => Box::new(DiskStorage::new(config)),
}
}