Skip to content

Commit

Permalink
- Add endpoint for retrieving a random photo.
Browse files Browse the repository at this point in the history
- Restructured photo/get to share code with photo/random
  • Loading branch information
TobiasDeBruijn committed Dec 3, 2023
1 parent 2ceed25 commit 4881cac
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 96 deletions.
1 change: 1 addition & 0 deletions server/chroma/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ img-parts = "0.3.0"
time = "0.3.28"
kamadak-exif = "0.5.5"
cabbage = "0.1.2"
rand = "0.8.5"

[dev-dependencies]
serde_json = "1.0.93"
100 changes: 5 additions & 95 deletions server/chroma/src/routes/v1/photo/get.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
use crate::routes::appdata::WebData;
use crate::routes::authorization::Authorization;
use crate::routes::error::{Error, WebResult};
use crate::routes::v1::photo::{get_prepare_image_data, ImageFormat};
use crate::routes::v1::PhotoQuality;
use actix_multiresponse::Payload;
use actix_web::web;
use dal::database::Photo;
use dal::storage_engine::StorageEngineError;
use dal::DalError;
use image::{DynamicImage, ImageOutputFormat};
use proto::photo_respone::Response;
use proto::{GetPhotoResponse, PhotoRespone};
use proto::GetPhotoResponse;
use serde::Deserialize;
use std::io::Cursor;
use tap::TapFallible;
use tracing::warn;

#[derive(Debug, Deserialize)]
pub struct Query {
Expand All @@ -29,20 +23,12 @@ pub struct Query {
format: ImageFormat,
}

#[derive(Eq, PartialEq, Debug, Default, Deserialize)]
pub enum ImageFormat {
Png,
Jpeg,
#[default]
WebP,
}

/// Retrieve a photo by its ID.
///
/// # Errors
///
/// - If the photo does not exist
/// - If something went wrong
/// - If something went wrong whilst retrieving or preparing the photo
pub async fn get(
_: Authorization,
data: WebData,
Expand All @@ -52,83 +38,7 @@ pub async fn get(
.await?
.ok_or(Error::NotFound)?;

if query.format.eq(&ImageFormat::WebP) {
match photo
.clone()
.photo_to_proto_url(&data.storage, query.quality_preference.clone().into())
.await
{
Ok(p) => {
return Ok(Payload(GetPhotoResponse { photo: Some(p) }));
}
Err(e) => match e {
DalError::Storage(e) => match e {
// URL mode is not supported
StorageEngineError::NotSupported => {}
_ => return Err(e.into()),
},
DalError::Db(e) => return Err(e.into()),
},
}
}

let mut proto = photo
.photo_to_proto_bytes(&data.storage, query.quality_preference.clone().into())
.await
.map_err(|e| match e {
DalError::Storage(e) => Error::from(e),
DalError::Db(e) => Error::from(e),
})?;

let bytes = if let Response::Bytes(b) = proto.data.unwrap().response.unwrap() {
b
} else {
unreachable!()
};
proto.data = Some(PhotoRespone {
response: Some(Response::Bytes(convert_format(bytes, &query.format)?)),
});

let proto =
get_prepare_image_data(&data, photo, &query.quality_preference, &query.format).await?;
Ok(Payload(GetPhotoResponse { photo: Some(proto) }))
}

fn convert_format(bytes: Vec<u8>, format: &ImageFormat) -> WebResult<Vec<u8>> {
match format {
ImageFormat::WebP => Ok(bytes),
ImageFormat::Png => {
let byte_count = bytes.len();
reencode_dynamic_image(decode_image(bytes)?, ImageOutputFormat::Png, byte_count)
}
ImageFormat::Jpeg => {
let byte_count = bytes.len();
reencode_dynamic_image(
decode_image(bytes)?,
ImageOutputFormat::Jpeg(100),
byte_count,
)
}
}
}

fn reencode_dynamic_image(
image: DynamicImage,
format: ImageOutputFormat,
byte_count: usize,
) -> WebResult<Vec<u8>> {
let mut cursor = Cursor::new(Vec::with_capacity(byte_count));
image
.write_to(&mut cursor, format)
.tap_err(|e| warn!("Failed to write image in format: {e}"))?;

Ok(cursor.into_inner())
}

fn decode_image(bytes: Vec<u8>) -> WebResult<DynamicImage> {
match webp::Decoder::new(&bytes).decode() {
Some(webp) => Ok(webp.to_image()),
None => {
warn!("Failed to decode WebP image");
Err(Error::WebpDecode)
}
}
}
138 changes: 137 additions & 1 deletion server/chroma/src/routes/v1/photo/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
use crate::routes::appdata::AppData;
use crate::routes::error::{Error, WebResult};
use crate::routes::routable::Routable;
use crate::routes::v1::PhotoQuality;
use actix_web::web;
use actix_web::web::ServiceConfig;
use dal::database::Photo;
use dal::storage_engine::StorageEngineError;
use dal::DalError;
use image::{DynamicImage, ImageOutputFormat};
use proto::photo_respone::Response;
use proto::PhotoRespone;
use serde::Deserialize;
use std::io::Cursor;
use tap::TapFallible;
use tracing::warn;

mod create;
mod delete;
mod get;
mod list;
mod random;

pub struct Router;

Expand All @@ -16,7 +30,129 @@ impl Routable for Router {
.route("", web::post().to(create::create))
.route("", web::delete().to(delete::delete))
.route("", web::get().to(get::get))
.route("/list", web::get().to(list::list)),
.route("/list", web::get().to(list::list))
.route("/random", web::get().to(random::random)),
);
}
}

#[derive(Eq, PartialEq, Debug, Default, Deserialize)]
pub enum ImageFormat {
Png,
Jpeg,
#[default]
WebP,
}

/// Retrieve the image from storage and convert it into Protobuf format.
/// Adjusting for the requesting quality and image format.
///
/// # Errors
///
/// - If an IO error occurs
/// - If the storage engine fails
/// - If format conversion fails
async fn get_prepare_image_data(
data: &AppData,
photo: Photo<'_>,
quality: &PhotoQuality,
format: &ImageFormat,
) -> WebResult<proto::Photo> {
if format.eq(&ImageFormat::WebP) {
match photo
.clone()
.photo_to_proto_url(&data.storage, quality.clone().into())
.await
{
Ok(p) => return Ok(p),
Err(e) => match e {
DalError::Storage(e) => match e {
// URL mode is not supported
StorageEngineError::NotSupported => {}
_ => return Err(e.into()),
},
DalError::Db(e) => return Err(e.into()),
},
}
}

let mut proto = photo
.photo_to_proto_bytes(&data.storage, quality.clone().into())
.await
.map_err(|e| match e {
DalError::Storage(e) => Error::from(e),
DalError::Db(e) => Error::from(e),
})?;

let bytes = if let Response::Bytes(b) = proto.data.unwrap().response.unwrap() {
b
} else {
unreachable!()
};

proto.data = Some(PhotoRespone {
response: Some(Response::Bytes(convert_format(bytes, &format)?)),
});

Ok(proto)
}

/// Convert an image from one format to the other.
/// The image in `bytes` must be the format specified in `format`.
///
/// # Errors
///
/// If decoding or encoding the image failed.
fn convert_format(bytes: Vec<u8>, format: &ImageFormat) -> WebResult<Vec<u8>> {
match format {
ImageFormat::WebP => Ok(bytes),
ImageFormat::Png => {
let byte_count = bytes.len();
reencode_dynamic_image(decode_image(bytes)?, ImageOutputFormat::Png, byte_count)
}
ImageFormat::Jpeg => {
let byte_count = bytes.len();
reencode_dynamic_image(
decode_image(bytes)?,
ImageOutputFormat::Jpeg(100),
byte_count,
)
}
}
}

/// Encode a DynamicImage to a specific format.
///
/// ## Parameters
/// - `byte_count` is the number of bytes expected to be in the final image.
///
/// # Errors
///
/// If encoding the image failed.
fn reencode_dynamic_image(
image: DynamicImage,
format: ImageOutputFormat,
byte_count: usize,
) -> WebResult<Vec<u8>> {
let mut cursor = Cursor::new(Vec::with_capacity(byte_count));
image
.write_to(&mut cursor, format)
.tap_err(|e| warn!("Failed to write image in format: {e}"))?;

Ok(cursor.into_inner())
}

/// Decode raw WebP image bytes to an [image::DynamicImage]
///
/// # Errors
///
/// If the provided bytes don't form a valid WebP image
fn decode_image(bytes: Vec<u8>) -> WebResult<DynamicImage> {
match webp::Decoder::new(&bytes).decode() {
Some(webp) => Ok(webp.to_image()),
None => {
warn!("Failed to decode WebP image");
Err(Error::WebpDecode)
}
}
}
79 changes: 79 additions & 0 deletions server/chroma/src/routes/v1/photo/random.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use crate::routes::appdata::WebData;
use crate::routes::authorization::Authorization;
use crate::routes::error::{Error, WebResult};
use crate::routes::v1::photo::{get_prepare_image_data, ImageFormat};
use crate::routes::v1::PhotoQuality;
use actix_multiresponse::Payload;
use actix_web::http::StatusCode;
use actix_web::web;
use dal::database::Photo;
use proto::GetPhotoResponse;
use rand::Rng;
use serde::Deserialize;
use tap::TapOptional;
use tracing::warn;

#[derive(Debug, Deserialize)]
pub struct Query {
/// The ID of the previous picture.
/// Providing this value will ensure the picture returned is
/// not the same picture as the picture with the ID provided.
previous_photo_id: Option<String>,
/// A preference for the quality of the photo.
/// If the requested quality does not exist, the photo's original resolution will be returned.
#[serde(default)]
quality_preference: PhotoQuality,
/// The format of the image.
/// E.g., WebP or PNG
#[serde(default)]
format: ImageFormat,
}

/// Get a random photo.
///
/// # Errors
///
/// - If something went wrong whilst retrieving or preparing the photo
pub async fn random(
_: Authorization,
data: WebData,
query: web::Query<Query>,
) -> WebResult<Payload<GetPhotoResponse>> {
let ids = Photo::list_all_ids(&data.db)
.await?
.into_iter()
.filter(|x| match &query.previous_photo_id {
Some(q) => x.ne(q),
None => true,
})
.collect::<Vec<_>>();

let mut rng = rand::thread_rng();

let mut loop_count = 0;
let photo = loop {
let id = ids.get(rng.gen_range(0..ids.len()))
.tap_none(|| warn!("Index out of range: Random index is out of range for list of IDs. This should not be possible."))
.ok_or(Error::Other(StatusCode::INTERNAL_SERVER_ERROR))?;

let photo = Photo::get_by_id(&data.db, id).await?;

match photo {
Some(p) => break p,
None => {
// This loop should really only run once. But it might run twice,
// but if we loop 5 times, something seriously wrong. Terminate
// the loop and return an error.
if loop_count > 5 {
return Err(Error::Other(StatusCode::INTERNAL_SERVER_ERROR));
}

loop_count += 1;
}
}
};

let proto =
get_prepare_image_data(&data, photo, &query.quality_preference, &query.format).await?;
Ok(Payload(GetPhotoResponse { photo: Some(proto) }))
}
7 changes: 7 additions & 0 deletions server/dal/src/database/photo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ impl<'a> Photo<'a> {
Ok(photo.map(|photo| photo.into_photo(db)))
}

pub async fn list_all_ids(db: &'a Database) -> DbResult<Vec<String>> {
sqlx::query_scalar("SELECT id FROM photo_metadata")
.fetch_all(&**db)
.await
.map_err(|e| e.into())
}

pub async fn delete(self) -> DbResult<()> {
let mut tx = self.db.begin().await?;
// Remove the photo from the album cover
Expand Down

0 comments on commit 4881cac

Please sign in to comment.