All checks were successful
continuous-integration/drone/push Build is passing
367 lines
12 KiB
Rust
367 lines
12 KiB
Rust
use crate::app_config::AppConfig;
|
|
use crate::constants;
|
|
use crate::controllers::HttpResult;
|
|
use crate::converters::finances_manager_converter::{
|
|
FinancesManagerAccount, FinancesManagerFile, FinancesManagerMovement,
|
|
};
|
|
use crate::extractors::auth_extractor::AuthExtractor;
|
|
use crate::extractors::file_extractor::FileExtractor;
|
|
use crate::models::accounts::{Account, AccountID, AccountType};
|
|
use crate::models::files;
|
|
use crate::models::files::FileID;
|
|
use crate::models::inbox::InboxEntry;
|
|
use crate::models::movements::{Movement, MovementID};
|
|
use crate::services::accounts_service::UpdateAccountQuery;
|
|
use crate::services::inbox_service::UpdateInboxEntryQuery;
|
|
use crate::services::movements_service::UpdateMovementQuery;
|
|
use crate::services::{accounts_service, files_service, inbox_service, movements_service};
|
|
use crate::utils::time_utils::{format_date, time};
|
|
use actix_files::NamedFile;
|
|
use actix_web::{HttpRequest, HttpResponse};
|
|
use rust_xlsxwriter::{Color, Format, Formula, Table, Workbook};
|
|
use serde::Serialize;
|
|
use serde::de::DeserializeOwned;
|
|
use std::collections::HashMap;
|
|
use std::fs::File;
|
|
use std::io::{Cursor, Read, Seek, Write};
|
|
use zip::write::SimpleFileOptions;
|
|
use zip::{ZipArchive, ZipWriter};
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum BackupControllerError {
|
|
#[error("The account with id {0:?} does not exists!")]
|
|
NonexistentAccountId(AccountID),
|
|
#[error("The file with id {0:?} does not exists!")]
|
|
NonexistentFileId(FileID),
|
|
#[error("The movement with id {0:?} does not exists!")]
|
|
NonexistentMovementId(MovementID),
|
|
}
|
|
|
|
/// 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 {
|
|
let file = FinancesManagerFile::parse(&String::from_utf8_lossy(&file.buff))?;
|
|
|
|
// Create each account & push the movements independently
|
|
for file_account in file.accounts {
|
|
let account = accounts_service::create(
|
|
auth.user_id(),
|
|
&UpdateAccountQuery {
|
|
name: file_account.name,
|
|
r#type: AccountType::Cash,
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
for file_movement in file_account.movements {
|
|
movements_service::create(&UpdateMovementQuery {
|
|
account_id: account.id(),
|
|
time: file_movement.time,
|
|
label: file_movement.label,
|
|
file_id: None,
|
|
amount: file_movement.amount,
|
|
checked: false,
|
|
})
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(HttpResponse::Accepted().finish())
|
|
}
|
|
|
|
/// Export data to a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file
|
|
pub async fn finances_manager_export(auth: AuthExtractor) -> HttpResult {
|
|
let mut accounts = accounts_service::get_list_user(auth.user_id()).await?;
|
|
accounts.sort_by_key(|a| a.id());
|
|
|
|
let mut out = FinancesManagerFile { accounts: vec![] };
|
|
|
|
for account in accounts {
|
|
let mut movements = movements_service::get_list_account(account.id()).await?;
|
|
movements.sort_by(|a, b| b.time.cmp(&a.time));
|
|
|
|
let mut file_account = FinancesManagerAccount {
|
|
name: account.name,
|
|
movements: Vec::with_capacity(movements.len()),
|
|
};
|
|
|
|
for movement in movements {
|
|
file_account.movements.push(FinancesManagerMovement {
|
|
label: movement.label,
|
|
time: movement.time as u64,
|
|
amount: movement.amount,
|
|
});
|
|
}
|
|
|
|
out.accounts.push(file_account);
|
|
}
|
|
|
|
Ok(HttpResponse::Ok()
|
|
.insert_header((
|
|
"Content-Disposition",
|
|
format!("attachment; filename={}", export_filename("finance")),
|
|
))
|
|
.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?,
|
|
)?;
|
|
zip_json(
|
|
&mut zip,
|
|
constants::zip_export::INBOX_FILE,
|
|
&inbox_service::get_list_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)?;
|
|
|
|
// 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))
|
|
}
|
|
|
|
/// Read JSON from archive
|
|
fn unzip_json<E: DeserializeOwned, R: Read + Seek>(
|
|
zip: &mut ZipArchive<R>,
|
|
path: &str,
|
|
) -> anyhow::Result<E> {
|
|
let mut file = zip.by_name(path)?;
|
|
let mut content = String::with_capacity(file.size() as usize);
|
|
file.read_to_string(&mut content)?;
|
|
|
|
Ok(serde_json::from_str(&content)?)
|
|
}
|
|
|
|
/// Replace all data with data included in ZIP file
|
|
pub async fn zip_import(auth: AuthExtractor, file: FileExtractor) -> HttpResult {
|
|
// Parse provided files
|
|
let zip_cursor = Cursor::new(file.buff);
|
|
let mut zip = ZipArchive::new(zip_cursor)?;
|
|
|
|
let new_accounts: Vec<Account> = unzip_json(&mut zip, constants::zip_export::ACCOUNTS_FILE)?;
|
|
let new_movements: Vec<Movement> = unzip_json(&mut zip, constants::zip_export::MOVEMENTS_FILE)?;
|
|
let new_inbox_entries: Vec<InboxEntry> =
|
|
unzip_json(&mut zip, constants::zip_export::INBOX_FILE)?;
|
|
let new_files: Vec<files::File> = unzip_json(&mut zip, constants::zip_export::FILES_FILE)?;
|
|
|
|
// Delete all data
|
|
inbox_service::delete_all_user(auth.user_id()).await?;
|
|
accounts_service::delete_all_user(auth.user_id()).await?;
|
|
files_service::delete_all_user(auth.user_id()).await?;
|
|
|
|
// Create the files
|
|
let mut files_mapping = HashMap::new();
|
|
for file in new_files {
|
|
let mut zip_file = zip
|
|
.by_name(&format!(
|
|
"{}{}",
|
|
constants::zip_export::FILES_DIR,
|
|
file.sha512
|
|
))
|
|
.map_err(|e| anyhow::anyhow!("Could not find file with hash {}: {}", file.sha512, e))?;
|
|
let mut file_buff = Vec::with_capacity(zip_file.size() as usize);
|
|
zip_file.read_to_end(&mut file_buff)?;
|
|
let created_file =
|
|
files_service::create_file_with_file_name(auth.user_id(), &file.file_name, &file_buff)
|
|
.await?;
|
|
files_mapping.insert(file.id(), created_file);
|
|
}
|
|
|
|
// Create the accounts
|
|
let mut accounts_mapping = HashMap::new();
|
|
for account in new_accounts {
|
|
let created_account = accounts_service::create(
|
|
auth.user_id(),
|
|
&UpdateAccountQuery {
|
|
name: account.name.to_string(),
|
|
r#type: account.account_type(),
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
if account.default_account {
|
|
accounts_service::set_default(auth.user_id(), created_account.id()).await?;
|
|
}
|
|
|
|
accounts_mapping.insert(account.id(), created_account);
|
|
}
|
|
|
|
// Create the movements
|
|
let mut movements_mapping = HashMap::new();
|
|
for movement in new_movements {
|
|
let created_movement = movements_service::create(&UpdateMovementQuery {
|
|
account_id: accounts_mapping
|
|
.get(&movement.account_id())
|
|
.ok_or_else(|| BackupControllerError::NonexistentAccountId(movement.account_id()))?
|
|
.id(),
|
|
time: movement.time as u64,
|
|
label: movement.label.to_string(),
|
|
file_id: movement
|
|
.file_id()
|
|
.map(|file_id| {
|
|
files_mapping
|
|
.get(&file_id)
|
|
.ok_or(BackupControllerError::NonexistentFileId(file_id))
|
|
})
|
|
.transpose()?
|
|
.map(|f| f.id()),
|
|
amount: movement.amount,
|
|
checked: movement.checked,
|
|
})
|
|
.await?;
|
|
|
|
movements_mapping.insert(movement.id(), created_movement);
|
|
}
|
|
|
|
// Create the inbox entries
|
|
for inbox_entry in new_inbox_entries {
|
|
inbox_service::create(
|
|
auth.user_id(),
|
|
&UpdateInboxEntryQuery {
|
|
file_id: files_mapping
|
|
.get(&inbox_entry.file_id())
|
|
.ok_or(BackupControllerError::NonexistentFileId(
|
|
inbox_entry.file_id(),
|
|
))?
|
|
.id(),
|
|
movement_id: inbox_entry
|
|
.movement_id()
|
|
.map(|mov_id| {
|
|
movements_mapping
|
|
.get(&mov_id)
|
|
.ok_or(BackupControllerError::NonexistentMovementId(mov_id))
|
|
})
|
|
.transpose()?
|
|
.map(|f| f.id()),
|
|
time: inbox_entry.time as u64,
|
|
label: inbox_entry.label,
|
|
amount: inbox_entry.amount,
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Ok(HttpResponse::Accepted().finish())
|
|
}
|
|
|
|
/// Export all movement to XSLX
|
|
pub async fn xslx_export(auth: AuthExtractor) -> HttpResult {
|
|
let mut workbook = Workbook::new();
|
|
|
|
for account in accounts_service::get_list_user(auth.user_id()).await? {
|
|
let worksheet = workbook.add_worksheet();
|
|
worksheet.set_name(account.name.to_string())?;
|
|
|
|
// Configure columns
|
|
let header_format = Format::new()
|
|
.set_bold()
|
|
.set_background_color(Color::Black)
|
|
.set_foreground_color(Color::White);
|
|
|
|
worksheet.set_column_width(0, 15)?;
|
|
worksheet.set_column_width(1, 70)?;
|
|
worksheet.set_column_width(2, 15)?;
|
|
|
|
// Write headers
|
|
worksheet.write_with_format(0, 0, "Date", &header_format)?;
|
|
worksheet.write_with_format(0, 1, "Label", &header_format)?;
|
|
worksheet.write_with_format(0, 2, "Amount", &header_format)?;
|
|
|
|
// Write movements
|
|
let mut movements = movements_service::get_list_account(account.id()).await?;
|
|
movements.sort_by(|a, b| a.time.to_string().cmp(&b.time.to_string()));
|
|
for (idx, movement) in movements.iter().enumerate() {
|
|
worksheet.write(idx as u32 + 1, 0, format_date(movement.time)?)?;
|
|
worksheet.write(idx as u32 + 1, 1, &movement.label)?;
|
|
worksheet.write(idx as u32 + 1, 2, movement.amount)?;
|
|
}
|
|
|
|
// Create table
|
|
let table = Table::new();
|
|
worksheet.add_table(0, 0, movements.len() as u32, 2, &table)?;
|
|
|
|
// Add total
|
|
worksheet.write_with_format(5, 5, "Total", &header_format)?;
|
|
worksheet.write_formula(
|
|
6,
|
|
5,
|
|
Formula::new(format!("SUM(C1,C{})", movements.len())).set_result(
|
|
movements
|
|
.iter()
|
|
.fold(0f32, |acc, m| acc + m.amount)
|
|
.to_string(),
|
|
),
|
|
)?;
|
|
}
|
|
|
|
// Save final Excel Document
|
|
let raw_excel = workbook.save_to_buffer()?;
|
|
|
|
Ok(HttpResponse::Ok()
|
|
.content_type("application/vnd.ms-excel")
|
|
.insert_header((
|
|
"Content-Disposition",
|
|
format!("attachment; filename={}", export_filename("xslx")),
|
|
))
|
|
.body(raw_excel))
|
|
}
|