add yml config system + initial routes
This commit is contained in:
parent
1edf622e10
commit
02bbc48b15
11 changed files with 2597 additions and 1 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
config.yml
|
||||
.env
|
2248
Cargo.lock
generated
Normal file
2248
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
28
Cargo.toml
Normal file
28
Cargo.toml
Normal 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"
|
65
README.md
65
README.md
|
@ -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
45
src/config.rs
Normal 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
90
src/main.rs
Normal 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
49
src/routes.rs
Normal 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
12
src/state.rs
Normal 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
15
src/storages/disk.rs
Normal 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
15
src/storages/memory.rs
Normal 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
28
src/storages/mod.rs
Normal 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)),
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue