Skip to main content

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        if !models::page_audio_files::path_is_referenced(&mut conn, &path).await? {
164            file_store.delete(Path::new(&path)).await.map_err(|_| {
165                ControllerError::new(
166                    ControllerErrorType::BadRequest,
167                    "Could not delete the file from the file store".to_string(),
168                    None,
169                )
170            })?;
171        }
172        token.authorized_ok(web::Json(()))
173    } else {
174        Err(ControllerError::new(
175            ControllerErrorType::BadRequest,
176            "The page needs to be related to a course.".to_string(),
177            None,
178        ))
179    }
180}
181
182/**
183GET `/api/v0/main-fronted/page_audio/:page_id/files` - Get a page audio files
184
185Request: `GET /api/v0/cms/page_audio/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02/files`
186*/
187#[instrument(skip(app_conf))]
188#[utoipa::path(
189    get,
190    path = "/{page_id}/files",
191    operation_id = "getPageAudioFiles",
192    tag = "page_audio",
193    params(
194        ("page_id" = Uuid, Path, description = "Page id")
195    ),
196    responses(
197        (status = 200, description = "Page audio files", body = Vec<PageAudioFile>)
198    )
199)]
200async fn get_page_audio(
201    page_id: web::Path<Uuid>,
202    pool: web::Data<PgPool>,
203    user: AuthUser,
204    app_conf: web::Data<ApplicationConfiguration>,
205) -> ControllerResult<web::Json<Vec<PageAudioFile>>> {
206    let mut conn = pool.acquire().await?;
207    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
208
209    let mut page_audio_files =
210        models::page_audio_files::get_page_audio_files(&mut conn, *page_id).await?;
211
212    let base_url = &app_conf.base_url;
213    for audio in page_audio_files.iter_mut() {
214        audio.path = format!("{base_url}/api/v0/files/{}", audio.path);
215    }
216
217    token.authorized_ok(web::Json(page_audio_files))
218}
219
220/**
221Add a route for each controller in this module.
222
223The name starts with an underline in order to appear before other functions in the module documentation.
224
225We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
226*/
227pub fn _add_routes(cfg: &mut ServiceConfig) {
228    cfg.route("/{page_id}", web::post().to(set_page_audio))
229        .route("/{file_id}", web::delete().to(remove_page_audio))
230        .route("/{page_id}/files", web::get().to(get_page_audio));
231}