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]]
name = "pict-rs"
version = "0.3.0-alpha.0"
version = "0.3.0-alpha.1"
dependencies = [
"actix-form-data",
"actix-fs",

View file

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

View file

@ -4,7 +4,7 @@ _a simple image hosting service_
## Usage
### Running
```
pict-rs 0.3.0-alpha.0
pict-rs 0.3.0-alpha.1
USAGE:
pict-rs [FLAGS] [OPTIONS] --path <path>
@ -105,6 +105,21 @@ pict-rs offers the following endpoints:
payload as the `POST` endpoint
- `GET /image/original/{file}` for getting a full-resolution image. `file` here is the `file` key from the
`/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.
existing transformations include
- `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
```
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 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
#[instrument(skip(value, manager))]
async fn upload(
@ -244,14 +220,12 @@ async fn delete(
type ProcessQuery = Vec<(String, String)>;
/// Process files
#[instrument(skip(manager, whitelist))]
async fn process(
async fn prepare_process(
query: web::Query<ProcessQuery>,
ext: web::Path<String>,
manager: web::Data<UploadManager>,
whitelist: web::Data<Option<HashSet<String>>>,
) -> Result<HttpResponse, UploadError> {
ext: &str,
manager: &UploadManager,
whitelist: &Option<HashSet<String>>,
) -> Result<(processor::ProcessChain, Format, String, PathBuf), UploadError> {
let (alias, operations) =
query
.into_inner()
@ -282,8 +256,6 @@ async fn process(
let chain = self::processor::build_chain(&operations);
let ext = ext.into_inner();
let content_type = from_ext(&ext)?;
let format = ext
.parse::<Format>()
.map_err(|_| UploadError::UnsupportedFormat)?;
@ -291,6 +263,36 @@ async fn process(
let base = manager.image_dir();
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
let thumbnail_exists = if let Err(e) = actix_fs::metadata(thumbnail_path.clone()).await {
if e.kind() != Some(std::io::ErrorKind::NotFound) {
@ -339,16 +341,27 @@ async fn process(
let path2 = thumbnail_path.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
debug!("Spawning storage task");
let span = Span::current();
let store_details = details.is_none();
let details2 = details.clone();
actix_rt::spawn(async move {
let entered = span.enter();
if store_details {
debug!("Storing details");
if let Err(e) = manager
.store_variant_details(path2.clone(), name.clone())
.store_variant_details(path2.clone(), name.clone(), &details2)
.await
{
error!("Error storing details, {}", e);
@ -366,30 +379,60 @@ async fn process(
drop(entered);
});
let details = details.unwrap_or(Details::now());
return Ok(srv_response(
Box::pin(futures::stream::once(async {
Ok(img_bytes) as Result<_, UploadError>
})),
content_type,
details.content_type(),
7 * DAYS,
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(
stream,
content_type,
details.content_type(),
7 * DAYS,
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
#[instrument(skip(manager))]
async fn serve(
@ -397,23 +440,26 @@ async fn serve(
manager: web::Data<UploadManager>,
) -> Result<HttpResponse, UploadError> {
let name = manager.from_alias(alias.into_inner()).await?;
let content_type = from_name(&name)?;
let mut path = manager.image_dir();
path.push(name.clone());
let details = manager.variant_details(path.clone(), name.clone()).await?;
if details.is_none() {
manager.store_variant_details(path.clone(), name).await?;
}
let details = details.unwrap_or(Details::now());
let details = if let Some(details) = details {
details
} else {
let details = Details::from_path(path.clone()).await?;
manager
.store_variant_details(path.clone(), name, &details)
.await?;
details
};
let stream = actix_fs::read_to_stream(path).await?;
Ok(srv_response(
stream,
content_type,
details.content_type(),
7 * DAYS,
details.system_time(),
))
@ -604,7 +650,17 @@ async fn main() -> Result<(), anyhow::Error> {
.route(web::get().to(delete)),
)
.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(
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>>>>;
#[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 {
width: usize,
height: usize,
content_type: Serde<mime::Mime>,
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 {
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 {
width,
height,
content_type: Serde::new(content_type),
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 {
self.created_at.into()
}
@ -184,7 +280,10 @@ impl UploadManager {
let main_tree = self.inner.main_tree.clone();
debug!("Getting details");
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,
};
debug!("Got details");
@ -196,6 +295,7 @@ impl UploadManager {
&self,
path: PathBuf,
filename: String,
details: &Details,
) -> Result<(), UploadError> {
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 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");
web::block(move || main_tree.insert(key, details_value.as_bytes())).await?;
debug!("Stored details");