From f335b9d0c07e8e8cb52a81476645a815047d1855 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 2 May 2025 23:24:38 +0200 Subject: [PATCH] Can export data as ZIP file --- moneymgr_backend/Cargo.lock | 210 +++++++++++++++++- moneymgr_backend/Cargo.toml | 3 + moneymgr_backend/src/constants.rs | 18 ++ .../src/controllers/backup_controller.rs | 89 +++++++- moneymgr_backend/src/controllers/mod.rs | 5 + moneymgr_backend/src/main.rs | 4 + 6 files changed, 321 insertions(+), 8 deletions(-) diff --git a/moneymgr_backend/Cargo.lock b/moneymgr_backend/Cargo.lock index 623444a..9b5157e 100644 --- a/moneymgr_backend/Cargo.lock +++ b/moneymgr_backend/Cargo.lock @@ -34,6 +34,29 @@ dependencies = [ "smallvec", ] +[[package]] +name = "actix-files" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "bitflags", + "bytes", + "derive_more 0.99.19", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + [[package]] name = "actix-http" version = "3.10.0" @@ -412,6 +435,15 @@ version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -612,6 +644,25 @@ dependencies = [ "bytes", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "castaway" version = "0.2.3" @@ -827,6 +878,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -836,6 +902,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.3" @@ -915,6 +987,12 @@ dependencies = [ "syn", ] +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "der" version = "0.7.9" @@ -936,6 +1014,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "0.99.19" @@ -1365,8 +1454,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -1555,6 +1646,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -2046,6 +2143,27 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "maybe-async" version = "0.2.10" @@ -2141,6 +2259,7 @@ name = "moneymgr_backend" version = "0.1.0" dependencies = [ "actix-cors", + "actix-files", "actix-multipart", "actix-remote-ip", "actix-session", @@ -2166,8 +2285,10 @@ dependencies = [ "serde", "serde_json", "sha2", + "tempfile", "thiserror 2.0.12", "tokio", + "zip", ] [[package]] @@ -2378,6 +2499,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3142,6 +3273,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -3274,9 +3411,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", "getrandom 0.3.1", @@ -3627,6 +3764,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4031,6 +4174,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yoke" version = "0.7.5" @@ -4101,6 +4253,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerovec" @@ -4124,6 +4290,46 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "flate2", + "getrandom 0.3.1", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/moneymgr_backend/Cargo.toml b/moneymgr_backend/Cargo.toml index d014640..70d3fcf 100644 --- a/moneymgr_backend/Cargo.toml +++ b/moneymgr_backend/Cargo.toml @@ -14,6 +14,7 @@ actix-cors = "0.7.0" actix-multipart = "0.7.0" actix-remote-ip = "0.1.0" actix-session = { version = "0.10.0", features = ["redis-session"] } +actix-files = "0.6.6" lazy_static = "1.5.0" anyhow = "1.0.97" serde = { version = "1.0.219", features = ["derive"] } @@ -32,3 +33,5 @@ rust-embed = { version = "8.6.0" } sha2 = "0.10.8" httpdate = "1.0.3" chrono = "0.4.41" +tempfile = "3.19.1" +zip = "2.6.1" \ No newline at end of file diff --git a/moneymgr_backend/src/constants.rs b/moneymgr_backend/src/constants.rs index 25da25a..d535683 100644 --- a/moneymgr_backend/src/constants.rs +++ b/moneymgr_backend/src/constants.rs @@ -53,3 +53,21 @@ pub const ACCOUNT_TYPES: [AccountTypeDesc; 3] = [ icon: include_str!("../assets/saving.svg"), }, ]; + +/// ZIP export paths +pub mod zip_export { + /// Accounts file path inside archive + pub const ACCOUNTS_FILE: &str = "accounts.json"; + + /// Movements file path inside archive + pub const MOVEMENTS_FILE: &str = "movements.json"; + + /// Files list file path inside archive + pub const FILES_FILE: &str = "files.json"; + + /// Files directory inside archive + pub const FILES_DIR: &str = "files/"; + + /// Inbox file path inside archive + pub const INBOX_FILE: &str = "inbox.json"; +} diff --git a/moneymgr_backend/src/controllers/backup_controller.rs b/moneymgr_backend/src/controllers/backup_controller.rs index 4e98a98..e2399f9 100644 --- a/moneymgr_backend/src/controllers/backup_controller.rs +++ b/moneymgr_backend/src/controllers/backup_controller.rs @@ -1,3 +1,5 @@ +use crate::app_config::AppConfig; +use crate::constants; use crate::controllers::HttpResult; use crate::converters::finances_manager_converter::{ FinancesManagerAccount, FinancesManagerFile, FinancesManagerMovement, @@ -7,9 +9,23 @@ use crate::extractors::file_extractor::FileExtractor; use crate::models::accounts::AccountType; use crate::services::accounts_service::UpdateAccountQuery; use crate::services::movements_service::UpdateMovementQuery; -use crate::services::{accounts_service, movements_service}; +use crate::services::{accounts_service, files_service, movements_service}; use crate::utils::time_utils::{format_date, time}; -use actix_web::HttpResponse; +use actix_files::NamedFile; +use actix_web::{HttpRequest, HttpResponse}; +use serde::Serialize; +use std::fs::File; +use std::io::Write; +use zip::ZipWriter; +use zip::write::SimpleFileOptions; + +/// Generate export filename +fn export_filename(ext: &str) -> String { + format!( + "export_{}.{ext}", + format_date(time() as i64).unwrap().replace('/', "-") + ) +} /// Import data from a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file pub async fn finances_manager_import(auth: AuthExtractor, file: FileExtractor) -> HttpResult { @@ -69,10 +85,71 @@ pub async fn finances_manager_export(auth: AuthExtractor) -> HttpResult { Ok(HttpResponse::Ok() .insert_header(( "Content-Disposition", - format!( - "attachment; filename=export_{}.finance", - format_date(time() as i64)?.replace('/', "-") - ), + format!("attachment; filename={}", export_filename("finance")), )) .body(out.encode())) } + +/// Add JSON file to ZIP +fn zip_json( + zip: &mut ZipWriter, + path: &str, + content: &E, +) -> anyhow::Result<()> { + let file_encoded = serde_json::to_string(&content)?; + + let options = SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated) + .unix_permissions(0o750); + + zip.start_file(path, options)?; + zip.write_all(file_encoded.as_bytes())?; + Ok(()) +} + +/// Export all user data as ZIP +pub async fn zip_export(req: HttpRequest, auth: AuthExtractor) -> HttpResult { + let dest_dir = tempfile::tempdir_in(&AppConfig::get().temp_dir)?; + let zip_path = dest_dir.path().join("export.zip"); + + let file = File::create(&zip_path)?; + let mut zip = ZipWriter::new(file); + + // Process all JSON documents + zip_json( + &mut zip, + constants::zip_export::ACCOUNTS_FILE, + &accounts_service::get_list_user(auth.user_id()).await?, + )?; + zip_json( + &mut zip, + constants::zip_export::MOVEMENTS_FILE, + &movements_service::get_all_movements_user(auth.user_id()).await?, + )?; + let files_list = files_service::get_all_files_user(auth.user_id()).await?; + zip_json(&mut zip, constants::zip_export::FILES_FILE, &files_list)?; + // TODO : inbox + + // Process all files + for file in files_list { + let buff = files_service::get_file_content(&file).await?; + + let options = SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated) + .unix_permissions(0o750); + + zip.start_file( + format!("{}{}", constants::zip_export::FILES_DIR, file.sha512), + options, + )?; + zip.write_all(&buff)?; + } + + // Finalize ZIP and return response + zip.finish()?; + let file = File::open(zip_path)?; + + let file = NamedFile::from_file(file, export_filename("zip"))?; + + Ok(file.into_response(&req)) +} diff --git a/moneymgr_backend/src/controllers/mod.rs b/moneymgr_backend/src/controllers/mod.rs index 3f7a4ca..0508847 100644 --- a/moneymgr_backend/src/controllers/mod.rs +++ b/moneymgr_backend/src/controllers/mod.rs @@ -1,6 +1,7 @@ use actix_web::http::StatusCode; use actix_web::{HttpResponse, ResponseError}; use std::error::Error; +use zip::result::ZipError; pub mod accounts_controller; pub mod auth_controller; @@ -30,6 +31,10 @@ pub enum HttpFailure { InternalError(#[from] anyhow::Error), #[error("a serde_json error occurred: {0}")] SerdeJsonError(#[from] serde_json::error::Error), + #[error("a zip manipulation error occurred: {0}")] + ZipError(#[from] ZipError), + #[error("a standard I/O manipulation error occurred: {0}")] + StdIoError(#[from] std::io::Error), } impl ResponseError for HttpFailure { diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index 65fd4d1..f4529a8 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -170,6 +170,10 @@ async fn main() -> std::io::Result<()> { "/api/backup/finances_manager/export", web::get().to(backup_controller::finances_manager_export), ) + .route( + "/api/backup/zip/export", + web::get().to(backup_controller::zip_export), + ) // Static assets .route("/", web::get().to(static_controller::root_index)) .route(