From ab3050db69a9836dc106a03cc645a995257e38c6 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sun, 27 Jul 2025 13:10:46 +0800 Subject: [PATCH] BLOG-78 Backend image upload and download (#84) ### Description - Add some endpoints about image: - POST `/image/upload` - GET `/image/{id}` > [!NOTE] > Since there isn't identity authentication, the `/image` endpoints should be restricted to private network in nginx. > [!NOTE] > Volume for backend should be configured in `pod.yaml`. ### Package Changes ```toml actix-multipart = "0.7.2" ``` ### Screenshots _No response_ ### Reference Resolves #78 ### Checklist - [x] A milestone is set - [x] The related issuse has been linked to this branch Reviewed-on: https://git.squidspirit.com/squid/blog/pulls/84 Co-authored-by: SquidSpirit Co-committed-by: SquidSpirit --- backend/Cargo.lock | 181 +++++++++++++++++- backend/Cargo.toml | 4 +- backend/Dockerfile | 2 + backend/README.md | 1 - backend/feature/image/Cargo.toml | 14 ++ backend/feature/image/src/adapter.rs | 2 + backend/feature/image/src/adapter/delivery.rs | 4 + .../src/adapter/delivery/image_controller.rs | 82 ++++++++ .../delivery/image_info_response_dto.rs | 7 + .../src/adapter/delivery/image_request_dto.rs | 16 ++ .../adapter/delivery/image_response_dto.rs | 5 + backend/feature/image/src/adapter/gateway.rs | 4 + .../src/adapter/gateway/image_db_mapper.rs | 25 +++ .../src/adapter/gateway/image_db_service.rs | 11 ++ .../adapter/gateway/image_repository_impl.rs | 52 +++++ .../src/adapter/gateway/image_storage.rs | 6 + backend/feature/image/src/application.rs | 3 + .../feature/image/src/application/error.rs | 1 + .../src/application/error/image_error.rs | 7 + .../feature/image/src/application/gateway.rs | 1 + .../application/gateway/image_repository.rs | 9 + .../feature/image/src/application/use_case.rs | 2 + .../use_case/get_image_use_case.rs | 30 +++ .../use_case/upload_image_use_case.rs | 28 +++ backend/feature/image/src/domain.rs | 1 + backend/feature/image/src/domain/entity.rs | 1 + .../feature/image/src/domain/entity/image.rs | 5 + backend/feature/image/src/framework.rs | 3 + backend/feature/image/src/framework/db.rs | 2 + .../src/framework/db/image_db_service_impl.rs | 65 +++++++ .../image/src/framework/db/image_record.rs | 5 + .../feature/image/src/framework/storage.rs | 1 + .../framework/storage/image_storage_impl.rs | 41 ++++ backend/feature/image/src/framework/web.rs | 1 + .../src/framework/web/image_web_routes.rs | 89 +++++++++ backend/feature/image/src/lib.rs | 4 + backend/feature/post/src/adapter/gateway.rs | 8 +- .../{color_mapper.rs => color_db_mapper.rs} | 2 +- .../src/adapter/gateway/label_db_mapper.rs | 17 ++ .../post/src/adapter/gateway/label_mapper.rs | 17 -- .../src/adapter/gateway/post_db_mapper.rs | 17 ++ .../src/adapter/gateway/post_db_service.rs | 2 +- ..._info_mapper.rs => post_info_db_mapper.rs} | 6 +- .../post/src/adapter/gateway/post_mapper.rs | 17 -- .../adapter/gateway/post_repository_impl.rs | 4 +- .../src/framework/db/post_db_service_impl.rs | 4 +- .../framework/db/post_with_label_record.rs | 2 +- backend/migrations/20250725231740_v0.3.0.sql | 12 ++ backend/server/Cargo.toml | 1 + backend/server/src/container.rs | 37 +++- backend/server/src/main.rs | 9 +- 51 files changed, 811 insertions(+), 59 deletions(-) create mode 100644 backend/feature/image/Cargo.toml create mode 100644 backend/feature/image/src/adapter.rs create mode 100644 backend/feature/image/src/adapter/delivery.rs create mode 100644 backend/feature/image/src/adapter/delivery/image_controller.rs create mode 100644 backend/feature/image/src/adapter/delivery/image_info_response_dto.rs create mode 100644 backend/feature/image/src/adapter/delivery/image_request_dto.rs create mode 100644 backend/feature/image/src/adapter/delivery/image_response_dto.rs create mode 100644 backend/feature/image/src/adapter/gateway.rs create mode 100644 backend/feature/image/src/adapter/gateway/image_db_mapper.rs create mode 100644 backend/feature/image/src/adapter/gateway/image_db_service.rs create mode 100644 backend/feature/image/src/adapter/gateway/image_repository_impl.rs create mode 100644 backend/feature/image/src/adapter/gateway/image_storage.rs create mode 100644 backend/feature/image/src/application.rs create mode 100644 backend/feature/image/src/application/error.rs create mode 100644 backend/feature/image/src/application/error/image_error.rs create mode 100644 backend/feature/image/src/application/gateway.rs create mode 100644 backend/feature/image/src/application/gateway/image_repository.rs create mode 100644 backend/feature/image/src/application/use_case.rs create mode 100644 backend/feature/image/src/application/use_case/get_image_use_case.rs create mode 100644 backend/feature/image/src/application/use_case/upload_image_use_case.rs create mode 100644 backend/feature/image/src/domain.rs create mode 100644 backend/feature/image/src/domain/entity.rs create mode 100644 backend/feature/image/src/domain/entity/image.rs create mode 100644 backend/feature/image/src/framework.rs create mode 100644 backend/feature/image/src/framework/db.rs create mode 100644 backend/feature/image/src/framework/db/image_db_service_impl.rs create mode 100644 backend/feature/image/src/framework/db/image_record.rs create mode 100644 backend/feature/image/src/framework/storage.rs create mode 100644 backend/feature/image/src/framework/storage/image_storage_impl.rs create mode 100644 backend/feature/image/src/framework/web.rs create mode 100644 backend/feature/image/src/framework/web/image_web_routes.rs create mode 100644 backend/feature/image/src/lib.rs rename backend/feature/post/src/adapter/gateway/{color_mapper.rs => color_db_mapper.rs} (89%) create mode 100644 backend/feature/post/src/adapter/gateway/label_db_mapper.rs delete mode 100644 backend/feature/post/src/adapter/gateway/label_mapper.rs create mode 100644 backend/feature/post/src/adapter/gateway/post_db_mapper.rs rename backend/feature/post/src/adapter/gateway/{post_info_mapper.rs => post_info_db_mapper.rs} (74%) delete mode 100644 backend/feature/post/src/adapter/gateway/post_mapper.rs create mode 100644 backend/migrations/20250725231740_v0.3.0.sql diff --git a/backend/Cargo.lock b/backend/Cargo.lock index e1453e1..9c5f4b0 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -34,7 +34,7 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more", + "derive_more 2.0.1", "encoding_rs", "flate2", "foldhash", @@ -68,6 +68,44 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-multipart" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "derive_more 0.99.20", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "rand 0.8.5", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "actix-router" version = "0.5.3" @@ -149,7 +187,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more", + "derive_more 2.0.1", "encoding_rs", "foldhash", "futures-core", @@ -466,6 +504,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "cookie" version = "0.16.2" @@ -541,6 +585,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "der" version = "0.7.10" @@ -561,6 +640,19 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -744,6 +836,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -788,6 +895,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -806,8 +924,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1098,6 +1218,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1119,6 +1245,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.2.0" +dependencies = [ + "actix-multipart", + "actix-web", + "async-trait", + "chrono", + "futures", + "log", + "serde", + "sqlx", +] + [[package]] name = "impl-more" version = "0.1.9" @@ -1413,6 +1553,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1676,6 +1822,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.0.7" @@ -1738,6 +1893,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" @@ -1770,6 +1931,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1789,6 +1959,7 @@ dependencies = [ "actix-web", "dotenv", "env_logger", + "image", "percent-encoding", "post", "sqlx", @@ -2100,6 +2271,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5ac94f9..7b4f8db 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["feature/post", "server"] +members = ["server", "feature/image", "feature/post"] resolver = "2" [workspace.package] @@ -7,6 +7,7 @@ version = "0.2.0" edition = "2024" [workspace.dependencies] +actix-multipart = "0.7.2" actix-web = "4.10.2" async-trait = "0.1.88" chrono = "0.4.41" @@ -25,4 +26,5 @@ sqlx = { version = "0.8.5", features = [ tokio = { version = "1.45.0", features = ["full"] } server.path = "server" +image.path = "feature/image" post.path = "feature/post" diff --git a/backend/Dockerfile b/backend/Dockerfile index 1268847..e67f7ae 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -11,9 +11,11 @@ FROM alpine:latest AS runner WORKDIR /app COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/server . EXPOSE 8080 +VOLUME ["/app/static"] ENV RUST_LOG=info ENV HOST=0.0.0.0 ENV PORT=8080 +ENV STORAGE_PATH=/app/static ENV DATABASE_HOST=127.0.0.1 ENV DATABASE_PORT=5432 ENV DATABASE_USER=postgres diff --git a/backend/README.md b/backend/README.md index 89d98eb..dbc4b51 100644 --- a/backend/README.md +++ b/backend/README.md @@ -39,4 +39,3 @@ ```bash RUST_LOG=debug watchexec -e rs -r 'cargo run' ``` - diff --git a/backend/feature/image/Cargo.toml b/backend/feature/image/Cargo.toml new file mode 100644 index 0000000..07a7f78 --- /dev/null +++ b/backend/feature/image/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "image" +version.workspace = true +edition.workspace = true + +[dependencies] +actix-multipart.workspace = true +actix-web.workspace = true +async-trait.workspace = true +chrono.workspace = true +futures.workspace = true +log.workspace = true +serde.workspace = true +sqlx.workspace = true diff --git a/backend/feature/image/src/adapter.rs b/backend/feature/image/src/adapter.rs new file mode 100644 index 0000000..a37dc94 --- /dev/null +++ b/backend/feature/image/src/adapter.rs @@ -0,0 +1,2 @@ +pub mod delivery; +pub mod gateway; \ No newline at end of file diff --git a/backend/feature/image/src/adapter/delivery.rs b/backend/feature/image/src/adapter/delivery.rs new file mode 100644 index 0000000..50d3c9a --- /dev/null +++ b/backend/feature/image/src/adapter/delivery.rs @@ -0,0 +1,4 @@ +pub mod image_controller; +pub mod image_info_response_dto; +pub mod image_request_dto; +pub mod image_response_dto; diff --git a/backend/feature/image/src/adapter/delivery/image_controller.rs b/backend/feature/image/src/adapter/delivery/image_controller.rs new file mode 100644 index 0000000..4f85440 --- /dev/null +++ b/backend/feature/image/src/adapter/delivery/image_controller.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + adapter::delivery::{ + image_info_response_dto::ImageInfoResponseDto, image_request_dto::ImageRequestDto, + image_response_dto::ImageResponseDto, + }, + application::{ + error::image_error::ImageError, + use_case::{ + get_image_use_case::GetImageUseCase, upload_image_use_case::UploadImageUseCase, + }, + }, +}; + +#[async_trait] +pub trait ImageController: Send + Sync { + async fn upload_image( + &self, + image: ImageRequestDto, + ) -> Result; + + async fn get_image_by_id(&self, id: i32) -> Result; +} + +pub struct ImageControllerImpl { + upload_image_use_case: Arc, + get_image_use_case: Arc, + + mime_type_whitelist: Vec, +} + +impl ImageControllerImpl { + pub fn new( + upload_image_use_case: Arc, + get_image_use_case: Arc, + ) -> Self { + Self { + upload_image_use_case, + get_image_use_case, + mime_type_whitelist: vec![ + "image/jpeg".to_string(), + "image/png".to_string(), + "image/gif".to_string(), + "image/webp".to_string(), + ], + } + } +} + +#[async_trait] +impl ImageController for ImageControllerImpl { + async fn upload_image( + &self, + image: ImageRequestDto, + ) -> Result { + if !self.mime_type_whitelist.contains(&image.mime_type) { + return Err(ImageError::UnsupportedMimeType); + } + + let mime_type = image.mime_type.clone(); + let id = self + .upload_image_use_case + .execute(image.into_entity()) + .await?; + Ok(ImageInfoResponseDto { + id: id, + mime_type: mime_type, + }) + } + + async fn get_image_by_id(&self, id: i32) -> Result { + let image = self.get_image_use_case.execute(id).await?; + Ok(ImageResponseDto { + id: id, + mime_type: image.mime_type, + data: image.data, + }) + } +} diff --git a/backend/feature/image/src/adapter/delivery/image_info_response_dto.rs b/backend/feature/image/src/adapter/delivery/image_info_response_dto.rs new file mode 100644 index 0000000..cb5b9f7 --- /dev/null +++ b/backend/feature/image/src/adapter/delivery/image_info_response_dto.rs @@ -0,0 +1,7 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct ImageInfoResponseDto { + pub id: i32, + pub mime_type: String, +} diff --git a/backend/feature/image/src/adapter/delivery/image_request_dto.rs b/backend/feature/image/src/adapter/delivery/image_request_dto.rs new file mode 100644 index 0000000..8c86c17 --- /dev/null +++ b/backend/feature/image/src/adapter/delivery/image_request_dto.rs @@ -0,0 +1,16 @@ +use crate::domain::entity::image::Image; + +pub struct ImageRequestDto { + pub mime_type: String, + pub data: Vec, +} + +impl ImageRequestDto { + pub fn into_entity(self) -> Image { + Image { + id: None, + mime_type: self.mime_type, + data: self.data, + } + } +} diff --git a/backend/feature/image/src/adapter/delivery/image_response_dto.rs b/backend/feature/image/src/adapter/delivery/image_response_dto.rs new file mode 100644 index 0000000..2541066 --- /dev/null +++ b/backend/feature/image/src/adapter/delivery/image_response_dto.rs @@ -0,0 +1,5 @@ +pub struct ImageResponseDto { + pub id: i32, + pub mime_type: String, + pub data: Vec, +} diff --git a/backend/feature/image/src/adapter/gateway.rs b/backend/feature/image/src/adapter/gateway.rs new file mode 100644 index 0000000..f288c1c --- /dev/null +++ b/backend/feature/image/src/adapter/gateway.rs @@ -0,0 +1,4 @@ +pub mod image_db_service; +pub mod image_db_mapper; +pub mod image_repository_impl; +pub mod image_storage; diff --git a/backend/feature/image/src/adapter/gateway/image_db_mapper.rs b/backend/feature/image/src/adapter/gateway/image_db_mapper.rs new file mode 100644 index 0000000..fb92823 --- /dev/null +++ b/backend/feature/image/src/adapter/gateway/image_db_mapper.rs @@ -0,0 +1,25 @@ +use crate::domain::entity::image::Image; + +pub struct ImageDbMapper { + pub id: Option, + pub mime_type: String, +} + +impl ImageDbMapper { + pub fn into_entity(self) -> Image { + Image { + id: self.id, + mime_type: self.mime_type, + data: Vec::new(), + } + } +} + +impl From for ImageDbMapper { + fn from(image: Image) -> Self { + ImageDbMapper { + id: image.id, + mime_type: image.mime_type, + } + } +} diff --git a/backend/feature/image/src/adapter/gateway/image_db_service.rs b/backend/feature/image/src/adapter/gateway/image_db_service.rs new file mode 100644 index 0000000..08dd3fd --- /dev/null +++ b/backend/feature/image/src/adapter/gateway/image_db_service.rs @@ -0,0 +1,11 @@ +use async_trait::async_trait; + +use crate::{ + adapter::gateway::image_db_mapper::ImageDbMapper, application::error::image_error::ImageError, +}; + +#[async_trait] +pub trait ImageDbService: Send + Sync { + async fn create_image_info(&self, image: ImageDbMapper) -> Result; + async fn get_image_info_by_id(&self, id: i32) -> Result; +} diff --git a/backend/feature/image/src/adapter/gateway/image_repository_impl.rs b/backend/feature/image/src/adapter/gateway/image_repository_impl.rs new file mode 100644 index 0000000..f1d5a3a --- /dev/null +++ b/backend/feature/image/src/adapter/gateway/image_repository_impl.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + adapter::gateway::{ + image_db_mapper::ImageDbMapper, image_db_service::ImageDbService, + image_storage::ImageStorage, + }, + application::{error::image_error::ImageError, gateway::image_repository::ImageRepository}, + domain::entity::image::Image, +}; + +pub struct ImageRepositoryImpl { + image_db_service: Arc, + image_storage: Arc, +} + +impl ImageRepositoryImpl { + pub fn new( + image_db_service: Arc, + image_storage: Arc, + ) -> Self { + Self { + image_db_service, + image_storage, + } + } +} + +#[async_trait] +impl ImageRepository for ImageRepositoryImpl { + async fn save_image(&self, image: Image) -> Result { + let data = image.data.clone(); + let image_id = self + .image_db_service + .create_image_info(ImageDbMapper::from(image)) + .await?; + self.image_storage.write_data(image_id, &data)?; + Ok(image_id) + } + + async fn get_image_by_id(&self, id: i32) -> Result { + let image_mapper = self.image_db_service.get_image_info_by_id(id).await?; + let data = self.image_storage.read_data(id)?; + Ok(Image { + id: image_mapper.id, + mime_type: image_mapper.mime_type, + data, + }) + } +} diff --git a/backend/feature/image/src/adapter/gateway/image_storage.rs b/backend/feature/image/src/adapter/gateway/image_storage.rs new file mode 100644 index 0000000..cd92402 --- /dev/null +++ b/backend/feature/image/src/adapter/gateway/image_storage.rs @@ -0,0 +1,6 @@ +use crate::application::error::image_error::ImageError; + +pub trait ImageStorage: Send + Sync { + fn write_data(&self, id: i32, data: &[u8]) -> Result<(), ImageError>; + fn read_data(&self, id: i32) -> Result, ImageError>; +} diff --git a/backend/feature/image/src/application.rs b/backend/feature/image/src/application.rs new file mode 100644 index 0000000..f24625e --- /dev/null +++ b/backend/feature/image/src/application.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod gateway; +pub mod use_case; diff --git a/backend/feature/image/src/application/error.rs b/backend/feature/image/src/application/error.rs new file mode 100644 index 0000000..3765d4d --- /dev/null +++ b/backend/feature/image/src/application/error.rs @@ -0,0 +1 @@ +pub mod image_error; \ No newline at end of file diff --git a/backend/feature/image/src/application/error/image_error.rs b/backend/feature/image/src/application/error/image_error.rs new file mode 100644 index 0000000..5c10b00 --- /dev/null +++ b/backend/feature/image/src/application/error/image_error.rs @@ -0,0 +1,7 @@ +#[derive(Debug, PartialEq)] +pub enum ImageError { + DatabaseError(String), + StorageError(String), + NotFound, + UnsupportedMimeType, +} diff --git a/backend/feature/image/src/application/gateway.rs b/backend/feature/image/src/application/gateway.rs new file mode 100644 index 0000000..e52c8d8 --- /dev/null +++ b/backend/feature/image/src/application/gateway.rs @@ -0,0 +1 @@ +pub mod image_repository; \ No newline at end of file diff --git a/backend/feature/image/src/application/gateway/image_repository.rs b/backend/feature/image/src/application/gateway/image_repository.rs new file mode 100644 index 0000000..49be12d --- /dev/null +++ b/backend/feature/image/src/application/gateway/image_repository.rs @@ -0,0 +1,9 @@ +use async_trait::async_trait; + +use crate::{application::error::image_error::ImageError, domain::entity::image::Image}; + +#[async_trait] +pub trait ImageRepository: Send + Sync { + async fn save_image(&self, image: Image) -> Result; + async fn get_image_by_id(&self, id: i32) -> Result; +} diff --git a/backend/feature/image/src/application/use_case.rs b/backend/feature/image/src/application/use_case.rs new file mode 100644 index 0000000..ce677ef --- /dev/null +++ b/backend/feature/image/src/application/use_case.rs @@ -0,0 +1,2 @@ +pub mod get_image_use_case; +pub mod upload_image_use_case; diff --git a/backend/feature/image/src/application/use_case/get_image_use_case.rs b/backend/feature/image/src/application/use_case/get_image_use_case.rs new file mode 100644 index 0000000..7d85b26 --- /dev/null +++ b/backend/feature/image/src/application/use_case/get_image_use_case.rs @@ -0,0 +1,30 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + application::{error::image_error::ImageError, gateway::image_repository::ImageRepository}, + domain::entity::image::Image, +}; + +#[async_trait] +pub trait GetImageUseCase: Send + Sync { + async fn execute(&self, id: i32) -> Result; +} + +pub struct GetImageUseCaseImpl { + image_repository: Arc, +} + +impl GetImageUseCaseImpl { + pub fn new(image_repository: Arc) -> Self { + Self { image_repository } + } +} + +#[async_trait] +impl GetImageUseCase for GetImageUseCaseImpl { + async fn execute(&self, id: i32) -> Result { + self.image_repository.get_image_by_id(id).await + } +} diff --git a/backend/feature/image/src/application/use_case/upload_image_use_case.rs b/backend/feature/image/src/application/use_case/upload_image_use_case.rs new file mode 100644 index 0000000..d7273e6 --- /dev/null +++ b/backend/feature/image/src/application/use_case/upload_image_use_case.rs @@ -0,0 +1,28 @@ +use crate::{ + application::{error::image_error::ImageError, gateway::image_repository::ImageRepository}, + domain::entity::image::Image, +}; +use async_trait::async_trait; +use std::sync::Arc; + +#[async_trait] +pub trait UploadImageUseCase: Send + Sync { + async fn execute(&self, image: Image) -> Result; +} + +pub struct UploadImageUseCaseImpl { + image_repository: Arc, +} + +impl UploadImageUseCaseImpl { + pub fn new(image_repository: Arc) -> Self { + Self { image_repository } + } +} + +#[async_trait] +impl UploadImageUseCase for UploadImageUseCaseImpl { + async fn execute(&self, image: Image) -> Result { + self.image_repository.save_image(image).await + } +} diff --git a/backend/feature/image/src/domain.rs b/backend/feature/image/src/domain.rs new file mode 100644 index 0000000..bccca66 --- /dev/null +++ b/backend/feature/image/src/domain.rs @@ -0,0 +1 @@ +pub mod entity; \ No newline at end of file diff --git a/backend/feature/image/src/domain/entity.rs b/backend/feature/image/src/domain/entity.rs new file mode 100644 index 0000000..b1b6b4f --- /dev/null +++ b/backend/feature/image/src/domain/entity.rs @@ -0,0 +1 @@ +pub mod image; \ No newline at end of file diff --git a/backend/feature/image/src/domain/entity/image.rs b/backend/feature/image/src/domain/entity/image.rs new file mode 100644 index 0000000..5f2dde4 --- /dev/null +++ b/backend/feature/image/src/domain/entity/image.rs @@ -0,0 +1,5 @@ +pub struct Image { + pub id: Option, + pub mime_type: String, + pub data: Vec, +} diff --git a/backend/feature/image/src/framework.rs b/backend/feature/image/src/framework.rs new file mode 100644 index 0000000..a433099 --- /dev/null +++ b/backend/feature/image/src/framework.rs @@ -0,0 +1,3 @@ +pub mod db; +pub mod storage; +pub mod web; diff --git a/backend/feature/image/src/framework/db.rs b/backend/feature/image/src/framework/db.rs new file mode 100644 index 0000000..11ef5ed --- /dev/null +++ b/backend/feature/image/src/framework/db.rs @@ -0,0 +1,2 @@ +pub mod image_db_service_impl; +pub mod image_record; \ No newline at end of file diff --git a/backend/feature/image/src/framework/db/image_db_service_impl.rs b/backend/feature/image/src/framework/db/image_db_service_impl.rs new file mode 100644 index 0000000..bf3df3d --- /dev/null +++ b/backend/feature/image/src/framework/db/image_db_service_impl.rs @@ -0,0 +1,65 @@ +use async_trait::async_trait; +use sqlx::{Pool, Postgres}; + +use crate::{ + adapter::gateway::{image_db_mapper::ImageDbMapper, image_db_service::ImageDbService}, + application::error::image_error::ImageError, + framework::db::image_record::ImageRecord, +}; + +pub struct ImageDbServiceImpl { + db_pool: Pool, +} + +impl ImageDbServiceImpl { + pub fn new(db_pool: Pool) -> Self { + Self { db_pool } + } +} + +#[async_trait] +impl ImageDbService for ImageDbServiceImpl { + async fn create_image_info(&self, image: ImageDbMapper) -> Result { + let mime_type = image.mime_type.clone(); + let id = sqlx::query_scalar!( + r#" + INSERT INTO image (mime_type) + VALUES ($1) + RETURNING id + "#, + mime_type + ) + .fetch_one(&self.db_pool) + .await; + + match id { + Ok(id) => Ok(id), + Err(e) => Err(ImageError::DatabaseError(e.to_string())), + } + } + + async fn get_image_info_by_id(&self, id: i32) -> Result { + let image_record = sqlx::query_as!( + ImageRecord, + r#" + SELECT id, mime_type + FROM image + WHERE id = $1 AND deleted_time IS NULL + "#, + id + ) + .fetch_optional(&self.db_pool) + .await; + + match image_record { + Ok(record) => match record { + Some(record) => Ok(ImageDbMapper { + id: Some(record.id), + mime_type: record.mime_type, + }), + None => Err(ImageError::NotFound), + }, + Err(e) => Err(ImageError::DatabaseError(e.to_string())), + } + } +} diff --git a/backend/feature/image/src/framework/db/image_record.rs b/backend/feature/image/src/framework/db/image_record.rs new file mode 100644 index 0000000..b74bbab --- /dev/null +++ b/backend/feature/image/src/framework/db/image_record.rs @@ -0,0 +1,5 @@ +#[derive(sqlx::FromRow)] +pub struct ImageRecord { + pub id: i32, + pub mime_type: String, +} diff --git a/backend/feature/image/src/framework/storage.rs b/backend/feature/image/src/framework/storage.rs new file mode 100644 index 0000000..387b4a8 --- /dev/null +++ b/backend/feature/image/src/framework/storage.rs @@ -0,0 +1 @@ +pub mod image_storage_impl; diff --git a/backend/feature/image/src/framework/storage/image_storage_impl.rs b/backend/feature/image/src/framework/storage/image_storage_impl.rs new file mode 100644 index 0000000..eb7b61a --- /dev/null +++ b/backend/feature/image/src/framework/storage/image_storage_impl.rs @@ -0,0 +1,41 @@ +use std::{ + fs::{self, File}, + io::Write, +}; + +use crate::{ + adapter::gateway::image_storage::ImageStorage, application::error::image_error::ImageError, +}; + +pub struct ImageStorageImpl { + sotrage_path: String, +} + +impl ImageStorageImpl { + pub fn new(storage_path: &str) -> Self { + ImageStorageImpl { + sotrage_path: storage_path.to_string(), + } + } +} + +impl ImageStorage for ImageStorageImpl { + fn write_data(&self, id: i32, data: &[u8]) -> Result<(), ImageError> { + let dir_path = format!("{}/images", self.sotrage_path); + fs::create_dir_all(&dir_path).map_err(|e| ImageError::StorageError(e.to_string()))?; + + let file_path = format!("{}/{}", dir_path, id); + let mut file = + File::create(&file_path).map_err(|e| ImageError::StorageError(e.to_string()))?; + file.write_all(data) + .map_err(|e| ImageError::StorageError(e.to_string()))?; + + Ok(()) + } + + fn read_data(&self, id: i32) -> Result, ImageError> { + let file_path = format!("{}/images/{}", self.sotrage_path, id); + let data = fs::read(&file_path).map_err(|e| ImageError::StorageError(e.to_string()))?; + Ok(data) + } +} diff --git a/backend/feature/image/src/framework/web.rs b/backend/feature/image/src/framework/web.rs new file mode 100644 index 0000000..0de5a4e --- /dev/null +++ b/backend/feature/image/src/framework/web.rs @@ -0,0 +1 @@ +pub mod image_web_routes; \ No newline at end of file diff --git a/backend/feature/image/src/framework/web/image_web_routes.rs b/backend/feature/image/src/framework/web/image_web_routes.rs new file mode 100644 index 0000000..4d0c206 --- /dev/null +++ b/backend/feature/image/src/framework/web/image_web_routes.rs @@ -0,0 +1,89 @@ +use actix_multipart::Multipart; +use actix_web::{HttpResponse, Responder, web}; +use futures::StreamExt; + +use crate::{ + adapter::delivery::{image_controller::ImageController, image_request_dto::ImageRequestDto}, + application::error::image_error::ImageError, +}; + +pub fn configure_image_routes(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/image") + .route("/upload", web::post().to(upload_image_handler)) + .route("/{id}", web::get().to(get_image_by_id_handler)), + ); +} + +async fn upload_image_handler( + image_controller: web::Data, + mut payload: Multipart, +) -> impl Responder { + let mut image_request_dto: Option = None; + + while let Some(item) = payload.next().await { + let mut field = match item { + Ok(field) => field, + Err(_) => return HttpResponse::BadRequest().finish(), + }; + + if field.name() != Some("file") { + continue; + } + + let mime_type = field + .content_type() + .cloned() + .map(|mt| mt.to_string()) + .unwrap_or_else(|| "application/octet-stream".to_string()); + + let mut data = Vec::new(); + while let Some(chunk) = field.next().await { + match chunk { + Ok(bytes) => data.extend_from_slice(&bytes), + Err(_) => return HttpResponse::InternalServerError().finish(), + } + } + + image_request_dto = Some(ImageRequestDto { mime_type, data }); + break; + } + + let image_request_dto = match image_request_dto { + Some(dto) => dto, + None => return HttpResponse::BadRequest().finish(), + }; + let result = image_controller.upload_image(image_request_dto).await; + + match result { + Ok(image_info) => HttpResponse::Created().json(image_info), + Err(e) => match e { + ImageError::UnsupportedMimeType => HttpResponse::BadRequest().body(format!("{e:?}")), + _ => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + }, + } +} + +async fn get_image_by_id_handler( + image_controller: web::Data, + path: web::Path, +) -> impl Responder { + let id = path.into_inner(); + let result = image_controller.get_image_by_id(id).await; + + match result { + Ok(image_response) => HttpResponse::Ok() + .content_type(image_response.mime_type) + .body(image_response.data), + Err(e) => match e { + ImageError::NotFound => HttpResponse::NotFound().finish(), + _ => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + }, + } +} diff --git a/backend/feature/image/src/lib.rs b/backend/feature/image/src/lib.rs new file mode 100644 index 0000000..062f800 --- /dev/null +++ b/backend/feature/image/src/lib.rs @@ -0,0 +1,4 @@ +pub mod adapter; +pub mod application; +pub mod domain; +pub mod framework; diff --git a/backend/feature/post/src/adapter/gateway.rs b/backend/feature/post/src/adapter/gateway.rs index 254c048..38d8fbf 100644 --- a/backend/feature/post/src/adapter/gateway.rs +++ b/backend/feature/post/src/adapter/gateway.rs @@ -1,6 +1,6 @@ -pub mod color_mapper; -pub mod label_mapper; +pub mod color_db_mapper; +pub mod label_db_mapper; +pub mod post_db_mapper; pub mod post_db_service; -pub mod post_info_mapper; -pub mod post_mapper; +pub mod post_info_db_mapper; pub mod post_repository_impl; diff --git a/backend/feature/post/src/adapter/gateway/color_mapper.rs b/backend/feature/post/src/adapter/gateway/color_db_mapper.rs similarity index 89% rename from backend/feature/post/src/adapter/gateway/color_mapper.rs rename to backend/feature/post/src/adapter/gateway/color_db_mapper.rs index f472952..9143702 100644 --- a/backend/feature/post/src/adapter/gateway/color_mapper.rs +++ b/backend/feature/post/src/adapter/gateway/color_db_mapper.rs @@ -5,7 +5,7 @@ pub struct ColorMapper { } impl ColorMapper { - pub fn to_entity(&self) -> Color { + pub fn into_entity(self) -> Color { Color { red: (self.value >> 24) as u8, green: ((self.value >> 16) & 0xFF) as u8, diff --git a/backend/feature/post/src/adapter/gateway/label_db_mapper.rs b/backend/feature/post/src/adapter/gateway/label_db_mapper.rs new file mode 100644 index 0000000..6e7adf2 --- /dev/null +++ b/backend/feature/post/src/adapter/gateway/label_db_mapper.rs @@ -0,0 +1,17 @@ +use crate::{adapter::gateway::color_db_mapper::ColorMapper, domain::entity::label::Label}; + +pub struct LabelMapper { + pub id: i32, + pub name: String, + pub color: ColorMapper, +} + +impl LabelMapper { + pub fn into_entity(self) -> Label { + Label { + id: self.id, + name: self.name, + color: self.color.into_entity(), + } + } +} diff --git a/backend/feature/post/src/adapter/gateway/label_mapper.rs b/backend/feature/post/src/adapter/gateway/label_mapper.rs deleted file mode 100644 index a423237..0000000 --- a/backend/feature/post/src/adapter/gateway/label_mapper.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::{adapter::gateway::color_mapper::ColorMapper, domain::entity::label::Label}; - -pub struct LabelMapper { - pub id: i32, - pub name: String, - pub color: ColorMapper, -} - -impl LabelMapper { - pub fn to_entity(&self) -> Label { - Label { - id: self.id, - name: self.name.clone(), - color: self.color.to_entity(), - } - } -} diff --git a/backend/feature/post/src/adapter/gateway/post_db_mapper.rs b/backend/feature/post/src/adapter/gateway/post_db_mapper.rs new file mode 100644 index 0000000..7ccfe00 --- /dev/null +++ b/backend/feature/post/src/adapter/gateway/post_db_mapper.rs @@ -0,0 +1,17 @@ +use crate::{adapter::gateway::post_info_db_mapper::PostInfoMapper, domain::entity::post::Post}; + +pub struct PostMapper { + pub id: i32, + pub info: PostInfoMapper, + pub content: String, +} + +impl PostMapper { + pub fn into_entity(self) -> Post { + Post { + id: self.id, + info: self.info.into_entity(), + content: self.content, + } + } +} diff --git a/backend/feature/post/src/adapter/gateway/post_db_service.rs b/backend/feature/post/src/adapter/gateway/post_db_service.rs index 873e146..489a0b2 100644 --- a/backend/feature/post/src/adapter/gateway/post_db_service.rs +++ b/backend/feature/post/src/adapter/gateway/post_db_service.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use crate::{ - adapter::gateway::{post_info_mapper::PostInfoMapper, post_mapper::PostMapper}, + adapter::gateway::{post_info_db_mapper::PostInfoMapper, post_db_mapper::PostMapper}, application::error::post_error::PostError, }; diff --git a/backend/feature/post/src/adapter/gateway/post_info_mapper.rs b/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs similarity index 74% rename from backend/feature/post/src/adapter/gateway/post_info_mapper.rs rename to backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs index b8c5490..9b3ee32 100644 --- a/backend/feature/post/src/adapter/gateway/post_info_mapper.rs +++ b/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, NaiveDateTime, Utc}; -use crate::{adapter::gateway::label_mapper::LabelMapper, domain::entity::post_info::PostInfo}; +use crate::{adapter::gateway::label_db_mapper::LabelMapper, domain::entity::post_info::PostInfo}; pub struct PostInfoMapper { pub id: i32, @@ -12,7 +12,7 @@ pub struct PostInfoMapper { } impl PostInfoMapper { - pub fn to_entity(&self) -> PostInfo { + pub fn into_entity(self) -> PostInfo { PostInfo { id: self.id, title: self.title.clone(), @@ -21,7 +21,7 @@ impl PostInfoMapper { published_time: self .published_time .map(|dt| DateTime::::from_naive_utc_and_offset(dt, Utc)), - labels: self.labels.iter().map(LabelMapper::to_entity).collect(), + labels: self.labels.into_iter().map(LabelMapper::into_entity).collect(), } } } diff --git a/backend/feature/post/src/adapter/gateway/post_mapper.rs b/backend/feature/post/src/adapter/gateway/post_mapper.rs deleted file mode 100644 index 42da4e2..0000000 --- a/backend/feature/post/src/adapter/gateway/post_mapper.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::{adapter::gateway::post_info_mapper::PostInfoMapper, domain::entity::post::Post}; - -pub struct PostMapper { - pub id: i32, - pub info: PostInfoMapper, - pub content: String, -} - -impl PostMapper { - pub fn to_entity(&self) -> Post { - Post { - id: self.id, - info: self.info.to_entity(), - content: self.content.clone(), - } - } -} diff --git a/backend/feature/post/src/adapter/gateway/post_repository_impl.rs b/backend/feature/post/src/adapter/gateway/post_repository_impl.rs index 5c85d5b..6b9da9c 100644 --- a/backend/feature/post/src/adapter/gateway/post_repository_impl.rs +++ b/backend/feature/post/src/adapter/gateway/post_repository_impl.rs @@ -28,7 +28,7 @@ impl PostRepository for PostRepositoryImpl { .map(|mappers| { mappers .into_iter() - .map(|mapper| mapper.to_entity()) + .map(|mapper| mapper.into_entity()) .collect::>() }) } @@ -37,6 +37,6 @@ impl PostRepository for PostRepositoryImpl { self.post_db_service .get_full_post(id) .await - .map(|mapper| mapper.to_entity()) + .map(|mapper| mapper.into_entity()) } } diff --git a/backend/feature/post/src/framework/db/post_db_service_impl.rs b/backend/feature/post/src/framework/db/post_db_service_impl.rs index 77311c1..a6e3948 100644 --- a/backend/feature/post/src/framework/db/post_db_service_impl.rs +++ b/backend/feature/post/src/framework/db/post_db_service_impl.rs @@ -5,8 +5,8 @@ use sqlx::{Pool, Postgres}; use crate::{ adapter::gateway::{ - color_mapper::ColorMapper, label_mapper::LabelMapper, post_db_service::PostDbService, - post_info_mapper::PostInfoMapper, post_mapper::PostMapper, + color_db_mapper::ColorMapper, label_db_mapper::LabelMapper, post_db_service::PostDbService, + post_info_db_mapper::PostInfoMapper, post_db_mapper::PostMapper, }, application::error::post_error::PostError, }; diff --git a/backend/feature/post/src/framework/db/post_with_label_record.rs b/backend/feature/post/src/framework/db/post_with_label_record.rs index 4999304..20fcabe 100644 --- a/backend/feature/post/src/framework/db/post_with_label_record.rs +++ b/backend/feature/post/src/framework/db/post_with_label_record.rs @@ -1,6 +1,6 @@ use chrono::NaiveDateTime; -#[derive(sqlx::FromRow, Debug)] +#[derive(sqlx::FromRow)] pub struct PostWithLabelRecord { pub post_id: i32, pub title: String, diff --git a/backend/migrations/20250725231740_v0.3.0.sql b/backend/migrations/20250725231740_v0.3.0.sql new file mode 100644 index 0000000..8186a66 --- /dev/null +++ b/backend/migrations/20250725231740_v0.3.0.sql @@ -0,0 +1,12 @@ +CREATE TABLE "image" ( + "id" SERIAL PRIMARY KEY NOT NULL, + "mime_type" VARCHAR(100) NOT NULL, + "deleted_time" TIMESTAMP, + "created_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TRIGGER "update_image_updated_time" +BEFORE UPDATE ON "image" +FOR EACH ROW +EXECUTE FUNCTION update_updated_time_column(); diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index ad808e1..b8e4471 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -10,4 +10,5 @@ env_logger.workspace = true percent-encoding.workspace = true sqlx.workspace = true +image.workspace = true post.workspace = true diff --git a/backend/server/src/container.rs b/backend/server/src/container.rs index 05a88fe..552c60c 100644 --- a/backend/server/src/container.rs +++ b/backend/server/src/container.rs @@ -1,5 +1,18 @@ use std::sync::Arc; +use image::{ + adapter::{ + delivery::image_controller::{ImageController, ImageControllerImpl}, + gateway::image_repository_impl::ImageRepositoryImpl, + }, + application::use_case::{ + get_image_use_case::GetImageUseCaseImpl, upload_image_use_case::UploadImageUseCaseImpl, + }, + framework::{ + db::image_db_service_impl::ImageDbServiceImpl, + storage::image_storage_impl::ImageStorageImpl, + }, +}; use post::{ adapter::{ delivery::post_controller::{PostController, PostControllerImpl}, @@ -15,23 +28,37 @@ use sqlx::{Pool, Postgres}; pub struct Container { pub post_controller: Arc, + pub image_controller: Arc, } impl Container { - pub fn new(db_pool: Pool) -> Self { + pub fn new(db_pool: Pool, storage_path: &str) -> Self { let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone())); - let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone())); - let get_all_post_info_use_case = Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository.clone())); let get_full_post_use_case = Arc::new(GetFullPostUseCaseImpl::new(post_repository.clone())); - let post_controller = Arc::new(PostControllerImpl::new( get_all_post_info_use_case, get_full_post_use_case, )); - Self { post_controller } + let image_db_service = Arc::new(ImageDbServiceImpl::new(db_pool.clone())); + let image_storage = Arc::new(ImageStorageImpl::new(storage_path.into())); + let image_repository = Arc::new(ImageRepositoryImpl::new( + image_db_service.clone(), + image_storage.clone(), + )); + let upload_image_use_case = Arc::new(UploadImageUseCaseImpl::new(image_repository.clone())); + let get_image_use_case = Arc::new(GetImageUseCaseImpl::new(image_repository)); + let image_controller = Arc::new(ImageControllerImpl::new( + upload_image_use_case, + get_image_use_case, + )); + + Self { + post_controller, + image_controller, + } } } diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 92f93a2..2c63851 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -4,6 +4,7 @@ use actix_web::{ dev::{ServiceFactory, ServiceRequest, ServiceResponse}, web, }; +use image::framework::web::image_web_routes::configure_image_routes; use post::framework::web::post_web_routes::configure_post_routes; use server::container::Container; use sqlx::{Pool, Postgres, postgres::PgPoolOptions}; @@ -15,6 +16,7 @@ async fn main() -> std::io::Result<()> { env_logger::init(); let db_pool = init_database().await; + let storage_path = env::var("STORAGE_PATH").unwrap_or_else(|_| "static".to_string()); let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); let port = env::var("PORT") @@ -22,7 +24,7 @@ async fn main() -> std::io::Result<()> { .parse::() .unwrap(); - HttpServer::new(move || create_app(db_pool.clone())) + HttpServer::new(move || create_app(db_pool.clone(), storage_path.clone())) .bind((host, port))? .run() .await @@ -59,6 +61,7 @@ async fn init_database() -> Pool { fn create_app( db_pool: Pool, + storage_path: String, ) -> App< impl ServiceFactory< ServiceRequest, @@ -68,9 +71,11 @@ fn create_app( Error = Error, >, > { - let container = Container::new(db_pool); + let container = Container::new(db_pool, &storage_path); App::new() .app_data(web::Data::from(container.post_controller)) + .app_data(web::Data::from(container.image_controller)) .configure(configure_post_routes) + .configure(configure_image_routes) }