BLOG-78 Backend image upload and download #84
181
backend/Cargo.lock
generated
181
backend/Cargo.lock
generated
@ -34,7 +34,7 @@ dependencies = [
|
|||||||
"brotli",
|
"brotli",
|
||||||
"bytes",
|
"bytes",
|
||||||
"bytestring",
|
"bytestring",
|
||||||
"derive_more",
|
"derive_more 2.0.1",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"flate2",
|
"flate2",
|
||||||
"foldhash",
|
"foldhash",
|
||||||
@ -68,6 +68,44 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@ -149,7 +187,7 @@ dependencies = [
|
|||||||
"bytestring",
|
"bytestring",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cookie",
|
"cookie",
|
||||||
"derive_more",
|
"derive_more 2.0.1",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"foldhash",
|
"foldhash",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -466,6 +504,12 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
@ -541,6 +585,41 @@ dependencies = [
|
|||||||
"typenum",
|
"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]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@ -561,6 +640,19 @@ dependencies = [
|
|||||||
"powerfmt",
|
"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]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@ -744,6 +836,21 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"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]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@ -788,6 +895,17 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
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]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@ -806,8 +924,10 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -1098,6 +1218,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@ -1119,6 +1245,20 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"actix-multipart",
|
||||||
|
"actix-web",
|
||||||
|
"async-trait",
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"sqlx",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "impl-more"
|
name = "impl-more"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@ -1413,6 +1553,12 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "pem-rfc7468"
|
name = "pem-rfc7468"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -1676,6 +1822,15 @@ version = "0.1.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
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]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -1738,6 +1893,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.219"
|
version = "1.0.219"
|
||||||
@ -1770,6 +1931,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_plain"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -1789,6 +1959,7 @@ dependencies = [
|
|||||||
"actix-web",
|
"actix-web",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"image",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"post",
|
"post",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@ -2100,6 +2271,12 @@ dependencies = [
|
|||||||
"unicode-properties",
|
"unicode-properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["feature/post", "server"]
|
members = ["server", "feature/image", "feature/post"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@ -7,6 +7,7 @@ version = "0.2.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
actix-multipart = "0.7.2"
|
||||||
actix-web = "4.10.2"
|
actix-web = "4.10.2"
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
@ -25,4 +26,5 @@ sqlx = { version = "0.8.5", features = [
|
|||||||
tokio = { version = "1.45.0", features = ["full"] }
|
tokio = { version = "1.45.0", features = ["full"] }
|
||||||
|
|
||||||
server.path = "server"
|
server.path = "server"
|
||||||
|
image.path = "feature/image"
|
||||||
post.path = "feature/post"
|
post.path = "feature/post"
|
||||||
|
@ -11,9 +11,11 @@ FROM alpine:latest AS runner
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/server .
|
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/server .
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
VOLUME ["/app/static"]
|
||||||
ENV RUST_LOG=info
|
ENV RUST_LOG=info
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
|
ENV STORAGE_PATH=/app/static
|
||||||
ENV DATABASE_HOST=127.0.0.1
|
ENV DATABASE_HOST=127.0.0.1
|
||||||
ENV DATABASE_PORT=5432
|
ENV DATABASE_PORT=5432
|
||||||
ENV DATABASE_USER=postgres
|
ENV DATABASE_USER=postgres
|
||||||
|
@ -39,4 +39,3 @@
|
|||||||
```bash
|
```bash
|
||||||
RUST_LOG=debug watchexec -e rs -r 'cargo run'
|
RUST_LOG=debug watchexec -e rs -r 'cargo run'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
14
backend/feature/image/Cargo.toml
Normal file
14
backend/feature/image/Cargo.toml
Normal file
@ -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
|
2
backend/feature/image/src/adapter.rs
Normal file
2
backend/feature/image/src/adapter.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod delivery;
|
||||||
|
pub mod gateway;
|
4
backend/feature/image/src/adapter/delivery.rs
Normal file
4
backend/feature/image/src/adapter/delivery.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod image_controller;
|
||||||
|
pub mod image_info_response_dto;
|
||||||
|
pub mod image_request_dto;
|
||||||
|
pub mod image_response_dto;
|
@ -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<ImageInfoResponseDto, ImageError>;
|
||||||
|
|
||||||
|
async fn get_image_by_id(&self, id: i32) -> Result<ImageResponseDto, ImageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImageControllerImpl {
|
||||||
|
upload_image_use_case: Arc<dyn UploadImageUseCase>,
|
||||||
|
get_image_use_case: Arc<dyn GetImageUseCase>,
|
||||||
|
|
||||||
|
mime_type_whitelist: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageControllerImpl {
|
||||||
|
pub fn new(
|
||||||
|
upload_image_use_case: Arc<dyn UploadImageUseCase>,
|
||||||
|
get_image_use_case: Arc<dyn GetImageUseCase>,
|
||||||
|
) -> 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<ImageInfoResponseDto, ImageError> {
|
||||||
|
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<ImageResponseDto, ImageError> {
|
||||||
|
let image = self.get_image_use_case.execute(id).await?;
|
||||||
|
Ok(ImageResponseDto {
|
||||||
|
id: id,
|
||||||
|
mime_type: image.mime_type,
|
||||||
|
data: image.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ImageInfoResponseDto {
|
||||||
|
pub id: i32,
|
||||||
|
pub mime_type: String,
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
use crate::domain::entity::image::Image;
|
||||||
|
|
||||||
|
pub struct ImageRequestDto {
|
||||||
|
pub mime_type: String,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageRequestDto {
|
||||||
|
pub fn into_entity(self) -> Image {
|
||||||
|
Image {
|
||||||
|
id: None,
|
||||||
|
mime_type: self.mime_type,
|
||||||
|
data: self.data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
pub struct ImageResponseDto {
|
||||||
|
pub id: i32,
|
||||||
|
pub mime_type: String,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
4
backend/feature/image/src/adapter/gateway.rs
Normal file
4
backend/feature/image/src/adapter/gateway.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod image_db_service;
|
||||||
|
pub mod image_db_mapper;
|
||||||
|
pub mod image_repository_impl;
|
||||||
|
pub mod image_storage;
|
25
backend/feature/image/src/adapter/gateway/image_db_mapper.rs
Normal file
25
backend/feature/image/src/adapter/gateway/image_db_mapper.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use crate::domain::entity::image::Image;
|
||||||
|
|
||||||
|
pub struct ImageDbMapper {
|
||||||
|
pub id: Option<i32>,
|
||||||
|
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<Image> for ImageDbMapper {
|
||||||
|
fn from(image: Image) -> Self {
|
||||||
|
ImageDbMapper {
|
||||||
|
id: image.id,
|
||||||
|
mime_type: image.mime_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<i32, ImageError>;
|
||||||
|
async fn get_image_info_by_id(&self, id: i32) -> Result<ImageDbMapper, ImageError>;
|
||||||
|
}
|
@ -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<dyn ImageDbService>,
|
||||||
|
image_storage: Arc<dyn ImageStorage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageRepositoryImpl {
|
||||||
|
pub fn new(
|
||||||
|
image_db_service: Arc<dyn ImageDbService>,
|
||||||
|
image_storage: Arc<dyn ImageStorage>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
image_db_service,
|
||||||
|
image_storage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ImageRepository for ImageRepositoryImpl {
|
||||||
|
async fn save_image(&self, image: Image) -> Result<i32, ImageError> {
|
||||||
|
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<Image, ImageError> {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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<Vec<u8>, ImageError>;
|
||||||
|
}
|
3
backend/feature/image/src/application.rs
Normal file
3
backend/feature/image/src/application.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod error;
|
||||||
|
pub mod gateway;
|
||||||
|
pub mod use_case;
|
1
backend/feature/image/src/application/error.rs
Normal file
1
backend/feature/image/src/application/error.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod image_error;
|
@ -0,0 +1,7 @@
|
|||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum ImageError {
|
||||||
|
DatabaseError(String),
|
||||||
|
StorageError(String),
|
||||||
|
NotFound,
|
||||||
|
UnsupportedMimeType,
|
||||||
|
}
|
1
backend/feature/image/src/application/gateway.rs
Normal file
1
backend/feature/image/src/application/gateway.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod image_repository;
|
@ -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<i32, ImageError>;
|
||||||
|
async fn get_image_by_id(&self, id: i32) -> Result<Image, ImageError>;
|
||||||
|
}
|
2
backend/feature/image/src/application/use_case.rs
Normal file
2
backend/feature/image/src/application/use_case.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod get_image_use_case;
|
||||||
|
pub mod upload_image_use_case;
|
@ -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<Image, ImageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GetImageUseCaseImpl {
|
||||||
|
image_repository: Arc<dyn ImageRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetImageUseCaseImpl {
|
||||||
|
pub fn new(image_repository: Arc<dyn ImageRepository>) -> Self {
|
||||||
|
Self { image_repository }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl GetImageUseCase for GetImageUseCaseImpl {
|
||||||
|
async fn execute(&self, id: i32) -> Result<Image, ImageError> {
|
||||||
|
self.image_repository.get_image_by_id(id).await
|
||||||
|
}
|
||||||
|
}
|
@ -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<i32, ImageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UploadImageUseCaseImpl {
|
||||||
|
image_repository: Arc<dyn ImageRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UploadImageUseCaseImpl {
|
||||||
|
pub fn new(image_repository: Arc<dyn ImageRepository>) -> Self {
|
||||||
|
Self { image_repository }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UploadImageUseCase for UploadImageUseCaseImpl {
|
||||||
|
async fn execute(&self, image: Image) -> Result<i32, ImageError> {
|
||||||
|
self.image_repository.save_image(image).await
|
||||||
|
}
|
||||||
|
}
|
1
backend/feature/image/src/domain.rs
Normal file
1
backend/feature/image/src/domain.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod entity;
|
1
backend/feature/image/src/domain/entity.rs
Normal file
1
backend/feature/image/src/domain/entity.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod image;
|
5
backend/feature/image/src/domain/entity/image.rs
Normal file
5
backend/feature/image/src/domain/entity/image.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub struct Image {
|
||||||
|
pub id: Option<i32>,
|
||||||
|
pub mime_type: String,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
3
backend/feature/image/src/framework.rs
Normal file
3
backend/feature/image/src/framework.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod db;
|
||||||
|
pub mod storage;
|
||||||
|
pub mod web;
|
2
backend/feature/image/src/framework/db.rs
Normal file
2
backend/feature/image/src/framework/db.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod image_db_service_impl;
|
||||||
|
pub mod image_record;
|
@ -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<Postgres>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageDbServiceImpl {
|
||||||
|
pub fn new(db_pool: Pool<Postgres>) -> Self {
|
||||||
|
Self { db_pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ImageDbService for ImageDbServiceImpl {
|
||||||
|
async fn create_image_info(&self, image: ImageDbMapper) -> Result<i32, ImageError> {
|
||||||
|
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<ImageDbMapper, ImageError> {
|
||||||
|
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())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
backend/feature/image/src/framework/db/image_record.rs
Normal file
5
backend/feature/image/src/framework/db/image_record.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct ImageRecord {
|
||||||
|
pub id: i32,
|
||||||
|
pub mime_type: String,
|
||||||
|
}
|
1
backend/feature/image/src/framework/storage.rs
Normal file
1
backend/feature/image/src/framework/storage.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod image_storage_impl;
|
@ -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<Vec<u8>, 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)
|
||||||
|
}
|
||||||
|
}
|
1
backend/feature/image/src/framework/web.rs
Normal file
1
backend/feature/image/src/framework/web.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod image_web_routes;
|
89
backend/feature/image/src/framework/web/image_web_routes.rs
Normal file
89
backend/feature/image/src/framework/web/image_web_routes.rs
Normal file
@ -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<dyn ImageController>,
|
||||||
|
mut payload: Multipart,
|
||||||
|
) -> impl Responder {
|
||||||
|
let mut image_request_dto: Option<ImageRequestDto> = 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<dyn ImageController>,
|
||||||
|
path: web::Path<i32>,
|
||||||
|
) -> 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()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
4
backend/feature/image/src/lib.rs
Normal file
4
backend/feature/image/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod adapter;
|
||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod framework;
|
@ -1,6 +1,6 @@
|
|||||||
pub mod color_mapper;
|
pub mod color_db_mapper;
|
||||||
pub mod label_mapper;
|
pub mod label_db_mapper;
|
||||||
|
pub mod post_db_mapper;
|
||||||
pub mod post_db_service;
|
pub mod post_db_service;
|
||||||
pub mod post_info_mapper;
|
pub mod post_info_db_mapper;
|
||||||
pub mod post_mapper;
|
|
||||||
pub mod post_repository_impl;
|
pub mod post_repository_impl;
|
||||||
|
@ -5,7 +5,7 @@ pub struct ColorMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ColorMapper {
|
impl ColorMapper {
|
||||||
pub fn to_entity(&self) -> Color {
|
pub fn into_entity(self) -> Color {
|
||||||
Color {
|
Color {
|
||||||
red: (self.value >> 24) as u8,
|
red: (self.value >> 24) as u8,
|
||||||
green: ((self.value >> 16) & 0xFF) as u8,
|
green: ((self.value >> 16) & 0xFF) as u8,
|
17
backend/feature/post/src/adapter/gateway/label_db_mapper.rs
Normal file
17
backend/feature/post/src/adapter/gateway/label_db_mapper.rs
Normal file
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
17
backend/feature/post/src/adapter/gateway/post_db_mapper.rs
Normal file
17
backend/feature/post/src/adapter/gateway/post_db_mapper.rs
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::{
|
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,
|
application::error::post_error::PostError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
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 struct PostInfoMapper {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@ -12,7 +12,7 @@ pub struct PostInfoMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PostInfoMapper {
|
impl PostInfoMapper {
|
||||||
pub fn to_entity(&self) -> PostInfo {
|
pub fn into_entity(self) -> PostInfo {
|
||||||
PostInfo {
|
PostInfo {
|
||||||
id: self.id,
|
id: self.id,
|
||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
@ -21,7 +21,7 @@ impl PostInfoMapper {
|
|||||||
published_time: self
|
published_time: self
|
||||||
.published_time
|
.published_time
|
||||||
.map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)),
|
.map(|dt| DateTime::<Utc>::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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -28,7 +28,7 @@ impl PostRepository for PostRepositoryImpl {
|
|||||||
.map(|mappers| {
|
.map(|mappers| {
|
||||||
mappers
|
mappers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mapper| mapper.to_entity())
|
.map(|mapper| mapper.into_entity())
|
||||||
.collect::<Vec<PostInfo>>()
|
.collect::<Vec<PostInfo>>()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -37,6 +37,6 @@ impl PostRepository for PostRepositoryImpl {
|
|||||||
self.post_db_service
|
self.post_db_service
|
||||||
.get_full_post(id)
|
.get_full_post(id)
|
||||||
.await
|
.await
|
||||||
.map(|mapper| mapper.to_entity())
|
.map(|mapper| mapper.into_entity())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ use sqlx::{Pool, Postgres};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
adapter::gateway::{
|
adapter::gateway::{
|
||||||
color_mapper::ColorMapper, label_mapper::LabelMapper, post_db_service::PostDbService,
|
color_db_mapper::ColorMapper, label_db_mapper::LabelMapper, post_db_service::PostDbService,
|
||||||
post_info_mapper::PostInfoMapper, post_mapper::PostMapper,
|
post_info_db_mapper::PostInfoMapper, post_db_mapper::PostMapper,
|
||||||
},
|
},
|
||||||
application::error::post_error::PostError,
|
application::error::post_error::PostError,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Debug)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub struct PostWithLabelRecord {
|
pub struct PostWithLabelRecord {
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
12
backend/migrations/20250725231740_v0.3.0.sql
Normal file
12
backend/migrations/20250725231740_v0.3.0.sql
Normal file
@ -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();
|
@ -10,4 +10,5 @@ env_logger.workspace = true
|
|||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
|
||||||
|
image.workspace = true
|
||||||
post.workspace = true
|
post.workspace = true
|
||||||
|
@ -1,5 +1,18 @@
|
|||||||
use std::sync::Arc;
|
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::{
|
use post::{
|
||||||
adapter::{
|
adapter::{
|
||||||
delivery::post_controller::{PostController, PostControllerImpl},
|
delivery::post_controller::{PostController, PostControllerImpl},
|
||||||
@ -15,23 +28,37 @@ use sqlx::{Pool, Postgres};
|
|||||||
|
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
pub post_controller: Arc<dyn PostController>,
|
pub post_controller: Arc<dyn PostController>,
|
||||||
|
pub image_controller: Arc<dyn ImageController>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Container {
|
impl Container {
|
||||||
pub fn new(db_pool: Pool<Postgres>) -> Self {
|
pub fn new(db_pool: Pool<Postgres>, storage_path: &str) -> Self {
|
||||||
let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone()));
|
let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone()));
|
||||||
|
|
||||||
let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone()));
|
let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone()));
|
||||||
|
|
||||||
let get_all_post_info_use_case =
|
let get_all_post_info_use_case =
|
||||||
Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository.clone()));
|
Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository.clone()));
|
||||||
let get_full_post_use_case = Arc::new(GetFullPostUseCaseImpl::new(post_repository.clone()));
|
let get_full_post_use_case = Arc::new(GetFullPostUseCaseImpl::new(post_repository.clone()));
|
||||||
|
|
||||||
let post_controller = Arc::new(PostControllerImpl::new(
|
let post_controller = Arc::new(PostControllerImpl::new(
|
||||||
get_all_post_info_use_case,
|
get_all_post_info_use_case,
|
||||||
get_full_post_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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ use actix_web::{
|
|||||||
dev::{ServiceFactory, ServiceRequest, ServiceResponse},
|
dev::{ServiceFactory, ServiceRequest, ServiceResponse},
|
||||||
web,
|
web,
|
||||||
};
|
};
|
||||||
|
use image::framework::web::image_web_routes::configure_image_routes;
|
||||||
use post::framework::web::post_web_routes::configure_post_routes;
|
use post::framework::web::post_web_routes::configure_post_routes;
|
||||||
use server::container::Container;
|
use server::container::Container;
|
||||||
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
|
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
|
||||||
@ -15,6 +16,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let db_pool = init_database().await;
|
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 host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||||||
let port = env::var("PORT")
|
let port = env::var("PORT")
|
||||||
@ -22,7 +24,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
HttpServer::new(move || create_app(db_pool.clone()))
|
HttpServer::new(move || create_app(db_pool.clone(), storage_path.clone()))
|
||||||
.bind((host, port))?
|
.bind((host, port))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
@ -59,6 +61,7 @@ async fn init_database() -> Pool<Postgres> {
|
|||||||
|
|
||||||
fn create_app(
|
fn create_app(
|
||||||
db_pool: Pool<Postgres>,
|
db_pool: Pool<Postgres>,
|
||||||
|
storage_path: String,
|
||||||
) -> App<
|
) -> App<
|
||||||
impl ServiceFactory<
|
impl ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
@ -68,9 +71,11 @@ fn create_app(
|
|||||||
Error = Error,
|
Error = Error,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let container = Container::new(db_pool);
|
let container = Container::new(db_pool, &storage_path);
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::from(container.post_controller))
|
.app_data(web::Data::from(container.post_controller))
|
||||||
|
.app_data(web::Data::from(container.image_controller))
|
||||||
.configure(configure_post_routes)
|
.configure(configure_post_routes)
|
||||||
|
.configure(configure_image_routes)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user