Merge remote-tracking branch 'asonix/master'

This commit is contained in:
Dessalines 2020-06-07 16:21:29 -04:00
commit 15cf50e3b1
4 changed files with 141 additions and 34 deletions

View file

@ -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:
@ -15,9 +14,11 @@ FLAGS:
-V, --version Prints version information -V, --version Prints version information
OPTIONS: 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

View file

@ -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,
@ -25,6 +25,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 {
@ -39,6 +46,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)]

View file

@ -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(

View file

@ -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))); }
}
} pub(crate) fn build_chain(
other => { args: &[String],
if let Ok(size) = other.parse() { whitelist: Option<&HashSet<String>>,
acc.push(Box::new(Thumbnail(size))); ) -> Vec<Box<dyn Processor + Send>> {
} else { args.into_iter()
warn!("Unknown processor {}", other); .filter_map(|arg| {
} parse!(Identity, arg.as_str(), whitelist);
} parse!(Thumbnail, arg.as_str(), whitelist);
}; parse!(Blur, arg.as_str(), whitelist);
acc
}) debug!("Skipping {}, invalid or whitelisted", arg);
None
})
.collect()
} }
pub(crate) fn build_path( pub(crate) fn build_path(