Can download uploaded images
This commit is contained in:
parent
c6148f6562
commit
75438f4ae0
1
geneit_backend/Cargo.lock
generated
1
geneit_backend/Cargo.lock
generated
@ -1250,6 +1250,7 @@ dependencies = [
|
|||||||
"diesel",
|
"diesel",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"httpdate",
|
||||||
"image",
|
"image",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lettre",
|
"lettre",
|
||||||
|
@ -31,4 +31,5 @@ rust_iso3166 = "0.1.10"
|
|||||||
rust-s3 = "0.33.0"
|
rust-s3 = "0.33.0"
|
||||||
sha2 = "0.10.7"
|
sha2 = "0.10.7"
|
||||||
image = "0.24.6"
|
image = "0.24.6"
|
||||||
uuid = { version = "1.4.1", features = ["v4"] }
|
uuid = { version = "1.4.1", features = ["v4"] }
|
||||||
|
httpdate = "1.0.2"
|
@ -18,6 +18,10 @@ pub struct AppConfig {
|
|||||||
#[clap(short, long, env)]
|
#[clap(short, long, env)]
|
||||||
pub proxy_ip: Option<String>,
|
pub proxy_ip: Option<String>,
|
||||||
|
|
||||||
|
/// Secret key, used to sign some resources. Must be randomly generated
|
||||||
|
#[clap(long, env, default_value = "")]
|
||||||
|
secret: String,
|
||||||
|
|
||||||
/// PostgreSQL database host
|
/// PostgreSQL database host
|
||||||
#[clap(long, env, default_value = "localhost")]
|
#[clap(long, env, default_value = "localhost")]
|
||||||
db_host: String,
|
db_host: String,
|
||||||
@ -159,6 +163,21 @@ impl AppConfig {
|
|||||||
&ARGS
|
&ARGS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get app secret
|
||||||
|
pub fn secret(&self) -> &str {
|
||||||
|
let mut secret = self.secret.as_str();
|
||||||
|
|
||||||
|
if cfg!(debug_assertions) && secret.is_empty() {
|
||||||
|
secret = "DEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEY";
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret.is_empty() {
|
||||||
|
panic!("SECRET is undefined or too short (min 30 chars)!")
|
||||||
|
}
|
||||||
|
|
||||||
|
secret
|
||||||
|
}
|
||||||
|
|
||||||
/// Get full db connection chain
|
/// Get full db connection chain
|
||||||
pub fn db_connection_chain(&self) -> String {
|
pub fn db_connection_chain(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
|
@ -55,6 +55,13 @@ pub async fn upload_file(path: &str, content: &[u8]) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a file
|
||||||
|
pub async fn get_file(path: &str) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let bucket = AppConfig::get().s3_bucket()?;
|
||||||
|
|
||||||
|
Ok(bucket.get_object(path).await?.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a file, if it exists
|
/// Delete a file, if it exists
|
||||||
pub async fn delete_file_if_exists(path: &str) -> anyhow::Result<()> {
|
pub async fn delete_file_if_exists(path: &str) -> anyhow::Result<()> {
|
||||||
let bucket = AppConfig::get().s3_bucket()?;
|
let bucket = AppConfig::get().s3_bucket()?;
|
||||||
|
@ -2,7 +2,7 @@ use crate::constants::{SizeConstraint, StaticConstraints};
|
|||||||
use crate::controllers::HttpResult;
|
use crate::controllers::HttpResult;
|
||||||
use crate::extractors::family_extractor::FamilyInPath;
|
use crate::extractors::family_extractor::FamilyInPath;
|
||||||
use crate::extractors::member_extractor::FamilyAndMemberInPath;
|
use crate::extractors::member_extractor::FamilyAndMemberInPath;
|
||||||
use crate::models::{Member, MemberID, Sex};
|
use crate::models::{Member, MemberID, PhotoID, Sex};
|
||||||
use crate::services::{members_service, photos_service};
|
use crate::services::{members_service, photos_service};
|
||||||
use crate::utils::countries_utils;
|
use crate::utils::countries_utils;
|
||||||
use actix_multipart::form::tempfile::TempFile;
|
use actix_multipart::form::tempfile::TempFile;
|
||||||
@ -231,6 +231,22 @@ impl MemberRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct MemberAPI {
|
||||||
|
#[serde(flatten)]
|
||||||
|
member: Member,
|
||||||
|
signed_photo_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemberAPI {
|
||||||
|
pub fn new(member: Member) -> Self {
|
||||||
|
Self {
|
||||||
|
signed_photo_id: member.photo_id().as_ref().map(PhotoID::to_signed_hash),
|
||||||
|
member,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new family member
|
/// Create a new family member
|
||||||
pub async fn create(f: FamilyInPath, req: web::Json<MemberRequest>) -> HttpResult {
|
pub async fn create(f: FamilyInPath, req: web::Json<MemberRequest>) -> HttpResult {
|
||||||
let mut member = members_service::create(f.family_id()).await?;
|
let mut member = members_service::create(f.family_id()).await?;
|
||||||
@ -253,12 +269,12 @@ pub async fn create(f: FamilyInPath, req: web::Json<MemberRequest>) -> HttpResul
|
|||||||
/// Get the entire list of members of the family
|
/// Get the entire list of members of the family
|
||||||
pub async fn get_all(f: FamilyInPath) -> HttpResult {
|
pub async fn get_all(f: FamilyInPath) -> HttpResult {
|
||||||
let members = members_service::get_all_of_family(f.family_id()).await?;
|
let members = members_service::get_all_of_family(f.family_id()).await?;
|
||||||
Ok(HttpResponse::Ok().json(members))
|
Ok(HttpResponse::Ok().json(members.into_iter().map(MemberAPI::new).collect::<Vec<_>>()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the information of a single family member
|
/// Get the information of a single family member
|
||||||
pub async fn get_single(m: FamilyAndMemberInPath) -> HttpResult {
|
pub async fn get_single(m: FamilyAndMemberInPath) -> HttpResult {
|
||||||
Ok(HttpResponse::Ok().json(m.to_member()))
|
Ok(HttpResponse::Ok().json(MemberAPI::new(m.to_member())))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a member information
|
/// Update a member information
|
||||||
|
@ -7,6 +7,7 @@ use std::fmt::{Debug, Display, Formatter};
|
|||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
pub mod families_controller;
|
pub mod families_controller;
|
||||||
pub mod members_controller;
|
pub mod members_controller;
|
||||||
|
pub mod photos_controller;
|
||||||
pub mod server_controller;
|
pub mod server_controller;
|
||||||
pub mod users_controller;
|
pub mod users_controller;
|
||||||
|
|
||||||
|
74
geneit_backend/src/controllers/photos_controller.rs
Normal file
74
geneit_backend/src/controllers/photos_controller.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
use crate::connections::s3_connection;
|
||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::models::PhotoID;
|
||||||
|
use crate::services::photos_service;
|
||||||
|
use actix_web::http::header;
|
||||||
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
|
use std::ops::Add;
|
||||||
|
use std::time::{Duration, UNIX_EPOCH};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct PhotoIdPath {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_full_size(id: web::Path<PhotoIdPath>, req: HttpRequest) -> HttpResult {
|
||||||
|
get_photo(&id, true, req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_thumbnail(id: web::Path<PhotoIdPath>, req: HttpRequest) -> HttpResult {
|
||||||
|
get_photo(&id, false, req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_photo(id: &PhotoIdPath, full_size: bool, req: HttpRequest) -> HttpResult {
|
||||||
|
let id = match PhotoID::from_signed_hash(&id.id) {
|
||||||
|
None => {
|
||||||
|
return Ok(HttpResponse::Unauthorized().body("Invalid hash"));
|
||||||
|
}
|
||||||
|
Some(p) => p,
|
||||||
|
};
|
||||||
|
|
||||||
|
let photo = photos_service::get_by_id(id).await?;
|
||||||
|
|
||||||
|
let (hash, content_type) = match full_size {
|
||||||
|
true => (photo.sha512.as_str(), photo.mime_type.as_str()),
|
||||||
|
false => (photo.thumb_sha512.as_str(), "application/png"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if an upload is un-necessary
|
||||||
|
if let Some(c) = req.headers().get(header::IF_NONE_MATCH) {
|
||||||
|
if c.to_str().unwrap_or("") == hash {
|
||||||
|
return Ok(HttpResponse::NotModified().finish());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) {
|
||||||
|
let date_str = c.to_str().unwrap_or("");
|
||||||
|
if let Ok(date) = httpdate::parse_http_date(date_str) {
|
||||||
|
if date
|
||||||
|
.add(Duration::from_secs(1))
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
>= photo.time_create as u64
|
||||||
|
{
|
||||||
|
return Ok(HttpResponse::NotModified().finish());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = s3_connection::get_file(&match full_size {
|
||||||
|
true => photo.photo_path(),
|
||||||
|
false => photo.thumbnail_path(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(content_type)
|
||||||
|
.insert_header(("etag", hash))
|
||||||
|
.insert_header((
|
||||||
|
"last-modified",
|
||||||
|
httpdate::fmt_http_date(UNIX_EPOCH + Duration::from_secs(photo.time_create as u64)),
|
||||||
|
))
|
||||||
|
.body(bytes))
|
||||||
|
}
|
@ -6,7 +6,8 @@ use actix_web::{web, App, HttpServer};
|
|||||||
use geneit_backend::app_config::AppConfig;
|
use geneit_backend::app_config::AppConfig;
|
||||||
use geneit_backend::connections::s3_connection;
|
use geneit_backend::connections::s3_connection;
|
||||||
use geneit_backend::controllers::{
|
use geneit_backend::controllers::{
|
||||||
auth_controller, families_controller, members_controller, server_controller, users_controller,
|
auth_controller, families_controller, members_controller, photos_controller, server_controller,
|
||||||
|
users_controller,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
@ -161,6 +162,14 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/family/{id}/member/{member_id}/photo",
|
"/family/{id}/member/{member_id}/photo",
|
||||||
web::delete().to(members_controller::remove_photo),
|
web::delete().to(members_controller::remove_photo),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/photo/{id}",
|
||||||
|
web::get().to(photos_controller::get_full_size),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/photo/{id}/thumbnail",
|
||||||
|
web::get().to(photos_controller::get_thumbnail),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.bind(AppConfig::get().listen_address.as_str())?
|
.bind(AppConfig::get().listen_address.as_str())?
|
||||||
.run()
|
.run()
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
use crate::schema::{families, members, memberships, photos, users};
|
use crate::schema::{families, members, memberships, photos, users};
|
||||||
|
use crate::utils::crypt_utils::sha256;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
/// User ID holder
|
/// User ID holder
|
||||||
@ -121,6 +123,28 @@ pub struct FamilyMembership {
|
|||||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||||
pub struct PhotoID(pub i32);
|
pub struct PhotoID(pub i32);
|
||||||
|
|
||||||
|
impl PhotoID {
|
||||||
|
pub fn to_signed_hash(&self) -> String {
|
||||||
|
let secret = AppConfig::get().secret();
|
||||||
|
format!(
|
||||||
|
"{}-{}",
|
||||||
|
self.0,
|
||||||
|
sha256(format!("{secret}{}{secret}", self.0).as_bytes())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_signed_hash(hash: &str) -> Option<Self> {
|
||||||
|
let (id, _) = hash.split_once('-')?;
|
||||||
|
let id = Self(id.parse().ok()?);
|
||||||
|
|
||||||
|
if id.to_signed_hash() != hash {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Debug, serde::Serialize)]
|
#[derive(Queryable, Debug, serde::Serialize)]
|
||||||
pub struct Photo {
|
pub struct Photo {
|
||||||
id: i32,
|
id: i32,
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
use sha2::{Digest, Sha512};
|
use sha2::{Digest, Sha256, Sha512};
|
||||||
|
|
||||||
/// Compute hash of a slice of bytes
|
/// Compute hash of a slice of bytes
|
||||||
|
pub fn sha256(bytes: &[u8]) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(bytes);
|
||||||
|
let h = hasher.finalize();
|
||||||
|
format!("{:x}", h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute hash of a slice of bytes (sha512)
|
||||||
pub fn sha512(bytes: &[u8]) -> String {
|
pub fn sha512(bytes: &[u8]) -> String {
|
||||||
let mut hasher = Sha512::new();
|
let mut hasher = Sha512::new();
|
||||||
hasher.update(bytes);
|
hasher.update(bytes);
|
||||||
|
Loading…
Reference in New Issue
Block a user