diff --git a/geneit_app/src/routes/NotFound.tsx b/geneit_app/src/routes/NotFound.tsx index cfb1cdb..c37f20f 100644 --- a/geneit_app/src/routes/NotFound.tsx +++ b/geneit_app/src/routes/NotFound.tsx @@ -1,6 +1,5 @@ -import { Link } from "react-router-dom"; -import { RouterLink } from "../widgets/RouterLink"; import { Button } from "@mui/material"; +import { RouterLink } from "../widgets/RouterLink"; export function NotFoundRoute(): React.ReactElement { return ( diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index 58d244f..132ba66 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -70,7 +70,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", - "zstd", + "zstd 0.12.3+zstd.1.5.2", ] [[package]] @@ -262,6 +262,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.7.6" @@ -469,6 +480,12 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bcrypt" version = "0.15.0" @@ -573,6 +590,27 @@ dependencies = [ "bytes", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cc" version = "1.0.79" @@ -674,6 +712,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.4.0" @@ -1257,6 +1301,7 @@ dependencies = [ "light-openid", "log", "mailchecker", + "mime_guess", "rand", "redis", "rust-s3", @@ -1267,6 +1312,7 @@ dependencies = [ "sha2", "thiserror", "uuid", + "zip", ] [[package]] @@ -1779,6 +1825,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minidom" version = "0.15.2" @@ -1991,12 +2047,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -2902,6 +2981,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3204,13 +3292,52 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + [[package]] name = "zstd" version = "0.12.3+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" dependencies = [ - "zstd-safe", + "zstd-safe 6.0.5+zstd.1.5.4", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", ] [[package]] diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index 7da641d..c163969 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -32,4 +32,6 @@ rust-s3 = "0.33.0" sha2 = "0.10.7" image = "0.24.6" uuid = { version = "1.4.1", features = ["v4"] } -httpdate = "1.0.2" \ No newline at end of file +httpdate = "1.0.2" +zip = "0.6.6" +mime_guess = "2.0.4" \ No newline at end of file diff --git a/geneit_backend/src/controllers/data_controller.rs b/geneit_backend/src/controllers/data_controller.rs new file mode 100644 index 0000000..78d677d --- /dev/null +++ b/geneit_backend/src/controllers/data_controller.rs @@ -0,0 +1,58 @@ +use crate::connections::s3_connection; +use crate::controllers::HttpResult; +use crate::extractors::family_extractor::FamilyInPath; +use crate::services::{couples_service, members_service, photos_service}; +use actix_web::HttpResponse; +use std::io::{Cursor, Write}; +use zip::write::FileOptions; +use zip::CompressionMethod; + +const MEMBERS_FILE: &str = "members.json"; +const COUPLES_FILE: &str = "couples.json"; + +/// Export whole family data +pub async fn export_family(f: FamilyInPath) -> HttpResult { + let files_opt = FileOptions::default().compression_method(CompressionMethod::Bzip2); + + let members = members_service::get_all_of_family(f.family_id()).await?; + let couples = couples_service::get_all_of_family(f.family_id()).await?; + + let buff = Vec::with_capacity(1000000); + let mut zip_file = zip::ZipWriter::new(Cursor::new(buff)); + + // Add main files + zip_file.start_file(MEMBERS_FILE, files_opt)?; + zip_file.write_all(serde_json::to_string(&members)?.as_bytes())?; + + zip_file.start_file(COUPLES_FILE, files_opt)?; + zip_file.write_all(serde_json::to_string(&couples)?.as_bytes())?; + + // Add photos + let mut photos = Vec::new(); + for member in &members { + if let Some(id) = member.photo_id() { + photos.push(id); + } + } + + for couple in &couples { + if let Some(id) = couple.photo_id() { + photos.push(id); + } + } + + for id in photos { + let photo = photos_service::get_by_id(id).await?; + let ext = photo.mime_extension().unwrap_or("bad"); + let file = s3_connection::get_file(&photo.photo_path()).await?; + + zip_file.start_file(format!("photos/{}.{ext}", id.0), files_opt)?; + zip_file.write_all(&file)?; + } + + let buff = zip_file.finish()?.into_inner(); + + Ok(HttpResponse::Ok() + .content_type("application/zip") + .body(buff)) +} diff --git a/geneit_backend/src/controllers/mod.rs b/geneit_backend/src/controllers/mod.rs index a5b5307..c5ba702 100644 --- a/geneit_backend/src/controllers/mod.rs +++ b/geneit_backend/src/controllers/mod.rs @@ -3,9 +3,11 @@ use actix_web::body::BoxBody; use actix_web::HttpResponse; use std::fmt::{Debug, Display, Formatter}; +use zip::result::ZipError; pub mod auth_controller; pub mod couples_controller; +pub mod data_controller; pub mod families_controller; pub mod members_controller; pub mod photos_controller; @@ -37,4 +39,22 @@ impl From for HttpErr { } } +impl From for HttpErr { + fn from(value: ZipError) -> Self { + HttpErr { err: value.into() } + } +} + +impl From for HttpErr { + fn from(value: serde_json::Error) -> Self { + HttpErr { err: value.into() } + } +} + +impl From for HttpErr { + fn from(value: std::io::Error) -> Self { + HttpErr { err: value.into() } + } +} + pub type HttpResult = Result; diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index c2f92a9..3789d5f 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -6,7 +6,7 @@ use actix_web::{web, App, HttpServer}; use geneit_backend::app_config::AppConfig; use geneit_backend::connections::s3_connection; use geneit_backend::controllers::{ - auth_controller, couples_controller, families_controller, members_controller, + auth_controller, couples_controller, data_controller, families_controller, members_controller, photos_controller, server_controller, users_controller, }; @@ -191,6 +191,11 @@ async fn main() -> std::io::Result<()> { "/family/{id}/couple/{couple_id}/photo", web::delete().to(couples_controller::remove_photo), ) + // Data controller + .route( + "/family/{id}/data/export", + web::get().to(data_controller::export_family), + ) // Photos controller .route( "/photo/{id}", diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 15fb71d..19c49fd 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -168,6 +168,13 @@ impl Photo { pub fn thumbnail_path(&self) -> String { format!("thumbnail/{}", self.file_id) } + + pub fn mime_extension(&self) -> Option<&str> { + mime_guess::get_mime_extensions_str(&self.mime_type) + .map(|e| e.first()) + .unwrap_or_default() + .copied() + } } #[derive(Insertable)]