dogy_backend_api/service/assets/images/
handlers.rs1use std::sync::OnceLock;
2
3use axum::{
4 body::Bytes,
5 extract::{Multipart, Query},
6 Json,
7};
8use azure_storage::StorageCredentials;
9use azure_storage_blobs::prelude::ClientBuilder;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12use uuid::Uuid;
13
14use crate::config::load_config;
15
16#[derive(Debug, Serialize)]
18pub struct ImageUploadResponse {
19 pub url: String,
21 pub image_name: String,
23}
24
25#[derive(Debug, Deserialize)]
27pub struct ImageUploadRequest {
28 pub name: Option<String>,
30}
31
32#[derive(Debug, Deserialize)]
34pub struct ImageDeleteRequest {
35 pub name: String,
37}
38
39fn get_storage_credentials() -> &'static StorageCredentials {
44 static STORAGE_CREDENTIALS_INSTANCE: OnceLock<StorageCredentials> = OnceLock::new();
45
46 STORAGE_CREDENTIALS_INSTANCE.get_or_init(|| {
47 let config = load_config();
48 let account = &config.STORAGE_ACCOUNT;
49 let access_key = &config.STORAGE_ACCESS_KEY;
50 StorageCredentials::access_key(account, access_key.clone())
51 })
52}
53
54async fn upload_blob(blob_name: String, data: &Bytes, content_type: String) -> String {
55 let storage_credentials = get_storage_credentials();
56 let config = load_config();
57 let account = &config.STORAGE_ACCOUNT;
58 let container = &config.STORAGE_CONTAINER;
59 let _ = ClientBuilder::new(account, storage_credentials.to_owned())
60 .blob_client(container, &blob_name)
61 .put_block_blob(data.to_owned())
62 .content_type(content_type)
63 .await
64 .unwrap();
65
66 format!(
67 "https://{}.blob.core.windows.net/{}/{}",
68 account, container, blob_name
69 )
70}
71
72async fn delete_blob(blob_name: &str) {
73 let storage_credentials = get_storage_credentials();
74 let config = load_config();
75 let account = &config.STORAGE_ACCOUNT;
76 let container = &config.STORAGE_CONTAINER;
77 let _ = ClientBuilder::new(account, storage_credentials.to_owned())
78 .blob_client(container, blob_name)
79 .delete()
80 .await
81 .unwrap();
82}
83
84fn retrieve_base_name_from_file(custom_name: &Option<String>, filename: &str) -> String {
85 match custom_name {
86 Some(name) => name.replace(" ", "_"),
87 None => filename.replace(" ", "_"),
88 }
89}
90
91fn convert_to_blob_name(full_filename: &str, custom_name: &Option<String>) -> (String, String) {
92 let splitted_filename = full_filename
93 .split_once('.')
94 .unwrap_or((full_filename, ".jpg"));
95
96 let extension = splitted_filename.1;
97 let id = Uuid::new_v4().to_string();
98 let base_name = retrieve_base_name_from_file(custom_name, splitted_filename.0);
99
100 (format!("{}_{}.{}", base_name, id, extension), base_name)
101}
102
103pub async fn upload_image(
104 Query(img_req): Query<ImageUploadRequest>,
105 mut multipart: Multipart,
106) -> Json<ImageUploadResponse> {
107 let mut blob_url = String::from("");
108 let mut fallback_name = String::from("");
109
110 while let Some(field) = multipart.next_field().await.unwrap() {
113 let filename = field.file_name().unwrap().to_string();
114 let content_type = field.content_type().unwrap().to_string();
115 let data = field.bytes().await.unwrap();
116 let (blob_name, image_name) = convert_to_blob_name(&filename, &img_req.name);
117 blob_url = upload_blob(blob_name, &data, content_type).await;
118 fallback_name = image_name;
119
120 }
122
123 Json(ImageUploadResponse {
124 image_name: img_req.name.unwrap_or(fallback_name),
125 url: blob_url,
126 })
127}
128
129pub async fn delete_image(Query(img_req): Query<ImageDeleteRequest>) -> Json<Value> {
130 delete_blob(&img_req.name).await;
131
132 Json(json!({
133 "message": format!("Blob `{}` deleted successfully.", img_req.name)
134 }))
135}