From 3a86e51a25bdcde9b8fe7405155988826477dfce Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 17:06:12 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20reqwest=E3=81=AB=E3=82=BF?= =?UTF-8?q?=E3=82=A4=E3=83=A0=E3=82=A2=E3=82=A6=E3=83=88=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-presentation/src/middleware/auth.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/sos24-presentation/src/middleware/auth.rs b/crates/sos24-presentation/src/middleware/auth.rs index f079fece..43d1223d 100644 --- a/crates/sos24-presentation/src/middleware/auth.rs +++ b/crates/sos24-presentation/src/middleware/auth.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::time::Duration; use anyhow::Context as _; use axum::{ @@ -10,6 +11,7 @@ use axum::{ use jsonwebtoken::{ decode, decode_header, jwk::JwkSet, Algorithm, DecodingKey, TokenData, Validation, }; +use reqwest::ClientBuilder; use serde::{Deserialize, Serialize}; use sos24_use_case::context::Context; @@ -94,7 +96,12 @@ pub(crate) async fn verify_id_token( ) -> anyhow::Result> { let header = decode_header(token)?; let kid = header.kid.context("No key ID found in JWT header")?; - let jwks: JwkSet = reqwest::get(JWK_URL).await?.json().await?; + + let client = ClientBuilder::new() + .timeout(Duration::from_secs(60)) + .build() + .context("Failed to create HTTP client")?; + let jwks: JwkSet = client.get(JWK_URL).send().await?.json().await?; let jwk = jwks.find(&kid).context("Unknown key ID")?; let key = DecodingKey::from_jwk(jwk)?; From fc6a61e0f6aff8a80669d77706e12e47b4097295 Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 17:30:52 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat:=20=E3=82=A8=E3=82=AF=E3=82=B9?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=88=E6=99=82=E3=81=AB=E6=97=A5=E6=99=82?= =?UTF-8?q?=E3=82=92JST=E3=81=A7=E5=87=BA=E5=8A=9B=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 54 +++++++++++++++++++ Cargo.toml | 1 + crates/sos24-presentation/Cargo.toml | 2 + .../sos24-presentation/src/model/project.rs | 3 +- crates/sos24-presentation/src/model/user.rs | 3 +- crates/sos24-use-case/Cargo.toml | 1 + .../form_answer/export_by_form_id.rs | 7 ++- 7 files changed, 68 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11517044..edbdabfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,28 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -2213,6 +2235,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.14" @@ -2262,6 +2293,26 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + [[package]] name = "phf_shared" version = "0.11.2" @@ -3095,6 +3146,8 @@ dependencies = [ "axum", "axum-extra", "base64 0.22.0", + "chrono", + "chrono-tz", "csv", "dotenvy", "jsonwebtoken", @@ -3119,6 +3172,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", + "chrono-tz", "sos24-domain", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 9c13ba3e..e7fb4675 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ aws-sdk-s3 = { version = "1.20.0", features = ["rt-tokio"] } base64 = "0.22.0" bitflags = "2.4.2" chrono = { version = "0.4.32", features = ["serde"] } +chrono-tz = "0.9.0" csv = "1.3.0" dotenvy = "0.15.7" emojis = "0.6.1" diff --git a/crates/sos24-presentation/Cargo.toml b/crates/sos24-presentation/Cargo.toml index b7b853ed..cd3e66a1 100644 --- a/crates/sos24-presentation/Cargo.toml +++ b/crates/sos24-presentation/Cargo.toml @@ -11,6 +11,8 @@ anyhow.workspace = true axum.workspace = true axum-extra.workspace = true base64.workspace = true +chrono.workspace = true +chrono-tz.workspace = true csv.workspace = true dotenvy.workspace = true jsonwebtoken.workspace = true diff --git a/crates/sos24-presentation/src/model/project.rs b/crates/sos24-presentation/src/model/project.rs index 13f27c25..db533276 100644 --- a/crates/sos24-presentation/src/model/project.rs +++ b/crates/sos24-presentation/src/model/project.rs @@ -1,3 +1,4 @@ +use chrono_tz::Asia::Tokyo; use serde::{Deserialize, Serialize}; use sos24_use_case::dto::project::ProjectAttributeDto; @@ -183,7 +184,7 @@ impl From<(ProjectDto, UserDto, Option)> for ProjectToBeExported { .collect::>() .join(";"), remark: project.remarks, - created_at: project.created_at.to_rfc3339(), + created_at: project.created_at.with_timezone(&Tokyo).to_rfc3339(), } } } diff --git a/crates/sos24-presentation/src/model/user.rs b/crates/sos24-presentation/src/model/user.rs index c6efb188..30220740 100644 --- a/crates/sos24-presentation/src/model/user.rs +++ b/crates/sos24-presentation/src/model/user.rs @@ -1,3 +1,4 @@ +use chrono_tz::Asia::Tokyo; use serde::{Deserialize, Serialize}; use sos24_use_case::dto::user::{CreateUserDto, UpdateUserDto, UserDto, UserRoleDto}; @@ -131,7 +132,7 @@ impl From for UserTobeExported { kana_name: user.kana_name, email: user.email, role: user.role.to_string(), - created_at: user.created_at.to_rfc3339(), + created_at: user.created_at.with_timezone(&Tokyo).to_rfc3339(), } } } diff --git a/crates/sos24-use-case/Cargo.toml b/crates/sos24-use-case/Cargo.toml index 333013a3..1f1a4428 100644 --- a/crates/sos24-use-case/Cargo.toml +++ b/crates/sos24-use-case/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true sos24-domain = { path = "../sos24-domain" } anyhow.workspace = true chrono.workspace = true +chrono-tz.workspace = true thiserror.workspace = true tokio.workspace = true uuid.workspace = true diff --git a/crates/sos24-use-case/src/interactor/form_answer/export_by_form_id.rs b/crates/sos24-use-case/src/interactor/form_answer/export_by_form_id.rs index 24bf04c9..d3b3ad60 100644 --- a/crates/sos24-use-case/src/interactor/form_answer/export_by_form_id.rs +++ b/crates/sos24-use-case/src/interactor/form_answer/export_by_form_id.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use chrono_tz::Asia::Tokyo; + use sos24_domain::entity::form::FormId; use sos24_domain::entity::form_answer::{FormAnswerItem, FormAnswerItemKind}; use sos24_domain::repository::form::FormRepository; @@ -70,7 +72,10 @@ impl FormAnswerUseCase { project_title: project.title.value().to_string(), project_group_name: project.group_name.value().to_string(), form_answer_item_values, - created_at: raw_form_answer.created_at.to_rfc3339(), + created_at: raw_form_answer + .created_at + .with_timezone(&Tokyo) + .to_rfc3339(), }); } From 4158f2e1540a51a06bdf8c6fdd0013f38d117358 Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 17:42:17 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=E6=96=87=E5=AD=97=E6=95=B0?= =?UTF-8?q?=E3=82=92=E6=9B=B8=E8=A8=98=E7=B4=A0=E5=8D=98=E4=BD=8D=E3=81=A7?= =?UTF-8?q?=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-domain/src/service/verify_form_answer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/sos24-domain/src/service/verify_form_answer.rs b/crates/sos24-domain/src/service/verify_form_answer.rs index 97b8c8f8..68c39a63 100644 --- a/crates/sos24-domain/src/service/verify_form_answer.rs +++ b/crates/sos24-domain/src/service/verify_form_answer.rs @@ -1,4 +1,5 @@ use thiserror::Error; +use unicode_segmentation::UnicodeSegmentation; use crate::entity::{ form::{ @@ -95,17 +96,18 @@ fn verify_item_string( answer_string: FormAnswerItemString, ) -> Result<(), VerifyFormAnswerError> { let value = answer_string.value(); + let value_len = value.graphemes(true).count(); if let Some(min_length) = form_string.min_length().clone() { let min_length = min_length.value(); - if value.chars().count() < min_length as usize { + if value_len < min_length as usize { return Err(VerifyFormAnswerError::TooShortString(item_id, min_length)); } } if let Some(max_length) = form_string.max_length().clone() { let max_length = max_length.value(); - if value.chars().count() > max_length as usize { + if value_len > max_length as usize { return Err(VerifyFormAnswerError::TooLongString(item_id, max_length)); } } From 8f811399804583c4f3a77d0139a2a91548b1b437 Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 20:38:17 +0900 Subject: [PATCH 04/18] =?UTF-8?q?refactor:=20=E3=82=A2=E3=83=BC=E3=82=AB?= =?UTF-8?q?=E3=82=A4=E3=83=96=E3=81=AEDTO=E3=81=A7=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E5=90=8D=E3=82=92=E8=BF=94=E3=81=99=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-presentation/src/route/file.rs | 6 +++--- crates/sos24-use-case/src/dto/file.rs | 2 +- .../sos24-use-case/src/interactor/file/export_by_owner.rs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/sos24-presentation/src/route/file.rs b/crates/sos24-presentation/src/route/file.rs index 4b780b3b..497d5360 100644 --- a/crates/sos24-presentation/src/route/file.rs +++ b/crates/sos24-presentation/src/route/file.rs @@ -153,15 +153,15 @@ pub async fn handle_export( AppError::from(err) })?; - let filename = format!("{}_ファイル一覧.zip", archive.owner_project_title); - let encoded_filename = percent_encoding::percent_encode(filename.as_bytes(), NON_ALPHANUMERIC); + let encoded_filename = + percent_encoding::percent_encode(archive.filename.as_bytes(), NON_ALPHANUMERIC); Response::builder() .header("Content-Type", "application/zip") .header( "Content-Disposition", format!( "attachment; filename=\"{}\" filename*=UTF-8''{}", - filename, encoded_filename + archive.filename, encoded_filename ), ) .body(AsyncReadBody::new(archive.body)) diff --git a/crates/sos24-use-case/src/dto/file.rs b/crates/sos24-use-case/src/dto/file.rs index cb8e2461..5e2be449 100644 --- a/crates/sos24-use-case/src/dto/file.rs +++ b/crates/sos24-use-case/src/dto/file.rs @@ -87,6 +87,6 @@ impl FromEntity for FileDto { } pub struct ArchiveToBeExportedDto { - pub owner_project_title: String, + pub filename: String, pub body: R, } diff --git a/crates/sos24-use-case/src/interactor/file/export_by_owner.rs b/crates/sos24-use-case/src/interactor/file/export_by_owner.rs index f7f22177..5372960b 100644 --- a/crates/sos24-use-case/src/interactor/file/export_by_owner.rs +++ b/crates/sos24-use-case/src/interactor/file/export_by_owner.rs @@ -32,7 +32,6 @@ impl FileUseCase { .await? .ok_or(FileUseCaseError::ProjectNotFound(owner_project.clone()))?; ensure!(raw_project.value.is_visible_to(&actor)); - let project = raw_project.value.destruct(); let file_list = self .repositories @@ -45,8 +44,10 @@ impl FileUseCase { .file_object_repository() .create_archive(bucket, file_list) .await?; + + let project = raw_project.value.destruct(); Ok(ArchiveToBeExportedDto { - owner_project_title: project.title.value(), + filename: format!("{}_ファイル一覧.zip", project.title.value()), body: archive, }) } From 416a1f4f12b4d19117cc0e93be3f874861fffebf Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 20:47:14 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=E7=94=B3=E8=AB=8B=E3=81=AB?= =?UTF-8?q?=E7=B4=90=E3=81=A5=E3=81=8F=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=82=92=E3=82=A8=E3=82=AF=E3=82=B9=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-domain/src/entity/form_answer.rs | 11 +++ .../src/error/convert_error.rs | 9 +- crates/sos24-presentation/src/model/file.rs | 3 +- crates/sos24-presentation/src/route/file.rs | 90 +++++++++++-------- crates/sos24-use-case/src/interactor/file.rs | 14 ++- .../src/interactor/file/export_by_form_id.rs | 71 +++++++++++++++ 6 files changed, 159 insertions(+), 39 deletions(-) create mode 100644 crates/sos24-use-case/src/interactor/file/export_by_form_id.rs diff --git a/crates/sos24-domain/src/entity/form_answer.rs b/crates/sos24-domain/src/entity/form_answer.rs index 5cf14310..4e6916c7 100644 --- a/crates/sos24-domain/src/entity/form_answer.rs +++ b/crates/sos24-domain/src/entity/form_answer.rs @@ -83,6 +83,17 @@ impl FormAnswer { self.items = items; Ok(()) } + + pub fn list_files(&self) -> Vec { + self.items + .iter() + .filter_map(|item| match &item.kind { + FormAnswerItemKind::File(file) => Some(file.clone().value()), + _ => None, + }) + .flatten() + .collect() + } } impl_value_object!(FormAnswerId(uuid::Uuid)); diff --git a/crates/sos24-presentation/src/error/convert_error.rs b/crates/sos24-presentation/src/error/convert_error.rs index 1f263dc5..7d33a663 100644 --- a/crates/sos24-presentation/src/error/convert_error.rs +++ b/crates/sos24-presentation/src/error/convert_error.rs @@ -195,6 +195,11 @@ impl From for AppError { "file/owner-not-found".to_string(), message, ), + FileUseCaseError::FormNotFound(_) => AppError::new( + StatusCode::NOT_FOUND, + "file/form-not-found".to_string(), + message, + ), FileUseCaseError::FileDataRepositoryError(e) => e.into(), FileUseCaseError::FileIdError(e) => e.into(), FileUseCaseError::PermissionDeniedError(e) => e.into(), @@ -202,8 +207,10 @@ impl From for AppError { FileUseCaseError::FileObjectRepositoryError(e) => e.into(), FileUseCaseError::ContextError(e) => e.into(), FileUseCaseError::ProjectRepositoryError(e) => e.into(), - FileUseCaseError::ProjectIdError(e) => e.into(), + FileUseCaseError::FormRepositoryError(e) => e.into(), + FileUseCaseError::FormIdError(e) => e.into(), + FileUseCaseError::FormAnswerRepositoryError(e) => e.into(), } } } diff --git a/crates/sos24-presentation/src/model/file.rs b/crates/sos24-presentation/src/model/file.rs index ff9b1693..7dbcfad5 100644 --- a/crates/sos24-presentation/src/model/file.rs +++ b/crates/sos24-presentation/src/model/file.rs @@ -69,5 +69,6 @@ pub struct CreatedFile { #[derive(Debug, Deserialize)] pub struct ExportFileQuery { - pub owner_project: Option, + pub project_id: Option, + pub form_id: Option, } diff --git a/crates/sos24-presentation/src/route/file.rs b/crates/sos24-presentation/src/route/file.rs index 497d5360..df5c3734 100644 --- a/crates/sos24-presentation/src/route/file.rs +++ b/crates/sos24-presentation/src/route/file.rs @@ -1,20 +1,20 @@ use std::sync::Arc; +use axum::{Extension, Json}; use axum::extract::{Multipart, Path, Query, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; -use axum::{Extension, Json}; use axum_extra::body::AsyncReadBody; use percent_encoding::NON_ALPHANUMERIC; use sos24_use_case::{context::Context, dto::file::CreateFileDto}; -use crate::model::file::{CreatedFile, ExportFileQuery}; use crate::{ error::AppError, model::file::{CreateFileQuery, File, FileInfo, Visibility}, module::Modules, }; +use crate::model::file::{CreatedFile, ExportFileQuery}; pub async fn handle_get( State(modules): State>, @@ -136,43 +136,61 @@ pub async fn handle_export( Query(query): Query, Extension(ctx): Extension, ) -> Result { - let Some(owner_project) = query.owner_project else { - return Err(AppError::new( + fn archive_to_body( + filename: String, + body: AsyncReadBody, + ) -> Result { + let encoded_filename = + percent_encoding::percent_encode(filename.as_bytes(), NON_ALPHANUMERIC); + Response::builder() + .header("Content-Type", "application/zip") + .header( + "Content-Disposition", + format!( + "attachment; filename=\"{}\" filename*=UTF-8''{}", + filename, encoded_filename + ), + ) + .body(body) + .map_err(|err| { + tracing::error!("Failed to create response: {err:?}"); + AppError::new( + StatusCode::INTERNAL_SERVER_ERROR, + "file/failed-to-create-response".to_string(), + format!("{err:?}"), + ) + }) + } + + match (query.project_id, query.form_id) { + (Some(project_id), None) => { + let archive = modules + .file_use_case() + .export_by_owner_project(&ctx, modules.config().s3_bucket_name.clone(), project_id) + .await + .map_err(|err| { + tracing::error!("Failed to export file: {err:?}"); + AppError::from(err) + })?; + archive_to_body(archive.filename, AsyncReadBody::new(archive.body)) + } + (None, Some(form_id)) => { + let archive = modules + .file_use_case() + .export_by_form_id(&ctx, modules.config().s3_bucket_name.clone(), form_id) + .await + .map_err(|err| { + tracing::error!("Failed to export file: {err:?}"); + AppError::from(err) + })?; + archive_to_body(archive.filename, AsyncReadBody::new(archive.body)) + } + _ => Err(AppError::new( StatusCode::BAD_REQUEST, "file/invalid-query".to_string(), "Invalid query".to_string(), - )); - }; - - let archive = modules - .file_use_case() - .export_by_owner_project(&ctx, modules.config().s3_bucket_name.clone(), owner_project) - .await - .map_err(|err| { - tracing::error!("Failed to export file: {err:?}"); - AppError::from(err) - })?; - - let encoded_filename = - percent_encoding::percent_encode(archive.filename.as_bytes(), NON_ALPHANUMERIC); - Response::builder() - .header("Content-Type", "application/zip") - .header( - "Content-Disposition", - format!( - "attachment; filename=\"{}\" filename*=UTF-8''{}", - archive.filename, encoded_filename - ), - ) - .body(AsyncReadBody::new(archive.body)) - .map_err(|err| { - tracing::error!("Failed to create response: {err:?}"); - AppError::new( - StatusCode::INTERNAL_SERVER_ERROR, - "file/failed-to-create-response".to_string(), - format!("{err:?}"), - ) - }) + )), + } } pub async fn handle_get_id( diff --git a/crates/sos24-use-case/src/interactor/file.rs b/crates/sos24-use-case/src/interactor/file.rs index fa59f3e1..0eb26b47 100644 --- a/crates/sos24-use-case/src/interactor/file.rs +++ b/crates/sos24-use-case/src/interactor/file.rs @@ -3,9 +3,12 @@ use std::sync::Arc; use thiserror::Error; use sos24_domain::entity::file_data::{FileId, FileIdError}; +use sos24_domain::entity::form::{FormId, FormIdError}; use sos24_domain::entity::project::{ProjectId, ProjectIdError}; use sos24_domain::repository::file_data::FileDataRepositoryError; use sos24_domain::repository::file_object::FileObjectRepositoryError; +use sos24_domain::repository::form::FormRepositoryError; +use sos24_domain::repository::form_answer::FormAnswerRepositoryError; use sos24_domain::repository::project::ProjectRepositoryError; use sos24_domain::{entity::permission::PermissionDeniedError, repository::Repositories}; @@ -13,7 +16,8 @@ use crate::context::ContextError; pub mod create; pub mod delete_by_id; -mod export_by_owner; +pub mod export_by_form_id; +pub mod export_by_owner; pub mod find_by_id; pub mod list; @@ -25,7 +29,15 @@ pub enum FileUseCaseError { ProjectNotFound(ProjectId), #[error("Owner not found")] OwnerNotFound, + #[error("Form not found: {0:?}")] + FormNotFound(FormId), + #[error(transparent)] + FormRepositoryError(#[from] FormRepositoryError), + #[error(transparent)] + FormIdError(#[from] FormIdError), + #[error(transparent)] + FormAnswerRepositoryError(#[from] FormAnswerRepositoryError), #[error(transparent)] FileDataRepositoryError(#[from] FileDataRepositoryError), #[error(transparent)] diff --git a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs new file mode 100644 index 00000000..021723ce --- /dev/null +++ b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs @@ -0,0 +1,71 @@ +use std::sync::Arc; + +use tokio::io::AsyncRead; + +use sos24_domain::entity::form::FormId; +use sos24_domain::entity::permission::Permissions; +use sos24_domain::repository::file_object::FileObjectRepository; +use sos24_domain::repository::form::FormRepository; +use sos24_domain::repository::form_answer::FormAnswerRepository; +use sos24_domain::repository::Repositories; +use sos24_domain::{ensure, repository::file_data::FileDataRepository}; + +use crate::context::Context; +use crate::dto::file::ArchiveToBeExportedDto; + +use super::{FileUseCase, FileUseCaseError}; + +impl FileUseCase { + pub async fn export_by_form_id( + &self, + ctx: &Context, + bucket: String, + form_id: String, + ) -> Result, FileUseCaseError> { + let actor = ctx.actor(Arc::clone(&self.repositories)).await?; + ensure!(actor.has_permission(Permissions::READ_FILE_ALL)); + + let form_id = FormId::try_from(form_id)?; + let form = self + .repositories + .form_repository() + .find_by_id(form_id.clone()) + .await? + .ok_or(FileUseCaseError::FormNotFound(form_id.clone()))?; + + let form_answer_list = self + .repositories + .form_answer_repository() + .find_by_form_id(form_id.clone()) + .await?; + + let mut file_list = Vec::new(); + for form_answer in form_answer_list { + let file_ids = form_answer.value.list_files(); + for file_id in file_ids { + let file = self + .repositories + .file_data_repository() + .find_by_id(file_id.clone()) + .await? + .ok_or(FileUseCaseError::NotFound(file_id))?; + file_list.push(file); + } + } + + let archive = self + .repositories + .file_object_repository() + .create_archive(bucket, file_list) + .await?; + + let form = form.value.destruct(); + Ok(ArchiveToBeExportedDto { + filename: format!("{}_ファイル一覧.zip", form.title.value()), + body: archive, + }) + } +} + +#[cfg(test)] +mod tests {} From 5976e383bedc062f4c56949463816f12bc997b44 Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 20:48:56 +0900 Subject: [PATCH 06/18] =?UTF-8?q?docs:=20=E3=82=B9=E3=82=AD=E3=83=BC?= =?UTF-8?q?=E3=83=9E=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schema/file.yml | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/schema/file.yml b/schema/file.yml index 58bbfc6f..72685fb0 100644 --- a/schema/file.yml +++ b/schema/file.yml @@ -160,6 +160,48 @@ paths: schema: $ref: "./error.yml#/schemas/Error" + /files/export: + get: + operationId: getFilesExport + summary: ファイル一覧のエクスポート + tags: + - files + - committee + parameters: + - name: form_id + in: query + schema: { type: string } + required: false + - name: project_id + in: query + schema: { type: string } + required: false + responses: + 200: + description: OK + content: + text/csv: + schema: + type: string + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: "./error.yml#/schemas/Error" + 403: + description: Forbidden + content: + application/json: + schema: + $ref: "./error.yml#/schemas/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "./error.yml#/schemas/Error" + schemas: CreateFile: type: object From d4d0da0c41a13ee0e5bf205e9b9fa8864771aafe Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 21:19:43 +0900 Subject: [PATCH 07/18] =?UTF-8?q?refactor:=20=E3=82=A2=E3=83=BC=E3=82=AB?= =?UTF-8?q?=E3=82=A4=E3=83=96=E4=BD=9C=E6=88=90=E6=99=82=E3=81=AB=E5=8F=97?= =?UTF-8?q?=E3=81=91=E5=8F=96=E3=82=8B=E5=BC=95=E6=95=B0=E3=81=AE=E5=9E=8B?= =?UTF-8?q?=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-domain/src/entity/file_object.rs | 35 +++++++++++++++++++ .../src/repository/file_object.rs | 8 ++--- .../src/s3/file_object.rs | 17 +++++---- crates/sos24-presentation/src/route/file.rs | 4 +-- .../src/interactor/file/export_by_form_id.rs | 8 ++++- .../src/interactor/file/export_by_owner.rs | 9 ++++- 6 files changed, 64 insertions(+), 17 deletions(-) diff --git a/crates/sos24-domain/src/entity/file_object.rs b/crates/sos24-domain/src/entity/file_object.rs index f5276fb8..7036983e 100644 --- a/crates/sos24-domain/src/entity/file_object.rs +++ b/crates/sos24-domain/src/entity/file_object.rs @@ -88,6 +88,41 @@ impl FileObjectKey { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ArchiveEntry { + key: FileObjectKey, + filename: FileName, + updated_at: chrono::DateTime, +} + +impl ArchiveEntry { + pub fn new( + key: FileObjectKey, + filename: FileName, + updated_at: chrono::DateTime, + ) -> Self { + Self { + key, + filename, + updated_at, + } + } + + pub fn destruct(self) -> DestructedArchiveEntry { + DestructedArchiveEntry { + key: self.key, + filename: self.filename, + updated_at: self.updated_at, + } + } +} + +pub struct DestructedArchiveEntry { + pub key: FileObjectKey, + pub filename: FileName, + pub updated_at: chrono::DateTime, +} + #[cfg(test)] mod test { use crate::entity::file_object::generate_content_disposition; diff --git a/crates/sos24-domain/src/repository/file_object.rs b/crates/sos24-domain/src/repository/file_object.rs index 98659052..620f2def 100644 --- a/crates/sos24-domain/src/repository/file_object.rs +++ b/crates/sos24-domain/src/repository/file_object.rs @@ -2,9 +2,9 @@ use mockall::automock; use thiserror::Error; use tokio::io::DuplexStream; -use crate::entity::common::date::WithDate; -use crate::entity::file_data::FileData; -use crate::entity::file_object::{ContentDisposition, FileObject, FileObjectKey, FileSignedUrl}; +use crate::entity::file_object::{ + ArchiveEntry, ContentDisposition, FileObject, FileObjectKey, FileSignedUrl, +}; #[derive(Debug, Error)] pub enum FileObjectRepositoryError { @@ -30,6 +30,6 @@ pub trait FileObjectRepository: Send + Sync + 'static { async fn create_archive( &self, bucket: String, - files: Vec>, + entry_list: Vec, ) -> Result; } diff --git a/crates/sos24-infrastructure/src/s3/file_object.rs b/crates/sos24-infrastructure/src/s3/file_object.rs index 3fad546a..fb6a6abb 100644 --- a/crates/sos24-infrastructure/src/s3/file_object.rs +++ b/crates/sos24-infrastructure/src/s3/file_object.rs @@ -7,8 +7,7 @@ use aws_sdk_s3::{presigning::PresigningConfig, primitives::SdkBody}; use tokio::io::DuplexStream; use tokio_util::compat::FuturesAsyncWriteCompatExt; -use sos24_domain::entity::common::date::WithDate; -use sos24_domain::entity::file_data::FileData; +use sos24_domain::entity::file_object::ArchiveEntry; use sos24_domain::{ entity::file_object::{ContentDisposition, FileObject, FileObjectKey, FileSignedUrl}, repository::file_object::{FileObjectRepository, FileObjectRepositoryError}, @@ -77,28 +76,28 @@ impl FileObjectRepository for S3FileObjectRepository { async fn create_archive( &self, bucket: String, - files: Vec>, + entry_list: Vec, ) -> Result { tracing::info!("ファイルのアーカイブを作成します"); let (writer, reader) = tokio::io::duplex(65535); let mut zip_writer = ZipFileWriter::with_tokio(writer); - for file in files { - let file_key = file.value.url().clone().value(); + for entry in entry_list { + let entry = entry.destruct(); let file_data = self .s3 .get_object() .bucket(&bucket) - .key(file_key) + .key(entry.key.value()) .send() .await .context("Failed to get object")?; let mut file_data_stream = file_data.body.into_async_read(); - let file_name = file.value.filename().clone().value(); - let zip_entry = ZipEntryBuilder::new(file_name.into(), Compression::Deflate) - .last_modification_date(file.updated_at.into()); + let zip_entry = + ZipEntryBuilder::new(entry.filename.value().into(), Compression::Deflate) + .last_modification_date(entry.updated_at.into()); let mut zip_entry_stream = zip_writer .write_entry_stream(zip_entry) .await diff --git a/crates/sos24-presentation/src/route/file.rs b/crates/sos24-presentation/src/route/file.rs index df5c3734..8ff402fa 100644 --- a/crates/sos24-presentation/src/route/file.rs +++ b/crates/sos24-presentation/src/route/file.rs @@ -1,20 +1,20 @@ use std::sync::Arc; -use axum::{Extension, Json}; use axum::extract::{Multipart, Path, Query, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; +use axum::{Extension, Json}; use axum_extra::body::AsyncReadBody; use percent_encoding::NON_ALPHANUMERIC; use sos24_use_case::{context::Context, dto::file::CreateFileDto}; +use crate::model::file::{CreatedFile, ExportFileQuery}; use crate::{ error::AppError, model::file::{CreateFileQuery, File, FileInfo, Visibility}, module::Modules, }; -use crate::model::file::{CreatedFile, ExportFileQuery}; pub async fn handle_get( State(modules): State>, diff --git a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs index 021723ce..fd085c8d 100644 --- a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs +++ b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use tokio::io::AsyncRead; +use sos24_domain::entity::file_object::ArchiveEntry; use sos24_domain::entity::form::FormId; use sos24_domain::entity::permission::Permissions; use sos24_domain::repository::file_object::FileObjectRepository; @@ -49,7 +50,12 @@ impl FileUseCase { .find_by_id(file_id.clone()) .await? .ok_or(FileUseCaseError::NotFound(file_id))?; - file_list.push(file); + let file_data = file.value.destruct(); + file_list.push(ArchiveEntry::new( + file_data.url, + file_data.name, + file.updated_at, + )); } } diff --git a/crates/sos24-use-case/src/interactor/file/export_by_owner.rs b/crates/sos24-use-case/src/interactor/file/export_by_owner.rs index 5372960b..a9282ae1 100644 --- a/crates/sos24-use-case/src/interactor/file/export_by_owner.rs +++ b/crates/sos24-use-case/src/interactor/file/export_by_owner.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use tokio::io::AsyncRead; +use sos24_domain::entity::file_object::ArchiveEntry; use sos24_domain::entity::permission::Permissions; use sos24_domain::entity::project::ProjectId; use sos24_domain::repository::file_object::FileObjectRepository; @@ -37,7 +38,13 @@ impl FileUseCase { .repositories .file_data_repository() .find_by_owner_project(owner_project) - .await?; + .await? + .into_iter() + .map(|file| { + let file_data = file.value.destruct(); + ArchiveEntry::new(file_data.url, file_data.name, file.updated_at) + }) + .collect(); let archive = self .repositories From da135b1769655015b84aa6210c3c620b84eb2081 Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 05:38:01 +0900 Subject: [PATCH 08/18] =?UTF-8?q?fix:=2064KiB=E3=82=88=E3=82=8A=E5=A4=A7?= =?UTF-8?q?=E3=81=8D=E3=81=84=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=A7?= =?UTF-8?q?=E3=82=A2=E3=83=BC=E3=82=AB=E3=82=A4=E3=83=96=E3=81=AE=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=81=8C=E7=B5=82=E4=BA=86=E3=81=97=E3=81=AA=E3=81=84?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sos24-domain/src/repository/file_object.rs | 5 +++-- .../sos24-infrastructure/src/s3/file_object.rs | 12 ++++++++---- .../src/interactor/file/export_by_owner.rs | 18 ++++++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/crates/sos24-domain/src/repository/file_object.rs b/crates/sos24-domain/src/repository/file_object.rs index 620f2def..f79b99fd 100644 --- a/crates/sos24-domain/src/repository/file_object.rs +++ b/crates/sos24-domain/src/repository/file_object.rs @@ -27,9 +27,10 @@ pub trait FileObjectRepository: Send + Sync + 'static { content_disposition: Option, ) -> Result; // TODO: 返り値をラッピングしておくと内部仕様が露出しなくてよい - async fn create_archive( + fn create_archive( &self, bucket: String, entry_list: Vec, - ) -> Result; + writer: DuplexStream, + ) -> impl std::future::Future> + Send; } diff --git a/crates/sos24-infrastructure/src/s3/file_object.rs b/crates/sos24-infrastructure/src/s3/file_object.rs index fb6a6abb..2ea18efa 100644 --- a/crates/sos24-infrastructure/src/s3/file_object.rs +++ b/crates/sos24-infrastructure/src/s3/file_object.rs @@ -77,14 +77,16 @@ impl FileObjectRepository for S3FileObjectRepository { &self, bucket: String, entry_list: Vec, - ) -> Result { + writer: DuplexStream, + ) -> Result<(), FileObjectRepositoryError> { tracing::info!("ファイルのアーカイブを作成します"); - let (writer, reader) = tokio::io::duplex(65535); let mut zip_writer = ZipFileWriter::with_tokio(writer); for entry in entry_list { let entry = entry.destruct(); + tracing::info!("ファイルをアーカイブに追加します: {:?}", entry.key); + let file_data = self .s3 .get_object() @@ -104,7 +106,7 @@ impl FileObjectRepository for S3FileObjectRepository { .context("Failed to write entry")? .compat_write(); - tokio::io::copy(&mut file_data_stream, &mut zip_entry_stream) + tokio::io::copy_buf(&mut file_data_stream, &mut zip_entry_stream) .await .context("Failed to copy")?; @@ -113,11 +115,13 @@ impl FileObjectRepository for S3FileObjectRepository { .close() .await .context("Failed to close")?; + + tracing::info!("ファイルをアーカイブに追加しました"); } zip_writer.close().await.context("Failed to close")?; tracing::info!("ファイルのアーカイブを作成しました"); - Ok(reader) + Ok(()) } } diff --git a/crates/sos24-use-case/src/interactor/file/export_by_owner.rs b/crates/sos24-use-case/src/interactor/file/export_by_owner.rs index a9282ae1..7c6fe211 100644 --- a/crates/sos24-use-case/src/interactor/file/export_by_owner.rs +++ b/crates/sos24-use-case/src/interactor/file/export_by_owner.rs @@ -46,16 +46,22 @@ impl FileUseCase { }) .collect(); - let archive = self - .repositories - .file_object_repository() - .create_archive(bucket, file_list) - .await?; + let (writer, reader) = tokio::io::duplex(65535); + let repositories = Arc::clone(&self.repositories); + tokio::spawn(async move { + if let Err(err) = repositories + .file_object_repository() + .create_archive(bucket, file_list, writer) + .await + { + tracing::error!("Failed to create archive: {err:?}"); + } + }); let project = raw_project.value.destruct(); Ok(ArchiveToBeExportedDto { filename: format!("{}_ファイル一覧.zip", project.title.value()), - body: archive, + body: reader, }) } } From c6557d73c0f956222828ddd3861f31cafb1f8bf8 Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 05:38:51 +0900 Subject: [PATCH 09/18] =?UTF-8?q?refactor:=20axum-extra=E3=81=AE=E3=81=8B?= =?UTF-8?q?=E3=82=8F=E3=82=8A=E3=81=ABtokio-util=E3=82=92=E3=81=A4?= =?UTF-8?q?=E3=81=8B=E3=81=86=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 27 ++------------------- Cargo.toml | 3 +-- crates/sos24-presentation/Cargo.toml | 2 +- crates/sos24-presentation/src/route/file.rs | 18 ++++++++------ 4 files changed, 15 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11517044..d563a12d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -446,30 +446,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-extra" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" -dependencies = [ - "axum", - "axum-core", - "bytes", - "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "http-body-util", - "mime", - "pin-project-lite", - "serde", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "axum-macros" version = "0.4.1" @@ -3093,7 +3069,6 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", - "axum-extra", "base64 0.22.0", "csv", "dotenvy", @@ -3107,6 +3082,7 @@ dependencies = [ "sos24-use-case", "thiserror", "tokio", + "tokio-util", "tower", "tower-http", "tracing", @@ -3122,6 +3098,7 @@ dependencies = [ "sos24-domain", "thiserror", "tokio", + "tracing", "url", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index 9c13ba3e..23929653 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ edition = "2021" anyhow = "1.0.79" async_zip = { version = "0.0.17", features = ["chrono", "deflate", "tokio"] } axum = { version = "0.7.4", features = ["macros", "multipart", "query"] } -axum-extra = { version = "0.9.3", features = ["async-read-body"] } aws-sdk-s3 = { version = "1.20.0", features = ["rt-tokio"] } base64 = "0.22.0" bitflags = "2.4.2" @@ -43,7 +42,7 @@ sqlx = { version = "0.7.3", features = [ ] } thiserror = "1.0.56" tokio = { version = "1.35.1", features = ["full"] } -tokio-util = { version = "0.7.10", features = ["compat"] } +tokio-util = { version = "0.7.10", features = ["compat", "io"] } tower = { version = "0.4.13", features = ["util"] } tower-http = { version = "0.5.1", features = ["cors", "trace"] } tracing = "0.1.40" diff --git a/crates/sos24-presentation/Cargo.toml b/crates/sos24-presentation/Cargo.toml index b7b853ed..dce0745d 100644 --- a/crates/sos24-presentation/Cargo.toml +++ b/crates/sos24-presentation/Cargo.toml @@ -9,7 +9,6 @@ sos24-domain = { path = "../sos24-domain" } sos24-use-case = { path = "../sos24-use-case" } anyhow.workspace = true axum.workspace = true -axum-extra.workspace = true base64.workspace = true csv.workspace = true dotenvy.workspace = true @@ -20,6 +19,7 @@ serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio.workspace = true +tokio-util.workspace = true tower.workspace = true tower-http.workspace = true tracing.workspace = true diff --git a/crates/sos24-presentation/src/route/file.rs b/crates/sos24-presentation/src/route/file.rs index 8ff402fa..8f119fc0 100644 --- a/crates/sos24-presentation/src/route/file.rs +++ b/crates/sos24-presentation/src/route/file.rs @@ -1,11 +1,12 @@ use std::sync::Arc; +use axum::body::Body; use axum::extract::{Multipart, Path, Query, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::{Extension, Json}; -use axum_extra::body::AsyncReadBody; use percent_encoding::NON_ALPHANUMERIC; +use tokio_util::io::ReaderStream; use sos24_use_case::{context::Context, dto::file::CreateFileDto}; @@ -136,10 +137,7 @@ pub async fn handle_export( Query(query): Query, Extension(ctx): Extension, ) -> Result { - fn archive_to_body( - filename: String, - body: AsyncReadBody, - ) -> Result { + fn archive_to_body(filename: String, body: Body) -> Result { let encoded_filename = percent_encoding::percent_encode(filename.as_bytes(), NON_ALPHANUMERIC); Response::builder() @@ -172,7 +170,10 @@ pub async fn handle_export( tracing::error!("Failed to export file: {err:?}"); AppError::from(err) })?; - archive_to_body(archive.filename, AsyncReadBody::new(archive.body)) + archive_to_body( + archive.filename, + Body::from_stream(ReaderStream::new(archive.body)), + ) } (None, Some(form_id)) => { let archive = modules @@ -183,7 +184,10 @@ pub async fn handle_export( tracing::error!("Failed to export file: {err:?}"); AppError::from(err) })?; - archive_to_body(archive.filename, AsyncReadBody::new(archive.body)) + archive_to_body( + archive.filename, + Body::from_stream(ReaderStream::new(archive.body)), + ) } _ => Err(AppError::new( StatusCode::BAD_REQUEST, From dd40837418ec4d585ac64e00b103eb28b46ebdc5 Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 05:39:51 +0900 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=A8=E3=82=AF=E3=82=B9=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?=E6=99=82=E3=81=AB=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E5=90=8D?= =?UTF-8?q?=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-domain/src/entity/form.rs | 4 + crates/sos24-domain/src/entity/form_answer.rs | 7 +- .../src/error/convert_error.rs | 5 ++ crates/sos24-use-case/Cargo.toml | 1 + crates/sos24-use-case/src/interactor/file.rs | 4 +- .../src/interactor/file/export_by_form_id.rs | 74 ++++++++++++++----- 6 files changed, 71 insertions(+), 24 deletions(-) diff --git a/crates/sos24-domain/src/entity/form.rs b/crates/sos24-domain/src/entity/form.rs index ba38f4ba..0946c322 100644 --- a/crates/sos24-domain/src/entity/form.rs +++ b/crates/sos24-domain/src/entity/form.rs @@ -210,6 +210,10 @@ impl Form { self.categories.matches(*project.category()) && self.attributes.matches(*project.attributes()) } + + pub fn find_item(&self, item_id: &FormItemId) -> Option<&FormItem> { + self.items.iter().find(|item| item.id() == item_id) + } } impl_value_object!(FormId(uuid::Uuid)); diff --git a/crates/sos24-domain/src/entity/form_answer.rs b/crates/sos24-domain/src/entity/form_answer.rs index 4e6916c7..c7ae3f54 100644 --- a/crates/sos24-domain/src/entity/form_answer.rs +++ b/crates/sos24-domain/src/entity/form_answer.rs @@ -84,14 +84,15 @@ impl FormAnswer { Ok(()) } - pub fn list_files(&self) -> Vec { + pub fn list_file_items(&self) -> Vec<(FormItemId, Vec)> { self.items .iter() .filter_map(|item| match &item.kind { - FormAnswerItemKind::File(file) => Some(file.clone().value()), + FormAnswerItemKind::File(file) => { + Some((item.item_id().clone(), file.clone().value())) + } _ => None, }) - .flatten() .collect() } } diff --git a/crates/sos24-presentation/src/error/convert_error.rs b/crates/sos24-presentation/src/error/convert_error.rs index 7d33a663..d0d3157a 100644 --- a/crates/sos24-presentation/src/error/convert_error.rs +++ b/crates/sos24-presentation/src/error/convert_error.rs @@ -200,6 +200,11 @@ impl From for AppError { "file/form-not-found".to_string(), message, ), + FileUseCaseError::FormItemNotFound(_) => AppError::new( + StatusCode::NOT_FOUND, + "file/form-item-not-found".to_string(), + message, + ), FileUseCaseError::FileDataRepositoryError(e) => e.into(), FileUseCaseError::FileIdError(e) => e.into(), FileUseCaseError::PermissionDeniedError(e) => e.into(), diff --git a/crates/sos24-use-case/Cargo.toml b/crates/sos24-use-case/Cargo.toml index 333013a3..86bd0e8e 100644 --- a/crates/sos24-use-case/Cargo.toml +++ b/crates/sos24-use-case/Cargo.toml @@ -9,5 +9,6 @@ anyhow.workspace = true chrono.workspace = true thiserror.workspace = true tokio.workspace = true +tracing.workspace = true uuid.workspace = true url.workspace = true diff --git a/crates/sos24-use-case/src/interactor/file.rs b/crates/sos24-use-case/src/interactor/file.rs index 0eb26b47..2f22b2ff 100644 --- a/crates/sos24-use-case/src/interactor/file.rs +++ b/crates/sos24-use-case/src/interactor/file.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use thiserror::Error; use sos24_domain::entity::file_data::{FileId, FileIdError}; -use sos24_domain::entity::form::{FormId, FormIdError}; +use sos24_domain::entity::form::{FormId, FormIdError, FormItemId}; use sos24_domain::entity::project::{ProjectId, ProjectIdError}; use sos24_domain::repository::file_data::FileDataRepositoryError; use sos24_domain::repository::file_object::FileObjectRepositoryError; @@ -31,6 +31,8 @@ pub enum FileUseCaseError { OwnerNotFound, #[error("Form not found: {0:?}")] FormNotFound(FormId), + #[error("Form item not found: {0:?}")] + FormItemNotFound(FormItemId), #[error(transparent)] FormRepositoryError(#[from] FormRepositoryError), diff --git a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs index fd085c8d..9bab61c6 100644 --- a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs +++ b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs @@ -2,12 +2,14 @@ use std::sync::Arc; use tokio::io::AsyncRead; +use sos24_domain::entity::file_data::FileName; use sos24_domain::entity::file_object::ArchiveEntry; use sos24_domain::entity::form::FormId; use sos24_domain::entity::permission::Permissions; use sos24_domain::repository::file_object::FileObjectRepository; use sos24_domain::repository::form::FormRepository; use sos24_domain::repository::form_answer::FormAnswerRepository; +use sos24_domain::repository::project::ProjectRepository; use sos24_domain::repository::Repositories; use sos24_domain::{ensure, repository::file_data::FileDataRepository}; @@ -42,33 +44,65 @@ impl FileUseCase { let mut file_list = Vec::new(); for form_answer in form_answer_list { - let file_ids = form_answer.value.list_files(); - for file_id in file_ids { - let file = self - .repositories - .file_data_repository() - .find_by_id(file_id.clone()) - .await? - .ok_or(FileUseCaseError::NotFound(file_id))?; - let file_data = file.value.destruct(); - file_list.push(ArchiveEntry::new( - file_data.url, - file_data.name, - file.updated_at, - )); + let project_id = form_answer.value.project_id().clone(); + let project = self + .repositories + .project_repository() + .find_by_id(project_id.clone()) + .await? + .ok_or(FileUseCaseError::ProjectNotFound(project_id))?; + let project = project.value.destruct(); + + let file_items = form_answer.value.list_file_items(); + for (item_id, files) in file_items { + let Some(form_item) = form.value.find_item(&item_id) else { + return Err(FileUseCaseError::FormItemNotFound(item_id)); + }; + + for (index, file_id) in files.into_iter().enumerate() { + let file = self + .repositories + .file_data_repository() + .find_by_id(file_id.clone()) + .await? + .ok_or(FileUseCaseError::NotFound(file_id))?; + let file_data = file.value.destruct(); + + let filename = format!( + "{}_{}_{}_{}_{}_{}_{}", + form.value.title().clone().value(), + form_item.name().clone().value(), + project.index.clone().value(), + project.title.clone().value(), + project.group_name.clone().value(), + index, + file_data.name.clone().value(), + ); + file_list.push(ArchiveEntry::new( + file_data.url, + FileName::new(filename), + file.updated_at, + )); + } } } - let archive = self - .repositories - .file_object_repository() - .create_archive(bucket, file_list) - .await?; + let (writer, reader) = tokio::io::duplex(65535); + let repositories = Arc::clone(&self.repositories); + tokio::spawn(async move { + if let Err(err) = repositories + .file_object_repository() + .create_archive(bucket, file_list, writer) + .await + { + tracing::error!("Failed to create archive: {err:?}"); + } + }); let form = form.value.destruct(); Ok(ArchiveToBeExportedDto { filename: format!("{}_ファイル一覧.zip", form.title.value()), - body: archive, + body: reader, }) } } From 54154b2d2f2fe4750b67c38b4d3e97a4bfd4b141 Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 05:40:27 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix:=20=E7=94=B3=E8=AB=8B=E4=B8=80?= =?UTF-8?q?=E8=A6=A7=E3=81=AE=E5=9E=8B=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-presentation/src/route/form.rs | 41 +++++++------------ .../src/interactor/form/list.rs | 10 ++--- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/crates/sos24-presentation/src/route/form.rs b/crates/sos24-presentation/src/route/form.rs index 0479933e..d5f0a656 100644 --- a/crates/sos24-presentation/src/route/form.rs +++ b/crates/sos24-presentation/src/route/form.rs @@ -22,36 +22,25 @@ pub async fn handle_get( State(modules): State>, Extension(ctx): Extension, ) -> Result { - match query.project_id { + let raw_form_list = match query.project_id { Some(project_id) => { - let raw_form_list = modules + modules .form_use_case() .find_by_project_id(&ctx, project_id) - .await; - raw_form_list - .map(|raw_form_list| { - let form_list: Vec = - raw_form_list.into_iter().map(FormSummary::from).collect(); - (StatusCode::OK, Json(form_list)).into_response() - }) - .map_err(|err| { - tracing::error!("Failed to find form by project id: {err:?}"); - err.into() - }) + .await } - None => { - let raw_form_list = modules.form_use_case().list(&ctx).await; - raw_form_list - .map(|raw_form_list| { - let form_list: Vec
= raw_form_list.into_iter().map(Form::from).collect(); - (StatusCode::OK, Json(form_list)).into_response() - }) - .map_err(|err| { - tracing::error!("Failed to list form: {err:?}"); - err.into() - }) - } - } + None => modules.form_use_case().list(&ctx).await, + }; + raw_form_list + .map(|raw_form_list| { + let form_list: Vec = + raw_form_list.into_iter().map(FormSummary::from).collect(); + (StatusCode::OK, Json(form_list)).into_response() + }) + .map_err(|err| { + tracing::error!("Failed to find form by project id: {err:?}"); + err.into() + }) } pub async fn handle_post( diff --git a/crates/sos24-use-case/src/interactor/form/list.rs b/crates/sos24-use-case/src/interactor/form/list.rs index 4dde1ab3..1b66bfc8 100644 --- a/crates/sos24-use-case/src/interactor/form/list.rs +++ b/crates/sos24-use-case/src/interactor/form/list.rs @@ -6,22 +6,20 @@ use sos24_domain::{ repository::{form::FormRepository, Repositories}, }; -use crate::{ - context::Context, - dto::{form::FormDto, FromEntity}, -}; +use crate::dto::form::FormSummaryDto; +use crate::{context::Context, dto::FromEntity}; use super::{FormUseCase, FormUseCaseError}; impl FormUseCase { - pub async fn list(&self, ctx: &Context) -> Result, FormUseCaseError> { + pub async fn list(&self, ctx: &Context) -> Result, FormUseCaseError> { let actor = ctx.actor(Arc::clone(&self.repositories)).await?; ensure!(actor.has_permission(Permissions::READ_FORM_ALL)); let raw_form_list = self.repositories.form_repository().list().await?; let form_list = raw_form_list .into_iter() - .map(|raw_form| FormDto::from_entity((raw_form, None))); + .map(|raw_form| FormSummaryDto::from_entity((raw_form, None))); Ok(form_list.collect()) } } From 9fd3566f9b56912e21eaa8667738cb8f14b5824e Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 05:53:34 +0900 Subject: [PATCH 12/18] =?UTF-8?q?feat:=20=E7=94=B3=E8=AB=8B=E9=A0=85?= =?UTF-8?q?=E7=9B=AE=E3=81=94=E3=81=A8=E3=81=AB=E3=83=87=E3=82=A3=E3=83=AC?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=83=AA=E3=82=92=E5=88=86=E3=81=91=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sos24-use-case/src/interactor/file/export_by_form_id.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs index 9bab61c6..7c63b8f7 100644 --- a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs +++ b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs @@ -69,13 +69,12 @@ impl FileUseCase { let file_data = file.value.destruct(); let filename = format!( - "{}_{}_{}_{}_{}_{}_{}", - form.value.title().clone().value(), + "{}/{}_{}_{}_{}_{}", form_item.name().clone().value(), project.index.clone().value(), project.title.clone().value(), project.group_name.clone().value(), - index, + index + 1, file_data.name.clone().value(), ); file_list.push(ArchiveEntry::new( From 3347df69feb683f1d81eb679af5fd09683cfb139 Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 06:39:32 +0900 Subject: [PATCH 13/18] =?UTF-8?q?feat:=20=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E5=90=8D=E3=82=92=E3=82=B5=E3=83=8B=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=82=BA=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-domain/src/entity/file_data.rs | 57 ++++++++++++++++++- .../src/test/fixture/file_data.rs | 2 +- .../src/postgresql/file_data.rs | 2 +- .../src/interactor/file/create.rs | 2 +- .../src/interactor/file/export_by_form_id.rs | 2 +- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/crates/sos24-domain/src/entity/file_data.rs b/crates/sos24-domain/src/entity/file_data.rs index de6030de..8b92ea34 100644 --- a/crates/sos24-domain/src/entity/file_data.rs +++ b/crates/sos24-domain/src/entity/file_data.rs @@ -1,3 +1,6 @@ +use std::ffi::OsStr; +use std::path::Path; + use getset::{Getters, Setters}; use thiserror::Error; @@ -55,7 +58,37 @@ pub struct DestructedFileData { } impl_value_object!(FileId(uuid::Uuid)); -impl_value_object!(FileName(String)); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileName(String); + +impl FileName { + pub fn sanitized(name: String) -> Self { + // ref: https://github.com/rwf2/Rocket/blob/60f3cd57b06243beaee87fd5b7545e3bf0fa6f60/core/lib/src/fs/file_name.rs#L140-L146 + static BAD_CHARS: &[char] = &[ + // These have special meaning in a file name. + '.', '/', '\\', // These are treated specially by shells. + '<', '>', '|', ':', '(', ')', '&', ';', '#', '?', '*', + ]; + + let file_name = Path::new(&name) + .file_name() + .and_then(OsStr::to_str) + .and_then(|n| n.split(BAD_CHARS).find(|s| !s.is_empty())) + .unwrap_or(""); + + let ext = Path::new(&name) + .extension() + .and_then(OsStr::to_str) + .unwrap_or(""); + + Self(format!("{}.{}", file_name, ext)) + } + + pub fn value(self) -> String { + self.0 + } +} #[derive(Debug, Error)] pub enum FileIdError { @@ -71,3 +104,25 @@ impl TryFrom for FileId { Ok(Self(uuid)) } } + +#[cfg(test)] +mod tests { + use crate::entity::file_data::FileName; + + #[test] + fn filename_sanitized() { + const TEST_CASES: [(&str, &str); 6] = [ + ("foo.txt", "foo.txt"), + ("foo.exe.txt", "foo.txt"), + ("../../foo.txt", "foo.txt"), + ("./foo.txt", "foo.txt"), + ("/bar/foo.txt", "foo.txt"), + ("/bar/.foo.txt", "foo.txt"), + ]; + + for (input, expected) in TEST_CASES { + let actual = FileName::sanitized(String::from(input)); + assert_eq!(actual.value().as_str(), expected); + } + } +} diff --git a/crates/sos24-domain/src/test/fixture/file_data.rs b/crates/sos24-domain/src/test/fixture/file_data.rs index 64562eb4..8134f1fb 100644 --- a/crates/sos24-domain/src/test/fixture/file_data.rs +++ b/crates/sos24-domain/src/test/fixture/file_data.rs @@ -7,7 +7,7 @@ pub fn id() -> FileId { } pub fn filename() -> FileName { - FileName::new("test.txt".to_string()) + FileName::sanitized("test.txt".to_string()) } pub fn file_data(owner: Option) -> FileData { diff --git a/crates/sos24-infrastructure/src/postgresql/file_data.rs b/crates/sos24-infrastructure/src/postgresql/file_data.rs index 180fb199..527231a1 100644 --- a/crates/sos24-infrastructure/src/postgresql/file_data.rs +++ b/crates/sos24-infrastructure/src/postgresql/file_data.rs @@ -32,7 +32,7 @@ impl TryFrom for WithDate { Ok(WithDate::new( FileData::new( FileId::new(value.id), - FileName::new(value.name), + FileName::sanitized(value.name), FileObjectKey::new(value.url), value.owner_project.map(ProjectId::new), ), diff --git a/crates/sos24-use-case/src/interactor/file/create.rs b/crates/sos24-use-case/src/interactor/file/create.rs index ba94baee..36a8c3d6 100644 --- a/crates/sos24-use-case/src/interactor/file/create.rs +++ b/crates/sos24-use-case/src/interactor/file/create.rs @@ -25,7 +25,7 @@ impl FileUseCase { ) -> Result { let actor = ctx.actor(Arc::clone(&self.repositories)).await?; let key = FileObjectKey::generate(key_prefix.as_str()); - let filename = FileName::new(raw_file.filename); + let filename = FileName::sanitized(raw_file.filename); let owner = match raw_file.owner { Some(it) => { ensure!(actor.has_permission(Permissions::CREATE_FILE_PRIVATE)); diff --git a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs index 7c63b8f7..07c891e2 100644 --- a/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs +++ b/crates/sos24-use-case/src/interactor/file/export_by_form_id.rs @@ -79,7 +79,7 @@ impl FileUseCase { ); file_list.push(ArchiveEntry::new( file_data.url, - FileName::new(filename), + FileName::sanitized(filename), file.updated_at, )); } From f6d37be15ff740202f5c419f553a56a01b8aba1b Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 07:06:18 +0900 Subject: [PATCH 14/18] =?UTF-8?q?fix:=20mongodb=E3=81=A7=E3=81=AE=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=99=82=E3=81=AB=E3=80=81=E5=9E=8B=E3=81=8C=E7=95=B0?= =?UTF-8?q?=E3=81=AA=E3=82=8B=E5=80=A4=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=9F=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-infrastructure/src/mongodb/form.rs | 16 ++++++++-------- .../src/mongodb/form_answer.rs | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/sos24-infrastructure/src/mongodb/form.rs b/crates/sos24-infrastructure/src/mongodb/form.rs index e8fe831a..516a62dd 100644 --- a/crates/sos24-infrastructure/src/mongodb/form.rs +++ b/crates/sos24-infrastructure/src/mongodb/form.rs @@ -307,14 +307,14 @@ impl FormRepository for MongoFormRepository { doc! { "_id": form_doc._id, "deleted_at": None:: }, doc! { "$set": doc! { - "title": form_doc.title, - "description": form_doc.description, - "starts_at": form_doc.starts_at, - "ends_at": form_doc.ends_at, - "categories": form_doc.categories, - "attributes": form_doc.attributes, + "title": bson::to_bson(&form_doc.title).unwrap(), + "description": bson::to_bson(&form_doc.description).unwrap(), + "starts_at": bson::to_bson(&form_doc.starts_at).unwrap(), + "ends_at":bson::to_bson(&form_doc.ends_at).unwrap(), + "categories": bson::to_bson(&form_doc.categories).unwrap(), + "attributes": bson::to_bson(&form_doc.attributes).unwrap(), "items": bson::to_bson(&form_doc.items).unwrap(), - "updated_at": form_doc.updated_at, + "updated_at": bson::to_bson(&form_doc.updated_at).unwrap(), } }, None, @@ -332,7 +332,7 @@ impl FormRepository for MongoFormRepository { self.collection .update_one( doc! { "_id": id.clone().value(), "deleted_at": None:: }, - doc! { "$set": { "deleted_at": chrono::Utc::now() } }, + doc! { "$set": { "deleted_at": bson::to_bson(&chrono::Utc::now()).unwrap() } }, None, ) .await diff --git a/crates/sos24-infrastructure/src/mongodb/form_answer.rs b/crates/sos24-infrastructure/src/mongodb/form_answer.rs index 5d543b39..dec0c2c0 100644 --- a/crates/sos24-infrastructure/src/mongodb/form_answer.rs +++ b/crates/sos24-infrastructure/src/mongodb/form_answer.rs @@ -303,10 +303,10 @@ impl FormAnswerRepository for MongoFormAnswerRepository { doc! { "_id": form_answer_doc._id, "deleted_at": None:: }, doc! { "$set": doc! { - "project_id": form_answer_doc.project_id, - "form_d": form_answer_doc.form_id, + "project_id": bson::to_bson(&form_answer_doc.project_id).unwrap(), + "form_id": bson::to_bson(&form_answer_doc.form_id).unwrap(), "items": bson::to_bson(&form_answer_doc.items).unwrap(), - "updated_at": form_answer_doc.updated_at, + "updated_at": bson::to_bson(&form_answer_doc.updated_at).unwrap(), } }, None, From 12ee933c63a81fc6aa98b343c389954ec7cf0a11 Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 14:13:19 +0900 Subject: [PATCH 15/18] =?UTF-8?q?docs:=20=E3=82=B9=E3=82=AD=E3=83=BC?= =?UTF-8?q?=E3=83=9E=E3=81=AE=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E5=BD=A2?= =?UTF-8?q?=E5=BC=8F=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schema/file.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/schema/file.yml b/schema/file.yml index 72685fb0..230c78e3 100644 --- a/schema/file.yml +++ b/schema/file.yml @@ -180,9 +180,10 @@ paths: 200: description: OK content: - text/csv: + application/zip: schema: type: string + format: binary 401: description: Unauthorized content: From 5f11243ad1d1cc02aece0ed3bf31ee63deb79996 Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 14:24:43 +0900 Subject: [PATCH 16/18] =?UTF-8?q?feat:=20=E4=BC=81=E7=94=BB=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=97=E3=81=9F=E3=81=A8=E3=81=8D=E3=81=AB?= =?UTF-8?q?=E3=80=81=E9=96=A2=E9=80=A3=E3=81=99=E3=82=8B=E3=83=AA=E3=82=BD?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=82=92=E5=89=8A=E9=99=A4=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sos24-domain/src/repository/file_data.rs | 4 +++ .../src/repository/form_answer.rs | 1 + .../sos24-domain/src/repository/invitation.rs | 2 ++ .../src/mongodb/form_answer.rs | 12 +++++++ .../src/postgresql/file_data.rs | 14 ++++++++ .../src/postgresql/invitation.rs | 11 +++++++ .../src/error/convert_error.rs | 3 ++ .../sos24-use-case/src/interactor/project.rs | 9 +++++ .../src/interactor/project/delete_by_id.rs | 33 ++++++++++++++++++- 9 files changed, 88 insertions(+), 1 deletion(-) diff --git a/crates/sos24-domain/src/repository/file_data.rs b/crates/sos24-domain/src/repository/file_data.rs index 2b7676e6..fe33d5c3 100644 --- a/crates/sos24-domain/src/repository/file_data.rs +++ b/crates/sos24-domain/src/repository/file_data.rs @@ -27,4 +27,8 @@ pub trait FileDataRepository: Send + Sync + 'static { owner_project: ProjectId, ) -> Result>, FileDataRepositoryError>; async fn delete_by_id(&self, id: FileId) -> Result<(), FileDataRepositoryError>; + async fn delete_by_owner_project( + &self, + owner_project: ProjectId, + ) -> Result<(), FileDataRepositoryError>; } diff --git a/crates/sos24-domain/src/repository/form_answer.rs b/crates/sos24-domain/src/repository/form_answer.rs index 6b707b81..c656cd15 100644 --- a/crates/sos24-domain/src/repository/form_answer.rs +++ b/crates/sos24-domain/src/repository/form_answer.rs @@ -35,4 +35,5 @@ pub trait FormAnswerRepository: Send + Sync + 'static { form_id: FormId, ) -> Result>, FormAnswerRepositoryError>; async fn update(&self, form_answer: FormAnswer) -> Result<(), FormAnswerRepositoryError>; + async fn delete_by_project_id(&self, id: ProjectId) -> Result<(), FormAnswerRepositoryError>; } diff --git a/crates/sos24-domain/src/repository/invitation.rs b/crates/sos24-domain/src/repository/invitation.rs index 643864d0..4d97cb2d 100644 --- a/crates/sos24-domain/src/repository/invitation.rs +++ b/crates/sos24-domain/src/repository/invitation.rs @@ -1,6 +1,7 @@ use mockall::automock; use thiserror::Error; +use crate::entity::project::ProjectId; use crate::entity::user::UserId; use crate::entity::{ common::date::WithDate, @@ -30,4 +31,5 @@ pub trait InvitationRepository: Send + Sync + 'static { async fn update(&self, invitation: Invitation) -> Result<(), InvitationRepositoryError>; async fn delete_by_id(&self, id: InvitationId) -> Result<(), InvitationRepositoryError>; + async fn delete_by_project_id(&self, id: ProjectId) -> Result<(), InvitationRepositoryError>; } diff --git a/crates/sos24-infrastructure/src/mongodb/form_answer.rs b/crates/sos24-infrastructure/src/mongodb/form_answer.rs index dec0c2c0..f9909cff 100644 --- a/crates/sos24-infrastructure/src/mongodb/form_answer.rs +++ b/crates/sos24-infrastructure/src/mongodb/form_answer.rs @@ -317,4 +317,16 @@ impl FormAnswerRepository for MongoFormAnswerRepository { tracing::info!("申請回答を更新しました"); Ok(()) } + + async fn delete_by_project_id(&self, id: ProjectId) -> Result<(), FormAnswerRepositoryError> { + self.collection + .update_many( + doc! { "project_id": id.value(), "deleted_at": None:: }, + doc! { "$set": { "deleted_at": chrono::Utc::now() } }, + None, + ) + .await + .context("Failed to delete form by project id")?; + Ok(()) + } } diff --git a/crates/sos24-infrastructure/src/postgresql/file_data.rs b/crates/sos24-infrastructure/src/postgresql/file_data.rs index 180fb199..c4e7fdd7 100644 --- a/crates/sos24-infrastructure/src/postgresql/file_data.rs +++ b/crates/sos24-infrastructure/src/postgresql/file_data.rs @@ -144,4 +144,18 @@ impl FileDataRepository for PgFileDataRepository { tracing::info!("ファイルデータの削除が完了しました: {id:?}"); Ok(()) } + + async fn delete_by_owner_project( + &self, + owner_project: ProjectId, + ) -> Result<(), FileDataRepositoryError> { + sqlx::query!( + r#"UPDATE files SET deleted_at = NOW() WHERE owner_project = $1 AND deleted_at IS NULL"#, + owner_project.value() + ) + .execute(&*self.db) + .await + .context("Failed to delete file data by owner project")?; + Ok(()) + } } diff --git a/crates/sos24-infrastructure/src/postgresql/invitation.rs b/crates/sos24-infrastructure/src/postgresql/invitation.rs index 17e9a35d..04ed1e11 100644 --- a/crates/sos24-infrastructure/src/postgresql/invitation.rs +++ b/crates/sos24-infrastructure/src/postgresql/invitation.rs @@ -189,4 +189,15 @@ impl InvitationRepository for PgInvitationRepository { tracing::info!("招待を削除しました: {id:?}"); Ok(()) } + + async fn delete_by_project_id(&self, id: ProjectId) -> Result<(), InvitationRepositoryError> { + sqlx::query!( + r#"UPDATE invitations SET deleted_at = now() WHERE project_id = $1 AND deleted_at IS NULL"#, + id.value() + ) + .execute(&*self.db) + .await + .context("Failed to delete invitation by project id")?; + Ok(()) + } } diff --git a/crates/sos24-presentation/src/error/convert_error.rs b/crates/sos24-presentation/src/error/convert_error.rs index 1f263dc5..98718d32 100644 --- a/crates/sos24-presentation/src/error/convert_error.rs +++ b/crates/sos24-presentation/src/error/convert_error.rs @@ -331,6 +331,9 @@ impl From for AppError { ProjectUseCaseError::PermissionDeniedError(e) => e.into(), ProjectUseCaseError::InternalError(e) => e.into(), ProjectUseCaseError::UserRepositoryError(e) => e.into(), + ProjectUseCaseError::FormAnswerRepositoryError(e) => e.into(), + ProjectUseCaseError::InvitationRepositoryError(e) => e.into(), + ProjectUseCaseError::FileDataRepositoryError(e) => e.into(), } } } diff --git a/crates/sos24-use-case/src/interactor/project.rs b/crates/sos24-use-case/src/interactor/project.rs index 0172f1dd..f572d140 100644 --- a/crates/sos24-use-case/src/interactor/project.rs +++ b/crates/sos24-use-case/src/interactor/project.rs @@ -4,6 +4,9 @@ use thiserror::Error; use sos24_domain::entity::project::BoundedStringError; use sos24_domain::entity::user::UserId; +use sos24_domain::repository::file_data::FileDataRepositoryError; +use sos24_domain::repository::form_answer::FormAnswerRepositoryError; +use sos24_domain::repository::invitation::InvitationRepositoryError; use sos24_domain::repository::user::UserRepositoryError; use sos24_domain::{ entity::{ @@ -35,6 +38,12 @@ pub enum ProjectUseCaseError { #[error("User not found: {0:?}")] UserNotFound(UserId), + #[error(transparent)] + FormAnswerRepositoryError(#[from] FormAnswerRepositoryError), + #[error(transparent)] + InvitationRepositoryError(#[from] InvitationRepositoryError), + #[error(transparent)] + FileDataRepositoryError(#[from] FileDataRepositoryError), #[error(transparent)] UserRepositoryError(#[from] UserRepositoryError), #[error(transparent)] diff --git a/crates/sos24-use-case/src/interactor/project/delete_by_id.rs b/crates/sos24-use-case/src/interactor/project/delete_by_id.rs index a705f248..5aa3a062 100644 --- a/crates/sos24-use-case/src/interactor/project/delete_by_id.rs +++ b/crates/sos24-use-case/src/interactor/project/delete_by_id.rs @@ -3,6 +3,9 @@ use std::sync::Arc; use sos24_domain::ensure; use sos24_domain::entity::permission::Permissions; use sos24_domain::entity::project::ProjectId; +use sos24_domain::repository::file_data::FileDataRepository; +use sos24_domain::repository::form_answer::FormAnswerRepository; +use sos24_domain::repository::invitation::InvitationRepository; use sos24_domain::repository::project::ProjectRepository; use sos24_domain::repository::Repositories; @@ -23,8 +26,24 @@ impl ProjectUseCase { self.repositories .project_repository() - .delete_by_id(id) + .delete_by_id(id.clone()) .await?; + + self.repositories + .form_answer_repository() + .delete_by_project_id(id.clone()) + .await?; + + self.repositories + .invitation_repository() + .delete_by_project_id(id.clone()) + .await?; + + self.repositories + .file_data_repository() + .delete_by_owner_project(id) + .await?; + Ok(()) } } @@ -84,6 +103,18 @@ mod tests { .project_repository_mut() .expect_delete_by_id() .returning(|_| Ok(())); + repositories + .form_answer_repository_mut() + .expect_delete_by_project_id() + .returning(|_| Ok(())); + repositories + .invitation_repository_mut() + .expect_delete_by_project_id() + .returning(|_| Ok(())); + repositories + .file_data_repository_mut() + .expect_delete_by_owner_project() + .returning(|_| Ok(())); let use_case = ProjectUseCase::new( Arc::new(repositories), fixture::project_application_period::applicable_period(), From 1bd465e14a60812f9b294d467b5234eb9c839987 Mon Sep 17 00:00:00 2001 From: Arata Date: Mon, 15 Apr 2024 15:15:16 +0900 Subject: [PATCH 17/18] chore: cargo sqlx prepare --workflow --- ...76af4de936e68c805db07f964c9ec58ab5455fb89f.json | 14 ++++++++++++++ ...6c925439757e36ab017d14956e7d3eaffaee8dc9f6.json | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 .sqlx/query-ee9013ca0b0ee32873e1c176af4de936e68c805db07f964c9ec58ab5455fb89f.json create mode 100644 .sqlx/query-fd47d8be29b70be98c664a6c925439757e36ab017d14956e7d3eaffaee8dc9f6.json diff --git a/.sqlx/query-ee9013ca0b0ee32873e1c176af4de936e68c805db07f964c9ec58ab5455fb89f.json b/.sqlx/query-ee9013ca0b0ee32873e1c176af4de936e68c805db07f964c9ec58ab5455fb89f.json new file mode 100644 index 00000000..356df311 --- /dev/null +++ b/.sqlx/query-ee9013ca0b0ee32873e1c176af4de936e68c805db07f964c9ec58ab5455fb89f.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE files SET deleted_at = NOW() WHERE owner_project = $1 AND deleted_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "ee9013ca0b0ee32873e1c176af4de936e68c805db07f964c9ec58ab5455fb89f" +} diff --git a/.sqlx/query-fd47d8be29b70be98c664a6c925439757e36ab017d14956e7d3eaffaee8dc9f6.json b/.sqlx/query-fd47d8be29b70be98c664a6c925439757e36ab017d14956e7d3eaffaee8dc9f6.json new file mode 100644 index 00000000..a21d0094 --- /dev/null +++ b/.sqlx/query-fd47d8be29b70be98c664a6c925439757e36ab017d14956e7d3eaffaee8dc9f6.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE invitations SET deleted_at = now() WHERE project_id = $1 AND deleted_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "fd47d8be29b70be98c664a6c925439757e36ab017d14956e7d3eaffaee8dc9f6" +} From 3accaf4b263eab44af8b2f7c500ec2dd58b75ba8 Mon Sep 17 00:00:00 2001 From: Arata Date: Tue, 16 Apr 2024 14:15:08 +0900 Subject: [PATCH 18/18] =?UTF-8?q?feat:=20bson=E3=81=AE=E5=9E=8B=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E3=81=86=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-infrastructure/src/mongodb/form_answer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sos24-infrastructure/src/mongodb/form_answer.rs b/crates/sos24-infrastructure/src/mongodb/form_answer.rs index f9909cff..af561fdd 100644 --- a/crates/sos24-infrastructure/src/mongodb/form_answer.rs +++ b/crates/sos24-infrastructure/src/mongodb/form_answer.rs @@ -322,7 +322,7 @@ impl FormAnswerRepository for MongoFormAnswerRepository { self.collection .update_many( doc! { "project_id": id.value(), "deleted_at": None:: }, - doc! { "$set": { "deleted_at": chrono::Utc::now() } }, + doc! { "$set": { "deleted_at": bson::to_bson(&chrono::Utc::now()).unwrap() } }, None, ) .await