dogy_backend_api/service/assets/images/
handlers.rs

1use 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/// Response body after successfully uploaded an image.
17#[derive(Debug, Serialize)]
18pub struct ImageUploadResponse {
19    /// Azure blob URL after upload
20    pub url: String,
21    /// Human-readable image name
22    pub image_name: String,
23}
24
25/// Query parameter for uploading image
26#[derive(Debug, Deserialize)]
27pub struct ImageUploadRequest {
28    /// Optional custom blob name.
29    pub name: Option<String>,
30}
31
32/// Query parameter for deleting image
33#[derive(Debug, Deserialize)]
34pub struct ImageDeleteRequest {
35    /// The blob name of the blob that you want to delete.
36    pub name: String,
37}
38
39/// Retrieves azure blob storage credentials and caches it.
40///
41/// Note that we need to return the storage credential instead of the [`ClientBuilder`]
42/// as the request or more specifically the blob name and file content changes per request.
43fn 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    // Iterate through all of the fields of FormData. However, we only really expect one field
111    // which is the file itself.
112    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        //dbg!(&filename);
121    }
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}