headless_lms_server/controllers/main_frontend/
page_audio_files.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/page_audio`.
2
3use std::path::Path;
4
5use futures::StreamExt;
6use models::page_audio_files::PageAudioFile;
7use utoipa::{OpenApi, ToSchema};
8
9use crate::prelude::*;
10
11#[derive(OpenApi)]
12#[openapi(paths(set_page_audio, remove_page_audio, get_page_audio))]
13pub(crate) struct MainFrontendPageAudioApiDoc;
14
15#[allow(dead_code)]
16#[derive(Debug, ToSchema)]
17struct PageAudioUploadPayload {
18    #[schema(content_media_type = "application/octet-stream")]
19    file: Vec<u8>,
20}
21
22/**
23POST `/api/v0/main-frontend/page_audio/:page_id` - Sets or updates the page audio.
24
25# Example
26
27Request:
28```http
29POST /api/v0/main-frontend/page_audio/d332f3d9-39a5-4a18-80f4-251727693c37 HTTP/1.1
30Content-Type: multipart/form-data
31
32BINARY_DATA
33```
34*/
35
36#[instrument(skip(request, payload, pool, file_store))]
37#[utoipa::path(
38    post,
39    path = "/{page_id}",
40    operation_id = "createPageAudioFile",
41    tag = "page_audio",
42    params(
43        ("page_id" = Uuid, Path, description = "Page id")
44    ),
45    request_body(content = inline(PageAudioUploadPayload), content_type = "multipart/form-data"),
46    responses(
47        (status = 200, description = "Page audio uploaded", body = bool)
48    )
49)]
50async fn set_page_audio(
51    request: HttpRequest,
52    mut payload: Multipart,
53    page_id: web::Path<Uuid>,
54    pool: web::Data<PgPool>,
55    user: AuthUser,
56    file_store: web::Data<dyn FileStore>,
57) -> ControllerResult<web::Json<bool>> {
58    let mut conn = pool.acquire().await?;
59    let page = models::pages::get_page(&mut conn, *page_id).await?;
60    if let Some(course_id) = page.course_id {
61        let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
62
63        let field = match payload.next().await {
64            Some(Ok(field)) => field,
65            Some(Err(error)) => {
66                return Err(ControllerError::new(
67                    ControllerErrorType::InternalServerError,
68                    error.to_string(),
69                    None,
70                ));
71            }
72            None => {
73                return Err(ControllerError::new(
74                    ControllerErrorType::BadRequest,
75                    "Didn't upload any files",
76                    None,
77                ));
78            }
79        };
80
81        let mime_type = field
82            .content_type()
83            .map(|ct| ct.to_string())
84            .unwrap_or_else(|| "".to_string());
85
86        match mime_type.as_str() {
87            "audio/mpeg" | "audio/ogg" => {}
88            unsupported => {
89                return Err(ControllerError::new(
90                    ControllerErrorType::BadRequest,
91                    format!("Unsupported audio Mime type: {}", unsupported),
92                    None,
93                ));
94            }
95        };
96
97        let course = models::courses::get_course(&mut conn, course_id).await?;
98        let media_path = upload_field_from_cms(
99            request.headers(),
100            field,
101            StoreKind::Course(course.id),
102            file_store.as_ref(),
103            &mut conn,
104            user,
105        )
106        .await?;
107
108        models::page_audio_files::insert_page_audio(
109            &mut conn,
110            page.id,
111            &media_path.as_path().to_string_lossy(),
112            &mime_type,
113        )
114        .await?;
115
116        token.authorized_ok(web::Json(true))
117    } else {
118        Err(ControllerError::new(
119            ControllerErrorType::BadRequest,
120            "The page needs to be related to a course.".to_string(),
121            None,
122        ))
123    }
124}
125
126/**
127DELETE `/api/v0/main-frontend/page_audio/:file_id` - Removes the chapter image.
128
129# Example
130
131Request:
132```http
133DELETE /api/v0/main-frontend/page_audio/d332f3d9-39a5-4a18-80f4-251727693c37 HTTP/1.1
134```
135*/
136
137#[instrument(skip(pool, file_store))]
138#[utoipa::path(
139    delete,
140    path = "/{file_id}",
141    operation_id = "deletePageAudioFile",
142    tag = "page_audio",
143    params(
144        ("file_id" = Uuid, Path, description = "Page audio file id")
145    ),
146    responses(
147        (status = 200, description = "Page audio deleted")
148    )
149)]
150async fn remove_page_audio(
151    file_id: web::Path<Uuid>,
152    pool: web::Data<PgPool>,
153    user: AuthUser,
154    file_store: web::Data<dyn FileStore>,
155) -> ControllerResult<web::Json<()>> {
156    let mut conn = pool.acquire().await?;
157    let audio = models::page_audio_files::get_page_audio_files_by_id(&mut conn, *file_id).await?;
158    let page = models::pages::get_page(&mut conn, audio.page_id).await?;
159    if let Some(course_id) = page.course_id {
160        let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
161
162        let path = models::page_audio_files::delete_page_audio(&mut conn, *file_id).await?;
163        file_store.delete(Path::new(&path)).await.map_err(|_| {
164            ControllerError::new(
165                ControllerErrorType::BadRequest,
166                "Could not delete the file from the file store".to_string(),
167                None,
168            )
169        })?;
170        token.authorized_ok(web::Json(()))
171    } else {
172        Err(ControllerError::new(
173            ControllerErrorType::BadRequest,
174            "The page needs to be related to a course.".to_string(),
175            None,
176        ))
177    }
178}
179
180/**
181GET `/api/v0/main-fronted/page_audio/:page_id/files` - Get a page audio files
182
183Request: `GET /api/v0/cms/page_audio/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02/files`
184*/
185#[instrument(skip(app_conf))]
186#[utoipa::path(
187    get,
188    path = "/{page_id}/files",
189    operation_id = "getPageAudioFiles",
190    tag = "page_audio",
191    params(
192        ("page_id" = Uuid, Path, description = "Page id")
193    ),
194    responses(
195        (status = 200, description = "Page audio files", body = Vec<PageAudioFile>)
196    )
197)]
198async fn get_page_audio(
199    page_id: web::Path<Uuid>,
200    pool: web::Data<PgPool>,
201    user: AuthUser,
202    app_conf: web::Data<ApplicationConfiguration>,
203) -> ControllerResult<web::Json<Vec<PageAudioFile>>> {
204    let mut conn = pool.acquire().await?;
205    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
206
207    let mut page_audio_files =
208        models::page_audio_files::get_page_audio_files(&mut conn, *page_id).await?;
209
210    let base_url = &app_conf.base_url;
211    for audio in page_audio_files.iter_mut() {
212        audio.path = format!("{base_url}/api/v0/files/{}", audio.path);
213    }
214
215    token.authorized_ok(web::Json(page_audio_files))
216}
217
218/**
219Add a route for each controller in this module.
220
221The name starts with an underline in order to appear before other functions in the module documentation.
222
223We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
224*/
225pub fn _add_routes(cfg: &mut ServiceConfig) {
226    cfg.route("/{page_id}", web::post().to(set_page_audio))
227        .route("/{file_id}", web::delete().to(remove_page_audio))
228        .route("/{page_id}/files", web::get().to(get_page_audio));
229}