Can import ZIP
This commit is contained in:
		@@ -6,7 +6,10 @@ use crate::converters::finances_manager_converter::{
 | 
			
		||||
};
 | 
			
		||||
use crate::extractors::auth_extractor::AuthExtractor;
 | 
			
		||||
use crate::extractors::file_extractor::FileExtractor;
 | 
			
		||||
use crate::models::accounts::AccountType;
 | 
			
		||||
use crate::models::accounts::{Account, AccountID, AccountType};
 | 
			
		||||
use crate::models::files;
 | 
			
		||||
use crate::models::files::FileID;
 | 
			
		||||
use crate::models::movements::Movement;
 | 
			
		||||
use crate::services::accounts_service::UpdateAccountQuery;
 | 
			
		||||
use crate::services::movements_service::UpdateMovementQuery;
 | 
			
		||||
use crate::services::{accounts_service, files_service, movements_service};
 | 
			
		||||
@@ -14,10 +17,20 @@ use crate::utils::time_utils::{format_date, time};
 | 
			
		||||
use actix_files::NamedFile;
 | 
			
		||||
use actix_web::{HttpRequest, HttpResponse};
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
use serde::de::DeserializeOwned;
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::fs::File;
 | 
			
		||||
use std::io::Write;
 | 
			
		||||
use zip::ZipWriter;
 | 
			
		||||
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),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Generate export filename
 | 
			
		||||
fn export_filename(ext: &str) -> String {
 | 
			
		||||
@@ -153,3 +166,94 @@ pub async fn zip_export(req: HttpRequest, auth: AuthExtractor) -> HttpResult {
 | 
			
		||||
 | 
			
		||||
    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_files: Vec<files::File> = unzip_json(&mut zip, constants::zip_export::FILES_FILE)?;
 | 
			
		||||
    // TODO : inbox
 | 
			
		||||
 | 
			
		||||
    // Delete all data
 | 
			
		||||
    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
 | 
			
		||||
    for movement in new_movements {
 | 
			
		||||
        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?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Accepted().finish())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,7 @@ pub async fn serve_file(req: HttpRequest, file: &File, download_file: bool) -> H
 | 
			
		||||
 | 
			
		||||
/// Delete an uploaded file
 | 
			
		||||
pub async fn delete(file_extractor: FileIdExtractor) -> HttpResult {
 | 
			
		||||
    match files_service::delete_file_if_unused(file_extractor.as_ref().id()).await {
 | 
			
		||||
    match files_service::delete_file_if_unused(file_extractor.as_ref().id(), true).await {
 | 
			
		||||
        Ok(true) => Ok(HttpResponse::Accepted().finish()),
 | 
			
		||||
        Ok(false) => Ok(HttpResponse::Conflict().finish()),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
use crate::controllers::backup_controller::BackupControllerError;
 | 
			
		||||
use actix_web::http::StatusCode;
 | 
			
		||||
use actix_web::{HttpResponse, ResponseError};
 | 
			
		||||
use std::error::Error;
 | 
			
		||||
@@ -35,6 +36,8 @@ pub enum HttpFailure {
 | 
			
		||||
    ZipError(#[from] ZipError),
 | 
			
		||||
    #[error("a standard I/O manipulation error occurred: {0}")]
 | 
			
		||||
    StdIoError(#[from] std::io::Error),
 | 
			
		||||
    #[error("an error occurred while performing backup/recovery operation: {0}")]
 | 
			
		||||
    BackupControllerError(#[from] BackupControllerError),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ResponseError for HttpFailure {
 | 
			
		||||
 
 | 
			
		||||
@@ -174,6 +174,10 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
                "/api/backup/zip/export",
 | 
			
		||||
                web::get().to(backup_controller::zip_export),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/backup/zip/import",
 | 
			
		||||
                web::post().to(backup_controller::zip_import),
 | 
			
		||||
            )
 | 
			
		||||
            // Static assets
 | 
			
		||||
            .route("/", web::get().to(static_controller::root_index))
 | 
			
		||||
            .route(
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ impl Display for AccountType {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Queryable, Debug, Clone, serde::Serialize)]
 | 
			
		||||
#[derive(Queryable, Debug, Clone, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct Account {
 | 
			
		||||
    id: i32,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,12 @@ use crate::models::users::UserID;
 | 
			
		||||
use crate::schema::*;
 | 
			
		||||
use diesel::prelude::*;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
 | 
			
		||||
#[derive(
 | 
			
		||||
    Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord,
 | 
			
		||||
)]
 | 
			
		||||
pub struct FileID(pub i32);
 | 
			
		||||
 | 
			
		||||
#[derive(Queryable, Debug, serde::Serialize)]
 | 
			
		||||
#[derive(Queryable, Debug, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct File {
 | 
			
		||||
    id: i32,
 | 
			
		||||
    pub time_create: i64,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ use diesel::{Insertable, Queryable, QueryableByName};
 | 
			
		||||
pub struct MovementID(pub i32);
 | 
			
		||||
 | 
			
		||||
/// Single movement information
 | 
			
		||||
#[derive(Queryable, QueryableByName, Debug, Clone, serde::Serialize)]
 | 
			
		||||
#[derive(Queryable, QueryableByName, Debug, Clone, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct Movement {
 | 
			
		||||
    /// The ID of the movement
 | 
			
		||||
    id: i32,
 | 
			
		||||
 
 | 
			
		||||
@@ -97,3 +97,11 @@ pub async fn delete(id: AccountID) -> anyhow::Result<()> {
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Delete all the accounts of a user
 | 
			
		||||
pub async fn delete_all_user(user: UserID) -> anyhow::Result<()> {
 | 
			
		||||
    for account in get_list_user(user).await? {
 | 
			
		||||
        delete(account.id()).await?;
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ use mime_guess::Mime;
 | 
			
		||||
enum FilesServiceError {
 | 
			
		||||
    #[error("UnknownMimeType!")]
 | 
			
		||||
    UnknownMimeType,
 | 
			
		||||
    #[error("Could not delete all the files of the user: a file is still referenced!")]
 | 
			
		||||
    DeleteAllUserFileStillReferenced,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn create_file_with_file_name(
 | 
			
		||||
@@ -84,11 +86,11 @@ pub async fn get_file_content(file: &File) -> anyhow::Result<Vec<u8>> {
 | 
			
		||||
 | 
			
		||||
/// Delete the file if it is not referenced anymore in the database. Returns true
 | 
			
		||||
/// if the file was actually deleted, false otherwise
 | 
			
		||||
pub async fn delete_file_if_unused(id: FileID) -> anyhow::Result<bool> {
 | 
			
		||||
pub async fn delete_file_if_unused(id: FileID, skip_age_check: bool) -> anyhow::Result<bool> {
 | 
			
		||||
    let file = get_file_with_id(id)?;
 | 
			
		||||
 | 
			
		||||
    // Check if the file is old enough
 | 
			
		||||
    if file.time_create as u64 + constants::MIN_FILE_LIFETIME > time() {
 | 
			
		||||
    if file.time_create as u64 + constants::MIN_FILE_LIFETIME > time() && !skip_age_check {
 | 
			
		||||
        return Ok(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -129,10 +131,21 @@ pub async fn run_garbage_collector() -> anyhow::Result<usize> {
 | 
			
		||||
    let mut count_deleted = 0;
 | 
			
		||||
 | 
			
		||||
    for file in get_entire_list().await? {
 | 
			
		||||
        if delete_file_if_unused(file.id()).await? {
 | 
			
		||||
        if delete_file_if_unused(file.id(), false).await? {
 | 
			
		||||
            count_deleted += 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(count_deleted)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Delete all the files of a given user
 | 
			
		||||
pub async fn delete_all_user(user_id: UserID) -> anyhow::Result<()> {
 | 
			
		||||
    for file in get_all_files_user(user_id).await? {
 | 
			
		||||
        if !delete_file_if_unused(file.id(), true).await? {
 | 
			
		||||
            return Err(FilesServiceError::DeleteAllUserFileStillReferenced.into());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user