Add content type, width, height to Details, add details endpoints

This commit is contained in:
asonix 2020-12-10 12:49:10 -06:00
parent c43737c894
commit c0a20588d3
5 changed files with 229 additions and 56 deletions

2
Cargo.lock generated
View File

@ -1449,7 +1449,7 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]] [[package]]
name = "pict-rs" name = "pict-rs"
version = "0.3.0-alpha.0" version = "0.3.0-alpha.1"
dependencies = [ dependencies = [
"actix-form-data", "actix-form-data",
"actix-fs", "actix-fs",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "pict-rs" name = "pict-rs"
description = "A simple image hosting service" description = "A simple image hosting service"
version = "0.3.0-alpha.0" version = "0.3.0-alpha.1"
authors = ["asonix <asonix@asonix.dog>"] authors = ["asonix <asonix@asonix.dog>"]
license = "AGPL-3.0" license = "AGPL-3.0"
readme = "README.md" readme = "README.md"

View File

@ -4,7 +4,7 @@ _a simple image hosting service_
## Usage ## Usage
### Running ### Running
``` ```
pict-rs 0.3.0-alpha.0 pict-rs 0.3.0-alpha.1
USAGE: USAGE:
pict-rs [FLAGS] [OPTIONS] --path <path> pict-rs [FLAGS] [OPTIONS] --path <path>
@ -105,6 +105,21 @@ pict-rs offers the following endpoints:
payload as the `POST` endpoint payload as the `POST` endpoint
- `GET /image/original/{file}` for getting a full-resolution image. `file` here is the `file` key from the - `GET /image/original/{file}` for getting a full-resolution image. `file` here is the `file` key from the
`/image` endpoint's JSON `/image` endpoint's JSON
- `GET /image/details/original/{file}` for getting the details of a full-resolution image.
The returned JSON is structured like so:
```json
{
"width": 800,
"height": 537,
"content_type": "image/webp",
"created_at": [
2020,
345,
67376,
394363487
]
}
```
- `GET /image/process.{ext}?src={file}&...` get a file with transformations applied. - `GET /image/process.{ext}?src={file}&...` get a file with transformations applied.
existing transformations include existing transformations include
- `identity=true`: apply no changes - `identity=true`: apply no changes
@ -121,6 +136,8 @@ pict-rs offers the following endpoints:
GET /image/process.jpg?src=asdf.png&thumbnail=256&blur=3.0 GET /image/process.jpg?src=asdf.png&thumbnail=256&blur=3.0
``` ```
which would create a 256x256px JPEG thumbnail and blur it which would create a 256x256px JPEG thumbnail and blur it
- `GET /image/details/process.{ext}?src={file}&...` for getting the details of a processed image.
The returned JSON is the same format as listed for the full-resolution details endpoint.
- `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to - `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to
delete a file, where `delete_token` and `file` are from the `/image` endpoint's JSON delete a file, where `delete_token` and `file` are from the `/image` endpoint's JSON

View File

@ -136,30 +136,6 @@ fn to_ext(mime: mime::Mime) -> Result<&'static str, UploadError> {
} }
} }
fn from_name(name: &str) -> Result<mime::Mime, UploadError> {
match name
.rsplit('.')
.next()
.ok_or(UploadError::UnsupportedFormat)?
{
"jpg" => Ok(mime::IMAGE_JPEG),
"webp" => Ok(image_webp()),
"png" => Ok(mime::IMAGE_PNG),
"mp4" => Ok(video_mp4()),
"gif" => Ok(mime::IMAGE_GIF),
_ => Err(UploadError::UnsupportedFormat),
}
}
fn from_ext(ext: &str) -> Result<mime::Mime, UploadError> {
match ext {
"jpg" => Ok(mime::IMAGE_JPEG),
"png" => Ok(mime::IMAGE_PNG),
"webp" => Ok(image_webp()),
_ => Err(UploadError::UnsupportedFormat),
}
}
/// Handle responding to succesful uploads /// Handle responding to succesful uploads
#[instrument(skip(value, manager))] #[instrument(skip(value, manager))]
async fn upload( async fn upload(
@ -244,14 +220,12 @@ async fn delete(
type ProcessQuery = Vec<(String, String)>; type ProcessQuery = Vec<(String, String)>;
/// Process files async fn prepare_process(
#[instrument(skip(manager, whitelist))]
async fn process(
query: web::Query<ProcessQuery>, query: web::Query<ProcessQuery>,
ext: web::Path<String>, ext: &str,
manager: web::Data<UploadManager>, manager: &UploadManager,
whitelist: web::Data<Option<HashSet<String>>>, whitelist: &Option<HashSet<String>>,
) -> Result<HttpResponse, UploadError> { ) -> Result<(processor::ProcessChain, Format, String, PathBuf), UploadError> {
let (alias, operations) = let (alias, operations) =
query query
.into_inner() .into_inner()
@ -282,8 +256,6 @@ async fn process(
let chain = self::processor::build_chain(&operations); let chain = self::processor::build_chain(&operations);
let ext = ext.into_inner();
let content_type = from_ext(&ext)?;
let format = ext let format = ext
.parse::<Format>() .parse::<Format>()
.map_err(|_| UploadError::UnsupportedFormat)?; .map_err(|_| UploadError::UnsupportedFormat)?;
@ -291,6 +263,36 @@ async fn process(
let base = manager.image_dir(); let base = manager.image_dir();
let thumbnail_path = self::processor::build_path(base, &chain, processed_name); let thumbnail_path = self::processor::build_path(base, &chain, processed_name);
Ok((chain, format, name, thumbnail_path))
}
async fn process_details(
query: web::Query<ProcessQuery>,
ext: web::Path<String>,
manager: web::Data<UploadManager>,
whitelist: web::Data<Option<HashSet<String>>>,
) -> Result<HttpResponse, UploadError> {
let (_, _, name, thumbnail_path) =
prepare_process(query, ext.as_str(), &manager, &whitelist).await?;
let details = manager.variant_details(thumbnail_path, name).await?;
let details = details.ok_or(UploadError::NoFiles)?;
Ok(HttpResponse::Ok().json(details))
}
/// Process files
#[instrument(skip(manager, whitelist))]
async fn process(
query: web::Query<ProcessQuery>,
ext: web::Path<String>,
manager: web::Data<UploadManager>,
whitelist: web::Data<Option<HashSet<String>>>,
) -> Result<HttpResponse, UploadError> {
let (chain, format, name, thumbnail_path) =
prepare_process(query, ext.as_str(), &manager, &whitelist).await?;
// If the thumbnail doesn't exist, we need to create it // If the thumbnail doesn't exist, we need to create it
let thumbnail_exists = if let Err(e) = actix_fs::metadata(thumbnail_path.clone()).await { let thumbnail_exists = if let Err(e) = actix_fs::metadata(thumbnail_path.clone()).await {
if e.kind() != Some(std::io::ErrorKind::NotFound) { if e.kind() != Some(std::io::ErrorKind::NotFound) {
@ -339,16 +341,27 @@ async fn process(
let path2 = thumbnail_path.clone(); let path2 = thumbnail_path.clone();
let img_bytes2 = img_bytes.clone(); let img_bytes2 = img_bytes.clone();
let store_details = details.is_none();
let details = if let Some(details) = details {
details
} else {
let details = Details::from_bytes(&img_bytes)?;
manager
.store_variant_details(path2.clone(), name.clone(), &details)
.await?;
details
};
// Save the file in another task, we want to return the thumbnail now // Save the file in another task, we want to return the thumbnail now
debug!("Spawning storage task"); debug!("Spawning storage task");
let span = Span::current(); let span = Span::current();
let store_details = details.is_none(); let details2 = details.clone();
actix_rt::spawn(async move { actix_rt::spawn(async move {
let entered = span.enter(); let entered = span.enter();
if store_details { if store_details {
debug!("Storing details"); debug!("Storing details");
if let Err(e) = manager if let Err(e) = manager
.store_variant_details(path2.clone(), name.clone()) .store_variant_details(path2.clone(), name.clone(), &details2)
.await .await
{ {
error!("Error storing details, {}", e); error!("Error storing details, {}", e);
@ -366,30 +379,60 @@ async fn process(
drop(entered); drop(entered);
}); });
let details = details.unwrap_or(Details::now());
return Ok(srv_response( return Ok(srv_response(
Box::pin(futures::stream::once(async { Box::pin(futures::stream::once(async {
Ok(img_bytes) as Result<_, UploadError> Ok(img_bytes) as Result<_, UploadError>
})), })),
content_type, details.content_type(),
7 * DAYS, 7 * DAYS,
details.system_time(), details.system_time(),
)); ));
} }
let stream = actix_fs::read_to_stream(thumbnail_path).await?; let details = if let Some(details) = details {
details
} else {
let details = Details::from_path(thumbnail_path.clone()).await?;
manager
.store_variant_details(thumbnail_path.clone(), name, &details)
.await?;
details
};
let details = details.unwrap_or(Details::now()); let stream = actix_fs::read_to_stream(thumbnail_path).await?;
Ok(srv_response( Ok(srv_response(
stream, stream,
content_type, details.content_type(),
7 * DAYS, 7 * DAYS,
details.system_time(), details.system_time(),
)) ))
} }
/// Fetch file details
async fn details(
alias: web::Path<String>,
manager: web::Data<UploadManager>,
) -> Result<HttpResponse, UploadError> {
let name = manager.from_alias(alias.into_inner()).await?;
let mut path = manager.image_dir();
path.push(name.clone());
let details = manager.variant_details(path.clone(), name.clone()).await?;
let details = if let Some(details) = details {
details
} else {
let new_details = Details::from_path(path.clone()).await?;
manager
.store_variant_details(path.clone(), name, &new_details)
.await?;
new_details
};
Ok(HttpResponse::Ok().json(details))
}
/// Serve files /// Serve files
#[instrument(skip(manager))] #[instrument(skip(manager))]
async fn serve( async fn serve(
@ -397,23 +440,26 @@ async fn serve(
manager: web::Data<UploadManager>, manager: web::Data<UploadManager>,
) -> Result<HttpResponse, UploadError> { ) -> Result<HttpResponse, UploadError> {
let name = manager.from_alias(alias.into_inner()).await?; let name = manager.from_alias(alias.into_inner()).await?;
let content_type = from_name(&name)?;
let mut path = manager.image_dir(); let mut path = manager.image_dir();
path.push(name.clone()); path.push(name.clone());
let details = manager.variant_details(path.clone(), name.clone()).await?; let details = manager.variant_details(path.clone(), name.clone()).await?;
if details.is_none() { let details = if let Some(details) = details {
manager.store_variant_details(path.clone(), name).await?; details
} } else {
let details = Details::from_path(path.clone()).await?;
let details = details.unwrap_or(Details::now()); manager
.store_variant_details(path.clone(), name, &details)
.await?;
details
};
let stream = actix_fs::read_to_stream(path).await?; let stream = actix_fs::read_to_stream(path).await?;
Ok(srv_response( Ok(srv_response(
stream, stream,
content_type, details.content_type(),
7 * DAYS, 7 * DAYS,
details.system_time(), details.system_time(),
)) ))
@ -604,7 +650,17 @@ async fn main() -> Result<(), anyhow::Error> {
.route(web::get().to(delete)), .route(web::get().to(delete)),
) )
.service(web::resource("/original/{filename}").route(web::get().to(serve))) .service(web::resource("/original/{filename}").route(web::get().to(serve)))
.service(web::resource("/process.{ext}").route(web::get().to(process))), .service(web::resource("/process.{ext}").route(web::get().to(process)))
.service(
web::scope("/details")
.service(
web::resource("/original/{filename}").route(web::get().to(details)),
)
.service(
web::resource("/process.{ext}")
.route(web::get().to(process_details)),
),
),
) )
.service( .service(
web::scope("/internal") web::scope("/internal")

View File

@ -46,18 +46,114 @@ impl std::fmt::Debug for UploadManager {
type UploadStream<E> = Pin<Box<dyn Stream<Item = Result<bytes::Bytes, E>>>>; type UploadStream<E> = Pin<Box<dyn Stream<Item = Result<bytes::Bytes, E>>>>;
#[derive(serde::Deserialize, serde::Serialize)] #[derive(Clone)]
pub(crate) struct Serde<T> {
inner: T,
}
impl<T> Serde<T> {
pub(crate) fn new(inner: T) -> Self {
Serde { inner }
}
}
mod my_serde {
impl<T> serde::Serialize for super::Serde<T>
where
T: std::fmt::Display,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = self.inner.to_string();
serde::Serialize::serialize(s.as_str(), serializer)
}
}
impl<'de, T> serde::Deserialize<'de> for super::Serde<T>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: String = serde::Deserialize::deserialize(deserializer)?;
let inner = s
.parse::<T>()
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
Ok(super::Serde { inner })
}
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub(crate) struct Details { pub(crate) struct Details {
width: usize,
height: usize,
content_type: Serde<mime::Mime>,
created_at: time::OffsetDateTime, created_at: time::OffsetDateTime,
} }
fn mime_from_media_type(media_type: rexiv2::MediaType) -> mime::Mime {
match media_type {
rexiv2::MediaType::Jpeg => mime::IMAGE_JPEG,
rexiv2::MediaType::Png => mime::IMAGE_PNG,
rexiv2::MediaType::Gif => mime::IMAGE_GIF,
rexiv2::MediaType::Other(s) if s == "image/webp" => s.parse::<mime::Mime>().unwrap(),
rexiv2::MediaType::Other(s) if s == "video/mp4" || s == "video/quicktime" => {
"video/mp4".parse::<mime::Mime>().unwrap()
}
_ => mime::APPLICATION_OCTET_STREAM,
}
}
impl Details { impl Details {
pub(crate) fn now() -> Self { pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self, UploadError> {
let metadata = rexiv2::Metadata::new_from_buffer(bytes)?;
let mime_type = mime_from_media_type(metadata.get_media_type()?);
let width = metadata.get_pixel_width();
let height = metadata.get_pixel_height();
let details = Details::now(width as usize, height as usize, mime_type);
Ok(details)
}
pub(crate) async fn from_path(path: PathBuf) -> Result<Self, UploadError> {
let (mime_type, width, height) = web::block(move || {
rexiv2::Metadata::new_from_path(&path).and_then(|metadata| {
metadata
.get_media_type()
.map(mime_from_media_type)
.map(|mime_type| {
(
mime_type,
metadata.get_pixel_width(),
metadata.get_pixel_height(),
)
})
})
})
.await?;
Ok(Details::now(width as usize, height as usize, mime_type))
}
fn now(width: usize, height: usize, content_type: mime::Mime) -> Self {
Details { Details {
width,
height,
content_type: Serde::new(content_type),
created_at: time::OffsetDateTime::now_utc(), created_at: time::OffsetDateTime::now_utc(),
} }
} }
pub(crate) fn content_type(&self) -> mime::Mime {
self.content_type.inner.clone()
}
pub(crate) fn system_time(&self) -> std::time::SystemTime { pub(crate) fn system_time(&self) -> std::time::SystemTime {
self.created_at.into() self.created_at.into()
} }
@ -184,7 +280,10 @@ impl UploadManager {
let main_tree = self.inner.main_tree.clone(); let main_tree = self.inner.main_tree.clone();
debug!("Getting details"); debug!("Getting details");
let opt = match web::block(move || main_tree.get(key)).await? { let opt = match web::block(move || main_tree.get(key)).await? {
Some(ivec) => Some(serde_json::from_slice(&ivec)?), Some(ivec) => match serde_json::from_slice(&ivec) {
Ok(details) => Some(details),
Err(_) => None,
},
None => None, None => None,
}; };
debug!("Got details"); debug!("Got details");
@ -196,6 +295,7 @@ impl UploadManager {
&self, &self,
path: PathBuf, path: PathBuf,
filename: String, filename: String,
details: &Details,
) -> Result<(), UploadError> { ) -> Result<(), UploadError> {
let path_string = path.to_str().ok_or(UploadError::Path)?.to_string(); let path_string = path.to_str().ok_or(UploadError::Path)?.to_string();
@ -207,7 +307,7 @@ impl UploadManager {
let key = variant_details_key(&hash, &path_string); let key = variant_details_key(&hash, &path_string);
let main_tree = self.inner.main_tree.clone(); let main_tree = self.inner.main_tree.clone();
let details_value = serde_json::to_string(&Details::now())?; let details_value = serde_json::to_string(details)?;
debug!("Storing details"); debug!("Storing details");
web::block(move || main_tree.insert(key, details_value.as_bytes())).await?; web::block(move || main_tree.insert(key, details_value.as_bytes())).await?;
debug!("Stored details"); debug!("Stored details");