Can download uploaded images
This commit is contained in:
		
							
								
								
									
										1
									
								
								geneit_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								geneit_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -1250,6 +1250,7 @@ dependencies = [
 | 
			
		||||
 "diesel",
 | 
			
		||||
 "env_logger",
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "httpdate",
 | 
			
		||||
 "image",
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "lettre",
 | 
			
		||||
 
 | 
			
		||||
@@ -32,3 +32,4 @@ rust-s3 = "0.33.0"
 | 
			
		||||
sha2 = "0.10.7"
 | 
			
		||||
image = "0.24.6"
 | 
			
		||||
uuid = { version = "1.4.1", features = ["v4"] }
 | 
			
		||||
httpdate = "1.0.2"
 | 
			
		||||
@@ -18,6 +18,10 @@ pub struct AppConfig {
 | 
			
		||||
    #[clap(short, long, env)]
 | 
			
		||||
    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
 | 
			
		||||
    #[clap(long, env, default_value = "localhost")]
 | 
			
		||||
    db_host: String,
 | 
			
		||||
@@ -159,6 +163,21 @@ impl AppConfig {
 | 
			
		||||
        &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
 | 
			
		||||
    pub fn db_connection_chain(&self) -> String {
 | 
			
		||||
        format!(
 | 
			
		||||
 
 | 
			
		||||
@@ -55,6 +55,13 @@ pub async fn upload_file(path: &str, content: &[u8]) -> anyhow::Result<()> {
 | 
			
		||||
    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
 | 
			
		||||
pub async fn delete_file_if_exists(path: &str) -> anyhow::Result<()> {
 | 
			
		||||
    let bucket = AppConfig::get().s3_bucket()?;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ use crate::constants::{SizeConstraint, StaticConstraints};
 | 
			
		||||
use crate::controllers::HttpResult;
 | 
			
		||||
use crate::extractors::family_extractor::FamilyInPath;
 | 
			
		||||
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::utils::countries_utils;
 | 
			
		||||
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
 | 
			
		||||
pub async fn create(f: FamilyInPath, req: web::Json<MemberRequest>) -> HttpResult {
 | 
			
		||||
    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
 | 
			
		||||
pub async fn get_all(f: FamilyInPath) -> HttpResult {
 | 
			
		||||
    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
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ use std::fmt::{Debug, Display, Formatter};
 | 
			
		||||
pub mod auth_controller;
 | 
			
		||||
pub mod families_controller;
 | 
			
		||||
pub mod members_controller;
 | 
			
		||||
pub mod photos_controller;
 | 
			
		||||
pub mod server_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::connections::s3_connection;
 | 
			
		||||
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]
 | 
			
		||||
@@ -161,6 +162,14 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
                "/family/{id}/member/{member_id}/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())?
 | 
			
		||||
    .run()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use crate::schema::{families, members, memberships, photos, users};
 | 
			
		||||
use crate::utils::crypt_utils::sha256;
 | 
			
		||||
use diesel::prelude::*;
 | 
			
		||||
 | 
			
		||||
/// User ID holder
 | 
			
		||||
@@ -121,6 +123,28 @@ pub struct FamilyMembership {
 | 
			
		||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
 | 
			
		||||
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)]
 | 
			
		||||
pub struct Photo {
 | 
			
		||||
    id: i32,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,14 @@
 | 
			
		||||
use sha2::{Digest, Sha512};
 | 
			
		||||
use sha2::{Digest, Sha256, Sha512};
 | 
			
		||||
 | 
			
		||||
/// 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 {
 | 
			
		||||
    let mut hasher = Sha512::new();
 | 
			
		||||
    hasher.update(bytes);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user