Can import photos

This commit is contained in:
Pierre HUBERT 2023-08-18 13:41:20 +02:00
parent 9e94cfc298
commit 5fa3d79b4c
8 changed files with 159 additions and 59 deletions

View File

@ -1129,6 +1129,12 @@ dependencies = [
"instant", "instant",
] ]
[[package]]
name = "fastrand"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
[[package]] [[package]]
name = "fdeflate" name = "fdeflate"
version = "0.3.0" version = "0.3.0"
@ -1310,6 +1316,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_with", "serde_with",
"sha2", "sha2",
"tempfile",
"thiserror", "thiserror",
"uuid", "uuid",
"zip", "zip",
@ -1603,17 +1610,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.8.0" version = "2.8.0"
@ -1627,7 +1623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"rustix 0.38.2", "rustix",
"windows-sys", "windows-sys",
] ]
@ -1691,7 +1687,7 @@ dependencies = [
"base64 0.21.2", "base64 0.21.2",
"email-encoding", "email-encoding",
"email_address", "email_address",
"fastrand", "fastrand 1.9.0",
"futures-util", "futures-util",
"hostname", "hostname",
"httpdate", "httpdate",
@ -1725,12 +1721,6 @@ dependencies = [
"urlencoding", "urlencoding",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.3" version = "0.4.3"
@ -2468,20 +2458,6 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "0.37.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8818fa822adcc98b18fedbb3632a6a33213c070556b5aa7c4c8cc21cff565c4c"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys 0.3.8",
"windows-sys",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.2" version = "0.38.2"
@ -2491,7 +2467,7 @@ dependencies = [
"bitflags 2.3.3", "bitflags 2.3.3",
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.3", "linux-raw-sys",
"windows-sys", "windows-sys",
] ]
@ -2784,15 +2760,14 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.6.0" version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651"
dependencies = [ dependencies = [
"autocfg",
"cfg-if", "cfg-if",
"fastrand", "fastrand 2.0.0",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"rustix 0.37.22", "rustix",
"windows-sys", "windows-sys",
] ]

View File

@ -34,4 +34,5 @@ image = "0.24.6"
uuid = { version = "1.4.1", features = ["v4"] } uuid = { version = "1.4.1", features = ["v4"] }
httpdate = "1.0.2" httpdate = "1.0.2"
zip = "0.6.6" zip = "0.6.6"
mime_guess = "2.0.4" mime_guess = "2.0.4"
tempfile = "3.7.1"

View File

@ -164,7 +164,7 @@ pub async fn set_photo(
m: FamilyAndCoupleInPath, m: FamilyAndCoupleInPath,
MultipartForm(form): MultipartForm<UploadPhotoForm>, MultipartForm(form): MultipartForm<UploadPhotoForm>,
) -> HttpResult { ) -> HttpResult {
let photo = photos_service::finalize_upload(form.photo).await?; let photo = photos_service::finalize_upload(form.photo.into()).await?;
let mut couple = m.to_couple(); let mut couple = m.to_couple();
couples_service::remove_photo(&mut couple).await?; couples_service::remove_photo(&mut couple).await?;

View File

@ -5,10 +5,12 @@ use crate::controllers::members_controller::MemberRequest;
use crate::controllers::HttpResult; use crate::controllers::HttpResult;
use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership}; use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
use crate::models::{CoupleID, MemberID, PhotoID}; use crate::models::{CoupleID, MemberID, PhotoID};
use crate::services::photos_service::UploadedFile;
use crate::services::{couples_service, members_service, photos_service}; use crate::services::{couples_service, members_service, photos_service};
use actix_multipart::form::tempfile::TempFile; use actix_multipart::form::tempfile::TempFile;
use actix_multipart::form::MultipartForm; use actix_multipart::form::MultipartForm;
use actix_web::HttpResponse; use actix_web::HttpResponse;
use mime_guess::Mime;
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
use std::io::{Cursor, Read, Write}; use std::io::{Cursor, Read, Write};
@ -17,6 +19,7 @@ use zip::{CompressionMethod, ZipArchive};
const MEMBERS_FILE: &str = "members.json"; const MEMBERS_FILE: &str = "members.json";
const COUPLES_FILE: &str = "couples.json"; const COUPLES_FILE: &str = "couples.json";
const PHOTOS_DIR: &str = "photos/";
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct ImportMemberRequest { struct ImportMemberRequest {
@ -28,7 +31,6 @@ struct ImportMemberRequest {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct ImportCoupleRequest { struct ImportCoupleRequest {
id: CoupleID,
photo_id: Option<PhotoID>, photo_id: Option<PhotoID>,
#[serde(flatten)] #[serde(flatten)]
data: CoupleRequest, data: CoupleRequest,
@ -70,7 +72,7 @@ pub async fn export_family(f: FamilyInPath) -> HttpResult {
let ext = photo.mime_extension().unwrap_or("bad"); let ext = photo.mime_extension().unwrap_or("bad");
let file = s3_connection::get_file(&photo.photo_path()).await?; let file = s3_connection::get_file(&photo.photo_path()).await?;
zip_file.start_file(format!("photos/{}.{ext}", id.0), files_opt)?; zip_file.start_file(format!("{PHOTOS_DIR}{}.{ext}", id.0), files_opt)?;
zip_file.write_all(&file)?; zip_file.write_all(&file)?;
} }
@ -87,6 +89,21 @@ pub struct UploadFamilyDataForm {
archive: TempFile, archive: TempFile,
} }
struct Photo {
path: String,
mimetype: Mime,
}
enum PhotoTarget {
Member(MemberID),
Couple(CoupleID),
}
struct PhotoToProcess {
id: PhotoID,
target: PhotoTarget,
}
/// Import whole family data /// Import whole family data
pub async fn import_family( pub async fn import_family(
f: FamilyInPathWithAdminMembership, f: FamilyInPathWithAdminMembership,
@ -94,6 +111,23 @@ pub async fn import_family(
) -> HttpResult { ) -> HttpResult {
let mut zip = ZipArchive::new(form.archive.file)?; let mut zip = ZipArchive::new(form.archive.file)?;
// Pre-process photos list
let mut photos = HashMap::new();
for file in zip.file_names() {
let (id, ext) = match file.strip_prefix(PHOTOS_DIR).map(|f| f.split_once('.')) {
Some(Some((id, ext))) => (id, ext),
_ => continue,
};
photos.insert(
PhotoID(id.parse()?),
Photo {
path: file.to_string(),
mimetype: mime_guess::from_ext(ext).first_or_octet_stream(),
},
);
}
// Parse general information // Parse general information
let members_list = serde_json::from_slice::<Vec<ImportMemberRequest>>(&read_zip_file( let members_list = serde_json::from_slice::<Vec<ImportMemberRequest>>(&read_zip_file(
&mut zip, &mut zip,
@ -115,6 +149,8 @@ pub async fn import_family(
let mut rev_members_id_mapping = HashMap::new(); let mut rev_members_id_mapping = HashMap::new();
let mut new_members = Vec::with_capacity(members_list.len()); let mut new_members = Vec::with_capacity(members_list.len());
let mut photos_to_insert = Vec::with_capacity(photos.len());
for req_m in members_list { for req_m in members_list {
// Create member entry in database // Create member entry in database
let new_m = members_service::create(f.family_id()).await?; let new_m = members_service::create(f.family_id()).await?;
@ -146,7 +182,14 @@ pub async fn import_family(
req_member_data.mother = members_id_mapping.get(&i).copied(); req_member_data.mother = members_id_mapping.get(&i).copied();
} }
req_member_data.to_member(member, true).await?; req_member_data.to_member(member).await?;
if let Some(id) = req_member.photo_id {
photos_to_insert.push(PhotoToProcess {
id,
target: PhotoTarget::Member(db_id),
})
}
} }
// Check for loops // Check for loops
@ -161,29 +204,72 @@ pub async fn import_family(
} }
// Extract and insert couples information // Extract and insert couples information
let mut couple_mapping = HashMap::new(); let mut new_couples = HashMap::new();
for c in &mut couples_list { for req_couple in &mut couples_list {
// Map wife and husband // Map wife and husband
if let Some(i) = c.data.wife { if let Some(i) = req_couple.data.wife {
c.data.wife = members_id_mapping.get(&i).copied(); req_couple.data.wife = members_id_mapping.get(&i).copied();
} }
if let Some(i) = c.data.husband { if let Some(i) = req_couple.data.husband {
c.data.husband = members_id_mapping.get(&i).copied(); req_couple.data.husband = members_id_mapping.get(&i).copied();
} }
let mut db_couple = couples_service::create(f.family_id()).await?; let mut db_couple = couples_service::create(f.family_id()).await?;
couple_mapping.insert(c.id, db_couple.id());
c.data.clone().to_couple(&mut db_couple, true).await?; req_couple.data.clone().to_couple(&mut db_couple).await?;
couples_service::update(&mut db_couple).await?; couples_service::update(&mut db_couple).await?;
if let Some(id) = req_couple.photo_id {
photos_to_insert.push(PhotoToProcess {
id,
target: PhotoTarget::Couple(db_couple.id()),
})
}
new_couples.insert(db_couple.id(), db_couple);
} }
// Insert photos // Insert member photos
// TODO for photo_to_process in photos_to_insert {
let photo = match photos.get(&photo_to_process.id) {
None => continue,
Some(photo) => photo,
};
Ok(HttpResponse::Ok().body("go on")) let mut file = zip.by_name(&photo.path)?;
if file.size() > constants::PHOTOS_MAX_SIZE as u64 {
return Ok(HttpResponse::BadRequest().body("File is too large!"));
}
let mut buff = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut buff)?;
let photo = photos_service::finalize_upload(UploadedFile::from_memory(
&buff,
Some(photo.mimetype.clone()),
)?)
.await?;
// Update appropriate database entry
match photo_to_process.target {
PhotoTarget::Member(member_id) => {
let member = new_members
.iter_mut()
.find(|m| m.id().eq(&member_id))
.unwrap();
member.set_photo_id(Some(photo.id()));
members_service::update(member).await?;
}
PhotoTarget::Couple(couple_id) => {
let couple = new_couples.get_mut(&couple_id).unwrap();
couple.set_photo_id(Some(photo.id()));
couples_service::update(couple).await?;
}
}
}
Ok(HttpResponse::Accepted().finish())
} }
fn read_zip_file<R: Read + io::Seek>( fn read_zip_file<R: Read + io::Seek>(

View File

@ -325,7 +325,7 @@ pub async fn set_photo(
m: FamilyAndMemberInPath, m: FamilyAndMemberInPath,
MultipartForm(form): MultipartForm<UploadPhotoForm>, MultipartForm(form): MultipartForm<UploadPhotoForm>,
) -> HttpResult { ) -> HttpResult {
let photo = photos_service::finalize_upload(form.photo).await?; let photo = photos_service::finalize_upload(form.photo.into()).await?;
let mut member = m.to_member(); let mut member = m.to_member();
members_service::remove_photo(&mut member).await?; members_service::remove_photo(&mut member).await?;

View File

@ -57,4 +57,10 @@ impl From<std::io::Error> for HttpErr {
} }
} }
impl From<std::num::ParseIntError> for HttpErr {
fn from(value: std::num::ParseIntError) -> Self {
HttpErr { err: value.into() }
}
}
pub type HttpResult = Result<HttpResponse, HttpErr>; pub type HttpResult = Result<HttpResponse, HttpErr>;

View File

@ -227,7 +227,7 @@ impl Sex {
} }
} }
#[derive(Queryable, Debug, serde::Serialize)] #[derive(Queryable, Debug, serde::Serialize, Clone)]
pub struct Member { pub struct Member {
id: i32, id: i32,
family_id: i32, family_id: i32,

View File

@ -8,7 +8,9 @@ use actix_multipart::form::tempfile::TempFile;
use diesel::prelude::*; use diesel::prelude::*;
use image::imageops::FilterType; use image::imageops::FilterType;
use image::ImageOutputFormat; use image::ImageOutputFormat;
use std::io::{Cursor, Read}; use mime_guess::Mime;
use std::fs::File;
use std::io::{Cursor, Read, Seek, Write};
use uuid::Uuid; use uuid::Uuid;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -23,8 +25,38 @@ enum PhotoServiceError {
MimeTypeForbidden(String), MimeTypeForbidden(String),
} }
pub struct UploadedFile {
pub size: usize,
pub content_type: Option<Mime>,
pub file: File,
}
impl From<TempFile> for UploadedFile {
fn from(value: TempFile) -> Self {
Self {
size: value.size,
content_type: value.content_type,
file: value.file.into_file(),
}
}
}
impl UploadedFile {
pub fn from_memory(buff: &[u8], content_type: Option<Mime>) -> anyhow::Result<Self> {
let mut file = tempfile::tempfile()?;
file.write_all(buff)?;
file.rewind()?;
Ok(Self {
size: buff.len(),
content_type,
file,
})
}
}
/// Finalize upload of a photo /// Finalize upload of a photo
pub async fn finalize_upload(mut file: TempFile) -> anyhow::Result<Photo> { pub async fn finalize_upload(mut file: UploadedFile) -> anyhow::Result<Photo> {
// Prerequisite checks // Prerequisite checks
if file.size > PHOTOS_MAX_SIZE { if file.size > PHOTOS_MAX_SIZE {
return Err(PhotoServiceError::FileToLarge(file.size).into()); return Err(PhotoServiceError::FileToLarge(file.size).into());