diff --git a/geneit_backend/src/controllers/couples_controller.rs b/geneit_backend/src/controllers/couples_controller.rs index 52fd088..c81f7bd 100644 --- a/geneit_backend/src/controllers/couples_controller.rs +++ b/geneit_backend/src/controllers/couples_controller.rs @@ -25,10 +25,10 @@ enum CoupleControllerErr { MalformedDateOfDivorce, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, Clone)] pub struct CoupleRequest { - wife: Option, - husband: Option, + pub wife: Option, + pub husband: Option, state: Option, #[serde(flatten, with = "prefix_wedding")] wedding: Option, @@ -37,9 +37,14 @@ pub struct CoupleRequest { } impl CoupleRequest { - pub async fn to_couple(self, couple: &mut Couple) -> anyhow::Result<()> { + pub async fn to_couple( + self, + couple: &mut Couple, + check_members_existence: bool, + ) -> anyhow::Result<()> { if let Some(wife) = self.wife { - if !members_service::exists(couple.family_id(), wife).await? { + if check_members_existence && !members_service::exists(couple.family_id(), wife).await? + { return Err(CoupleControllerErr::WifeNotExisting.into()); } @@ -49,7 +54,9 @@ impl CoupleRequest { } if let Some(husband) = self.husband { - if !members_service::exists(couple.family_id(), husband).await? { + if check_members_existence + && !members_service::exists(couple.family_id(), husband).await? + { return Err(CoupleControllerErr::HusbandNotExisting.into()); } } @@ -102,7 +109,7 @@ impl CoupleAPI { pub async fn create(m: FamilyInPath, req: web::Json) -> HttpResult { let mut couple = couples_service::create(m.family_id()).await?; - if let Err(e) = req.0.to_couple(&mut couple).await { + if let Err(e) = req.0.to_couple(&mut couple, true).await { log::error!("Failed to apply couple information! {e}"); couples_service::delete(&mut couple).await?; return Ok(HttpResponse::BadRequest().body(e.to_string())); @@ -137,7 +144,7 @@ pub async fn get_single(m: FamilyAndCoupleInPath) -> HttpResult { pub async fn update(m: FamilyAndCoupleInPath, req: web::Json) -> HttpResult { let mut couple = m.to_couple(); - if let Err(e) = req.0.to_couple(&mut couple).await { + if let Err(e) = req.0.to_couple(&mut couple, true).await { log::error!("Failed to parse couple information {e}!"); return Ok(HttpResponse::BadRequest().body(e.to_string())); } diff --git a/geneit_backend/src/controllers/data_controller.rs b/geneit_backend/src/controllers/data_controller.rs index 78d677d..342b6a9 100644 --- a/geneit_backend/src/controllers/data_controller.rs +++ b/geneit_backend/src/controllers/data_controller.rs @@ -1,15 +1,39 @@ use crate::connections::s3_connection; +use crate::constants; +use crate::controllers::couples_controller::CoupleRequest; +use crate::controllers::members_controller::MemberRequest; use crate::controllers::HttpResult; -use crate::extractors::family_extractor::FamilyInPath; +use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership}; +use crate::models::{CoupleID, MemberID, PhotoID}; use crate::services::{couples_service, members_service, photos_service}; +use actix_multipart::form::tempfile::TempFile; +use actix_multipart::form::MultipartForm; use actix_web::HttpResponse; -use std::io::{Cursor, Write}; +use std::collections::HashMap; +use std::io; +use std::io::{Cursor, Read, Write}; use zip::write::FileOptions; -use zip::CompressionMethod; +use zip::{CompressionMethod, ZipArchive}; const MEMBERS_FILE: &str = "members.json"; const COUPLES_FILE: &str = "couples.json"; +#[derive(serde::Deserialize)] +struct ImportMemberRequest { + id: MemberID, + photo_id: Option, + #[serde(flatten)] + data: MemberRequest, +} + +#[derive(serde::Deserialize)] +struct ImportCoupleRequest { + id: CoupleID, + photo_id: Option, + #[serde(flatten)] + data: CoupleRequest, +} + /// Export whole family data pub async fn export_family(f: FamilyInPath) -> HttpResult { let files_opt = FileOptions::default().compression_method(CompressionMethod::Bzip2); @@ -56,3 +80,120 @@ pub async fn export_family(f: FamilyInPath) -> HttpResult { .content_type("application/zip") .body(buff)) } + +#[derive(Debug, MultipartForm)] +pub struct UploadFamilyDataForm { + #[multipart(rename = "archive")] + archive: TempFile, +} + +/// Import whole family data +pub async fn import_family( + f: FamilyInPathWithAdminMembership, + MultipartForm(form): MultipartForm, +) -> HttpResult { + let mut zip = ZipArchive::new(form.archive.file)?; + + // Parse general information + let members_list = serde_json::from_slice::>(&read_zip_file( + &mut zip, + MEMBERS_FILE, + )?)?; + let mut couples_list = serde_json::from_slice::>(&read_zip_file( + &mut zip, + COUPLES_FILE, + )?)?; + + // Delete all existing members + members_service::delete_all_family(f.family_id()).await?; + couples_service::delete_all_family(f.family_id()).await?; + + // Create empty members set + let mut mapped_req_members = HashMap::new(); + + let mut members_id_mapping = HashMap::new(); + let mut rev_members_id_mapping = HashMap::new(); + let mut new_members = Vec::with_capacity(members_list.len()); + + for req_m in members_list { + // Create member entry in database + let new_m = members_service::create(f.family_id()).await?; + + // Map new member ID with request id + members_id_mapping.insert(req_m.id, new_m.id()); + rev_members_id_mapping.insert(new_m.id(), req_m.id); + + // Save new member structure + new_members.push(new_m); + + // Save request member information, mapped with its old id + mapped_req_members.insert(req_m.id, req_m); + } + + // Set member information, checking for eventual loops + for member in &mut new_members { + let db_id = member.id(); + let req_id = *rev_members_id_mapping.get(&db_id).unwrap(); + + let req_member = mapped_req_members.get(&req_id).unwrap(); + + // Map mother and father id and extract member information + let mut req_member_data = req_member.data.clone(); + if let Some(i) = req_member_data.father { + req_member_data.father = members_id_mapping.get(&i).copied(); + } + if let Some(i) = req_member_data.mother { + req_member_data.mother = members_id_mapping.get(&i).copied(); + } + + req_member_data.to_member(member, true).await?; + } + + // Check for loops + let members_slice = new_members.iter().collect::>(); + if members_service::loop_detection::detect_loop(&members_slice) { + return Ok(HttpResponse::BadRequest().body("Loop detected in members relationships!")); + } + + // Save member information + for member in &mut new_members { + members_service::update(member).await?; + } + + // Extract and insert couples information + let mut couple_mapping = HashMap::new(); + for c in &mut couples_list { + // Map wife and husband + if let Some(i) = c.data.wife { + c.data.wife = members_id_mapping.get(&i).copied(); + } + + if let Some(i) = c.data.husband { + c.data.husband = members_id_mapping.get(&i).copied(); + } + + 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?; + + couples_service::update(&mut db_couple).await?; + } + + // Insert photos + // TODO + + Ok(HttpResponse::Ok().body("go on")) +} + +fn read_zip_file( + archive: &mut ZipArchive, + file: &str, +) -> anyhow::Result> { + let mut entry = archive.by_name(file)?; + assert!(entry.size() < constants::PHOTOS_MAX_SIZE as u64); + let mut buff = Vec::with_capacity(entry.size() as usize); + entry.read_to_end(&mut buff)?; + + Ok(buff) +} diff --git a/geneit_backend/src/controllers/members_controller.rs b/geneit_backend/src/controllers/members_controller.rs index 2b7bb68..165c940 100644 --- a/geneit_backend/src/controllers/members_controller.rs +++ b/geneit_backend/src/controllers/members_controller.rs @@ -12,7 +12,7 @@ use actix_web::{web, HttpResponse}; serde_with::with_prefix!(prefix_birth "birth_"); serde_with::with_prefix!(prefix_death "death_"); -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, Clone)] pub struct RequestDate { pub year: Option, pub month: Option, @@ -29,7 +29,7 @@ impl RequestDate { } } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, Clone)] pub struct MemberRequest { first_name: Option, last_name: Option, @@ -41,8 +41,8 @@ pub struct MemberRequest { postal_code: Option, country: Option, sex: Option, - mother: Option, - father: Option, + pub mother: Option, + pub father: Option, #[serde(flatten, with = "prefix_birth")] birth: Option, #[serde(default)] @@ -104,7 +104,11 @@ fn check_opt_str_val( } impl MemberRequest { - pub async fn to_member(self, member: &mut Member) -> anyhow::Result<()> { + pub async fn to_member( + self, + member: &mut Member, + check_members_existence: bool, /* TODO: remove this field */ + ) -> anyhow::Result<()> { let c = StaticConstraints::default(); check_opt_str_val( &self.first_name, @@ -191,7 +195,9 @@ impl MemberRequest { )?; if let Some(mother) = self.mother { - if !members_service::exists(member.family_id(), mother).await? { + if check_members_existence + && !members_service::exists(member.family_id(), mother).await? + { return Err(MemberControllerErr::MotherNotExisting.into()); } @@ -201,7 +207,9 @@ impl MemberRequest { } if let Some(father) = self.father { - if !members_service::exists(member.family_id(), father).await? { + if check_members_existence + && !members_service::exists(member.family_id(), father).await? + { return Err(MemberControllerErr::FatherNotExisting.into()); } } @@ -254,7 +262,7 @@ impl MemberAPI { pub async fn create(f: FamilyInPath, req: web::Json) -> HttpResult { let mut member = members_service::create(f.family_id()).await?; - if let Err(e) = req.0.to_member(&mut member).await { + if let Err(e) = req.0.to_member(&mut member, true).await { log::error!("Failed to apply member information! {e}"); members_service::delete(&mut member).await?; return Ok(HttpResponse::BadRequest().body(e.to_string())); @@ -284,7 +292,7 @@ pub async fn get_single(m: FamilyAndMemberInPath) -> HttpResult { pub async fn update(m: FamilyAndMemberInPath, req: web::Json) -> HttpResult { let mut member = m.to_member(); - if let Err(e) = req.0.to_member(&mut member).await { + if let Err(e) = req.0.to_member(&mut member, true).await { log::error!("Failed to parse member information {e}!"); return Ok(HttpResponse::BadRequest().body(e.to_string())); } diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 3789d5f..3825999 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -196,6 +196,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/data/export", web::get().to(data_controller::export_family), ) + .route( + "/family/{id}/data/import", + web::put().to(data_controller::import_family), + ) // Photos controller .route( "/photo/{id}", diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 19c49fd..e2a562e 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -202,7 +202,7 @@ impl NewPhoto { #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct MemberID(pub i32); -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone)] pub enum Sex { #[serde(rename = "M")] Male, @@ -310,7 +310,7 @@ pub struct NewMember { #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct CoupleID(pub i32); -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone)] pub enum CoupleState { #[serde(rename = "N")] None,