mirror of
https://git.asonix.dog/asonix/pict-rs.git
synced 2024-12-29 13:50:35 +00:00
Merge remote-tracking branch 'asonix/master'
This commit is contained in:
commit
15cf50e3b1
4 changed files with 141 additions and 34 deletions
17
README.md
17
README.md
|
@ -4,7 +4,6 @@ _a simple image hosting service_
|
|||
## Usage
|
||||
### Running
|
||||
```
|
||||
$ ./pict-rs --help
|
||||
pict-rs 0.1.0
|
||||
|
||||
USAGE:
|
||||
|
@ -15,9 +14,11 @@ FLAGS:
|
|||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-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'
|
||||
-p, --path <path> The path to the data directory, e.g. data/
|
||||
-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'
|
||||
-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:
|
||||
|
@ -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
|
||||
```
|
||||
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
|
||||
pict-rs offers four endpoints:
|
||||
|
@ -63,10 +68,10 @@ pict-rs offers four endpoints:
|
|||
existing transformations include
|
||||
- `identity`: apply no changes
|
||||
- `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
|
||||
```
|
||||
GET /image/256/blur3.0/asdf.png
|
||||
GET /image/thumbnail256/blur3.0/asdf.png
|
||||
```
|
||||
which would create a 256x256px
|
||||
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 {
|
||||
#[structopt(
|
||||
short,
|
||||
|
@ -25,6 +25,13 @@ pub(crate) struct Config {
|
|||
help = "An image format to convert all uploaded files into, supports 'jpg' and 'png'"
|
||||
)]
|
||||
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 {
|
||||
|
@ -39,6 +46,12 @@ impl Config {
|
|||
pub(crate) fn format(&self) -> Option<Format> {
|
||||
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)]
|
||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -8,7 +8,7 @@ use actix_web::{
|
|||
};
|
||||
use futures::stream::{Stream, TryStreamExt};
|
||||
use log::{error, info};
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
use structopt::StructOpt;
|
||||
|
||||
mod config;
|
||||
|
@ -154,8 +154,9 @@ async fn delete(
|
|||
|
||||
/// Serve files
|
||||
async fn serve(
|
||||
manager: web::Data<UploadManager>,
|
||||
segments: web::Path<String>,
|
||||
manager: web::Data<UploadManager>,
|
||||
whitelist: web::Data<Option<HashSet<String>>>,
|
||||
) -> Result<HttpResponse, UploadError> {
|
||||
let mut segments: Vec<String> = segments
|
||||
.into_inner()
|
||||
|
@ -164,7 +165,7 @@ async fn serve(
|
|||
.collect();
|
||||
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 base = manager.image_dir();
|
||||
|
@ -280,16 +281,20 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
})),
|
||||
);
|
||||
|
||||
let config2 = config.clone();
|
||||
HttpServer::new(move || {
|
||||
let client = Client::build()
|
||||
.header("User-Agent", "pict-rs v0.1.0-master")
|
||||
.finish();
|
||||
|
||||
let config = config2.clone();
|
||||
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(Compress::default())
|
||||
.data(manager.clone())
|
||||
.data(client)
|
||||
.data(config.filter_whitelist())
|
||||
.service(
|
||||
web::scope("/image")
|
||||
.service(
|
||||
|
|
130
src/processor.rs
130
src/processor.rs
|
@ -1,17 +1,59 @@
|
|||
use crate::error::UploadError;
|
||||
use actix_web::web;
|
||||
use image::{DynamicImage, GenericImageView};
|
||||
use log::warn;
|
||||
use std::path::PathBuf;
|
||||
use log::debug;
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
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 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;
|
||||
|
||||
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 {
|
||||
path
|
||||
}
|
||||
|
@ -24,8 +66,30 @@ impl Processor for Identity {
|
|||
pub(crate) struct Thumbnail(u32);
|
||||
|
||||
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 {
|
||||
path.push("thumbnail");
|
||||
path.push(Self::name());
|
||||
path.push(self.0.to_string());
|
||||
path
|
||||
}
|
||||
|
@ -42,8 +106,24 @@ impl Processor for Thumbnail {
|
|||
pub(crate) struct Blur(f32);
|
||||
|
||||
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 {
|
||||
path.push("blur");
|
||||
path.push(Self::name());
|
||||
path.push(self.0.to_string());
|
||||
path
|
||||
}
|
||||
|
@ -53,25 +133,29 @@ impl Processor for Blur {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_chain(args: &[String]) -> Vec<Box<dyn Processor + Send>> {
|
||||
args.into_iter().fold(Vec::new(), |mut acc, arg| {
|
||||
match arg.to_lowercase().as_str() {
|
||||
"identity" => acc.push(Box::new(Identity)),
|
||||
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)));
|
||||
} else {
|
||||
warn!("Unknown processor {}", other);
|
||||
}
|
||||
}
|
||||
};
|
||||
acc
|
||||
})
|
||||
macro_rules! parse {
|
||||
($x:ident, $y:expr, $z:expr) => {{
|
||||
if $x::is_processor($y) && $x::is_whitelisted($z) {
|
||||
return $x::parse($y);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) fn build_chain(
|
||||
args: &[String],
|
||||
whitelist: Option<&HashSet<String>>,
|
||||
) -> Vec<Box<dyn Processor + Send>> {
|
||||
args.into_iter()
|
||||
.filter_map(|arg| {
|
||||
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(
|
||||
|
|
Loading…
Reference in a new issue