Can export data as ZIP file

This commit is contained in:
Pierre HUBERT 2025-05-02 23:24:38 +02:00
parent 1b3ce1a98d
commit f335b9d0c0
6 changed files with 321 additions and 8 deletions

View File

@ -34,6 +34,29 @@ dependencies = [
"smallvec", "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]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.10.0" version = "3.10.0"
@ -412,6 +435,15 @@ version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 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]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.7.1" version = "1.7.1"
@ -612,6 +644,25 @@ dependencies = [
"bytes", "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]] [[package]]
name = "castaway" name = "castaway"
version = "0.2.3" version = "0.2.3"
@ -827,6 +878,21 @@ dependencies = [
"libc", "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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@ -836,6 +902,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "crunchy" name = "crunchy"
version = "0.2.3" version = "0.2.3"
@ -915,6 +987,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "deflate64"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.9"
@ -936,6 +1014,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.19" version = "0.99.19"
@ -1365,8 +1454,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.13.3+wasi-0.2.2", "wasi 0.13.3+wasi-0.2.2",
"wasm-bindgen",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@ -1555,6 +1646,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.1" version = "1.10.1"
@ -2046,6 +2143,27 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 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]] [[package]]
name = "maybe-async" name = "maybe-async"
version = "0.2.10" version = "0.2.10"
@ -2141,6 +2259,7 @@ name = "moneymgr_backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-cors", "actix-cors",
"actix-files",
"actix-multipart", "actix-multipart",
"actix-remote-ip", "actix-remote-ip",
"actix-session", "actix-session",
@ -2166,8 +2285,10 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"tempfile",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"zip",
] ]
[[package]] [[package]]
@ -2378,6 +2499,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" 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]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -3142,6 +3273,12 @@ dependencies = [
"rand_core 0.6.4", "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]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -3274,9 +3411,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.19.0" version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.1", "getrandom 0.3.1",
@ -3627,6 +3764,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "v_htmlescape"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@ -4031,6 +4174,15 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 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]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.5" version = "0.7.5"
@ -4101,6 +4253,20 @@ name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 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]] [[package]]
name = "zerovec" name = "zerovec"
@ -4124,6 +4290,46 @@ dependencies = [
"syn", "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]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.3" version = "0.13.3"

View File

@ -14,6 +14,7 @@ actix-cors = "0.7.0"
actix-multipart = "0.7.0" actix-multipart = "0.7.0"
actix-remote-ip = "0.1.0" actix-remote-ip = "0.1.0"
actix-session = { version = "0.10.0", features = ["redis-session"] } actix-session = { version = "0.10.0", features = ["redis-session"] }
actix-files = "0.6.6"
lazy_static = "1.5.0" lazy_static = "1.5.0"
anyhow = "1.0.97" anyhow = "1.0.97"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
@ -32,3 +33,5 @@ rust-embed = { version = "8.6.0" }
sha2 = "0.10.8" sha2 = "0.10.8"
httpdate = "1.0.3" httpdate = "1.0.3"
chrono = "0.4.41" chrono = "0.4.41"
tempfile = "3.19.1"
zip = "2.6.1"

View File

@ -53,3 +53,21 @@ pub const ACCOUNT_TYPES: [AccountTypeDesc; 3] = [
icon: include_str!("../assets/saving.svg"), 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";
}

View File

@ -1,3 +1,5 @@
use crate::app_config::AppConfig;
use crate::constants;
use crate::controllers::HttpResult; use crate::controllers::HttpResult;
use crate::converters::finances_manager_converter::{ use crate::converters::finances_manager_converter::{
FinancesManagerAccount, FinancesManagerFile, FinancesManagerMovement, FinancesManagerAccount, FinancesManagerFile, FinancesManagerMovement,
@ -7,9 +9,23 @@ use crate::extractors::file_extractor::FileExtractor;
use crate::models::accounts::AccountType; use crate::models::accounts::AccountType;
use crate::services::accounts_service::UpdateAccountQuery; use crate::services::accounts_service::UpdateAccountQuery;
use crate::services::movements_service::UpdateMovementQuery; 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 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 /// Import data from a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file
pub async fn finances_manager_import(auth: AuthExtractor, file: FileExtractor) -> HttpResult { 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() Ok(HttpResponse::Ok()
.insert_header(( .insert_header((
"Content-Disposition", "Content-Disposition",
format!( format!("attachment; filename={}", export_filename("finance")),
"attachment; filename=export_{}.finance",
format_date(time() as i64)?.replace('/', "-")
),
)) ))
.body(out.encode())) .body(out.encode()))
} }
/// Add JSON file to ZIP
fn zip_json<E: Serialize>(
zip: &mut ZipWriter<File>,
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))
}

View File

@ -1,6 +1,7 @@
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError}; use actix_web::{HttpResponse, ResponseError};
use std::error::Error; use std::error::Error;
use zip::result::ZipError;
pub mod accounts_controller; pub mod accounts_controller;
pub mod auth_controller; pub mod auth_controller;
@ -30,6 +31,10 @@ pub enum HttpFailure {
InternalError(#[from] anyhow::Error), InternalError(#[from] anyhow::Error),
#[error("a serde_json error occurred: {0}")] #[error("a serde_json error occurred: {0}")]
SerdeJsonError(#[from] serde_json::error::Error), 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 { impl ResponseError for HttpFailure {

View File

@ -170,6 +170,10 @@ async fn main() -> std::io::Result<()> {
"/api/backup/finances_manager/export", "/api/backup/finances_manager/export",
web::get().to(backup_controller::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 // Static assets
.route("/", web::get().to(static_controller::root_index)) .route("/", web::get().to(static_controller::root_index))
.route( .route(