mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 19:31:35 +00:00
Add filter whitelist
This commit is contained in:
parent
35aca6a426
commit
4f55251310
4 changed files with 141 additions and 34 deletions
11
README.md
11
README.md
|
@ -4,7 +4,6 @@ _a simple image hosting service_
|
||||||
## Usage
|
## Usage
|
||||||
### Running
|
### Running
|
||||||
```
|
```
|
||||||
$ ./pict-rs --help
|
|
||||||
pict-rs 0.1.0
|
pict-rs 0.1.0
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
|
@ -18,6 +17,8 @@ OPTIONS:
|
||||||
-a, --addr <addr> The address and port the server binds to, e.g. 127.0.0.1:80
|
-a, --addr <addr> The address and port the server binds to, e.g. 127.0.0.1:80
|
||||||
-f, --format <format> An image format to convert all uploaded files into, supports 'jpg' and 'png'
|
-f, --format <format> An image format to convert all uploaded files into, supports 'jpg' and 'png'
|
||||||
-p, --path <path> The path to the data directory, e.g. data/
|
-p, --path <path> The path to the data directory, e.g. data/
|
||||||
|
-w, --whitelist <whitelist>... An optional list of filters to whitelist, supports 'identity', 'thumbnail', and
|
||||||
|
'blur'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Example:
|
#### Example:
|
||||||
|
@ -29,6 +30,10 @@ Running locally, port 9000, storing data in data/, and converting all uploads to
|
||||||
```
|
```
|
||||||
$ ./pict-rs -a 127.0.0.1:9000 -p data/ -f png
|
$ ./pict-rs -a 127.0.0.1:9000 -p data/ -f png
|
||||||
```
|
```
|
||||||
|
Running locally, port 8080, storing data in data/, and only allowing the `thumbnail` and `identity` filters
|
||||||
|
```
|
||||||
|
$ ./pict-rs -a 127.0.0.1:8080 -p data/ -w thumbnail identity
|
||||||
|
```
|
||||||
|
|
||||||
### API
|
### API
|
||||||
pict-rs offers four endpoints:
|
pict-rs offers four endpoints:
|
||||||
|
@ -63,10 +68,10 @@ pict-rs offers four endpoints:
|
||||||
existing transformations include
|
existing transformations include
|
||||||
- `identity`: apply no changes
|
- `identity`: apply no changes
|
||||||
- `blur{float}`: apply a gaussian blur to the file
|
- `blur{float}`: apply a gaussian blur to the file
|
||||||
- `{int}`: produce a thumbnail of the image fitting inside an `{int}` by `{int}` square
|
- `thumbnail{int}`: produce a thumbnail of the image fitting inside an `{int}` by `{int}` square
|
||||||
An example of usage could be
|
An example of usage could be
|
||||||
```
|
```
|
||||||
GET /image/256/blur3.0/asdf.png
|
GET /image/thumbnail256/blur3.0/asdf.png
|
||||||
```
|
```
|
||||||
which would create a 256x256px
|
which would create a 256x256px
|
||||||
thumbnail and blur it
|
thumbnail and blur it
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{net::SocketAddr, path::PathBuf};
|
use std::{collections::HashSet, net::SocketAddr, path::PathBuf};
|
||||||
|
|
||||||
#[derive(structopt::StructOpt)]
|
#[derive(Clone, Debug, structopt::StructOpt)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
#[structopt(
|
#[structopt(
|
||||||
short,
|
short,
|
||||||
|
@ -18,6 +18,13 @@ pub(crate) struct Config {
|
||||||
help = "An image format to convert all uploaded files into, supports 'jpg' and 'png'"
|
help = "An image format to convert all uploaded files into, supports 'jpg' and 'png'"
|
||||||
)]
|
)]
|
||||||
format: Option<Format>,
|
format: Option<Format>,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "An optional list of filters to whitelist, supports 'identity', 'thumbnail', and 'blur'"
|
||||||
|
)]
|
||||||
|
whitelist: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -32,6 +39,12 @@ impl Config {
|
||||||
pub(crate) fn format(&self) -> Option<Format> {
|
pub(crate) fn format(&self) -> Option<Format> {
|
||||||
self.format.clone()
|
self.format.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn filter_whitelist(&self) -> Option<HashSet<String>> {
|
||||||
|
self.whitelist
|
||||||
|
.as_ref()
|
||||||
|
.map(|wl| wl.iter().cloned().collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -8,7 +8,7 @@ use actix_web::{
|
||||||
};
|
};
|
||||||
use futures::stream::{Stream, TryStreamExt};
|
use futures::stream::{Stream, TryStreamExt};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use std::path::PathBuf;
|
use std::{collections::HashSet, path::PathBuf};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
@ -154,8 +154,9 @@ async fn delete(
|
||||||
|
|
||||||
/// Serve files
|
/// Serve files
|
||||||
async fn serve(
|
async fn serve(
|
||||||
manager: web::Data<UploadManager>,
|
|
||||||
segments: web::Path<String>,
|
segments: web::Path<String>,
|
||||||
|
manager: web::Data<UploadManager>,
|
||||||
|
whitelist: web::Data<Option<HashSet<String>>>,
|
||||||
) -> Result<HttpResponse, UploadError> {
|
) -> Result<HttpResponse, UploadError> {
|
||||||
let mut segments: Vec<String> = segments
|
let mut segments: Vec<String> = segments
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
@ -164,7 +165,7 @@ async fn serve(
|
||||||
.collect();
|
.collect();
|
||||||
let alias = segments.pop().ok_or(UploadError::MissingFilename)?;
|
let alias = segments.pop().ok_or(UploadError::MissingFilename)?;
|
||||||
|
|
||||||
let chain = self::processor::build_chain(&segments);
|
let chain = self::processor::build_chain(&segments, whitelist.as_ref().as_ref());
|
||||||
|
|
||||||
let name = manager.from_alias(alias).await?;
|
let name = manager.from_alias(alias).await?;
|
||||||
let base = manager.image_dir();
|
let base = manager.image_dir();
|
||||||
|
@ -280,16 +281,20 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let config2 = config.clone();
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let client = Client::build()
|
let client = Client::build()
|
||||||
.header("User-Agent", "pict-rs v0.1.0-master")
|
.header("User-Agent", "pict-rs v0.1.0-master")
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
|
let config = config2.clone();
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(Compress::default())
|
.wrap(Compress::default())
|
||||||
.data(manager.clone())
|
.data(manager.clone())
|
||||||
.data(client)
|
.data(client)
|
||||||
|
.data(config.filter_whitelist())
|
||||||
.service(
|
.service(
|
||||||
web::scope("/image")
|
web::scope("/image")
|
||||||
.service(
|
.service(
|
||||||
|
|
126
src/processor.rs
126
src/processor.rs
|
@ -1,17 +1,59 @@
|
||||||
use crate::error::UploadError;
|
use crate::error::UploadError;
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use image::{DynamicImage, GenericImageView};
|
use image::{DynamicImage, GenericImageView};
|
||||||
use log::warn;
|
use log::debug;
|
||||||
use std::path::PathBuf;
|
use std::{collections::HashSet, path::PathBuf};
|
||||||
|
|
||||||
pub(crate) trait Processor {
|
pub(crate) trait Processor {
|
||||||
|
fn name() -> &'static str
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
fn is_processor(s: &str) -> bool
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
fn parse(s: &str) -> Option<Box<dyn Processor + Send>>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
fn path(&self, path: PathBuf) -> PathBuf;
|
fn path(&self, path: PathBuf) -> PathBuf;
|
||||||
fn process(&self, img: DynamicImage) -> Result<DynamicImage, UploadError>;
|
fn process(&self, img: DynamicImage) -> Result<DynamicImage, UploadError>;
|
||||||
|
|
||||||
|
fn is_whitelisted(whitelist: Option<&HashSet<String>>) -> bool
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
whitelist
|
||||||
|
.map(|wl| wl.contains(Self::name()))
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Identity;
|
pub(crate) struct Identity;
|
||||||
|
|
||||||
impl Processor for Identity {
|
impl Processor for Identity {
|
||||||
|
fn name() -> &'static str
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
"identity"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_processor(s: &str) -> bool
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
s == Self::name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(_: &str) -> Option<Box<dyn Processor + Send>>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Some(Box::new(Identity))
|
||||||
|
}
|
||||||
|
|
||||||
fn path(&self, path: PathBuf) -> PathBuf {
|
fn path(&self, path: PathBuf) -> PathBuf {
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
@ -24,8 +66,30 @@ impl Processor for Identity {
|
||||||
pub(crate) struct Thumbnail(u32);
|
pub(crate) struct Thumbnail(u32);
|
||||||
|
|
||||||
impl Processor for Thumbnail {
|
impl Processor for Thumbnail {
|
||||||
|
fn name() -> &'static str
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
"thumbnail"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_processor(s: &str) -> bool
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
s.starts_with(Self::name())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(s: &str) -> Option<Box<dyn Processor + Send>>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let size = s.trim_start_matches(Self::name()).parse().ok()?;
|
||||||
|
Some(Box::new(Thumbnail(size)))
|
||||||
|
}
|
||||||
|
|
||||||
fn path(&self, mut path: PathBuf) -> PathBuf {
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
||||||
path.push("thumbnail");
|
path.push(Self::name());
|
||||||
path.push(self.0.to_string());
|
path.push(self.0.to_string());
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
@ -42,8 +106,24 @@ impl Processor for Thumbnail {
|
||||||
pub(crate) struct Blur(f32);
|
pub(crate) struct Blur(f32);
|
||||||
|
|
||||||
impl Processor for Blur {
|
impl Processor for Blur {
|
||||||
|
fn name() -> &'static str
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
"blur"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_processor(s: &str) -> bool {
|
||||||
|
s.starts_with(Self::name())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(s: &str) -> Option<Box<dyn Processor + Send>> {
|
||||||
|
let sigma = s.trim_start_matches(Self::name()).parse().ok()?;
|
||||||
|
Some(Box::new(Blur(sigma)))
|
||||||
|
}
|
||||||
|
|
||||||
fn path(&self, mut path: PathBuf) -> PathBuf {
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
||||||
path.push("blur");
|
path.push(Self::name());
|
||||||
path.push(self.0.to_string());
|
path.push(self.0.to_string());
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
@ -53,25 +133,29 @@ impl Processor for Blur {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_chain(args: &[String]) -> Vec<Box<dyn Processor + Send>> {
|
macro_rules! parse {
|
||||||
args.into_iter().fold(Vec::new(), |mut acc, arg| {
|
($x:ident, $y:expr, $z:expr) => {{
|
||||||
match arg.to_lowercase().as_str() {
|
if $x::is_processor($y) && $x::is_whitelisted($z) {
|
||||||
"identity" => acc.push(Box::new(Identity)),
|
return $x::parse($y);
|
||||||
other if other.starts_with("blur") => {
|
|
||||||
if let Ok(sigma) = other.trim_start_matches("blur").parse() {
|
|
||||||
acc.push(Box::new(Blur(sigma)));
|
|
||||||
}
|
}
|
||||||
}
|
}};
|
||||||
other => {
|
}
|
||||||
if let Ok(size) = other.parse() {
|
|
||||||
acc.push(Box::new(Thumbnail(size)));
|
pub(crate) fn build_chain(
|
||||||
} else {
|
args: &[String],
|
||||||
warn!("Unknown processor {}", other);
|
whitelist: Option<&HashSet<String>>,
|
||||||
}
|
) -> Vec<Box<dyn Processor + Send>> {
|
||||||
}
|
args.into_iter()
|
||||||
};
|
.filter_map(|arg| {
|
||||||
acc
|
parse!(Identity, arg.as_str(), whitelist);
|
||||||
|
parse!(Thumbnail, arg.as_str(), whitelist);
|
||||||
|
parse!(Blur, arg.as_str(), whitelist);
|
||||||
|
|
||||||
|
debug!("Skipping {}, invalid or whitelisted", arg);
|
||||||
|
|
||||||
|
None
|
||||||
})
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_path(
|
pub(crate) fn build_path(
|
||||||
|
|
Loading…
Reference in a new issue