headless_lms_server/controllers/
files.rs1use super::helpers::file_uploading;
6pub use crate::domain::{authorization::AuthorizationToken, models_requests::UploadClaim};
7use crate::prelude::*;
8use actix_files::NamedFile;
9use std::{collections::HashMap, path::Path};
10use tokio::fs::read;
11#[instrument(skip(file_store))]
39#[allow(clippy::async_yields_async)]
40async fn redirect_to_storage_service(
41 tail: web::Path<String>,
42 file_store: web::Data<dyn FileStore>,
43) -> HttpResponse {
44 let inner = tail.into_inner();
45 let tail_path = Path::new(&inner);
46
47 match file_store.get_direct_download_url(tail_path).await {
48 Ok(url) => HttpResponse::Found()
49 .append_header(("location", url))
50 .append_header(("cache-control", "max-age=300, private"))
51 .finish(),
52 Err(e) => {
53 error!("Could not get file {:?}", e);
54 HttpResponse::NotFound()
55 .append_header(("cache-control", "max-age=300, private"))
56 .finish()
57 }
58 }
59}
60
61#[instrument(skip(req))]
74async fn serve_upload(req: HttpRequest, pool: web::Data<PgPool>) -> ControllerResult<HttpResponse> {
75 let mut conn = pool.acquire().await?;
76
77 let base_folder = Path::new("uploads");
79 let relative_path = req.match_info().query("tail");
80 let path = base_folder.join(relative_path);
81
82 let named_file = NamedFile::open(path).map_err(|_e| {
83 ControllerError::new(
84 ControllerErrorType::NotFound,
85 "File not found".to_string(),
86 None,
87 )
88 })?;
89 let path = named_file.path();
90 let contents = read(path).await.map_err(|_e| {
91 ControllerError::new(
92 ControllerErrorType::InternalServerError,
93 "Could not read file".to_string(),
94 None,
95 )
96 })?;
97
98 let extension = path.extension().map(|o| o.to_string_lossy().to_string());
99 let mut mime_type = None;
100 if let Some(ext_string) = extension {
101 mime_type = match ext_string.as_str() {
102 "jpg" => Some("image/jpg"),
103 "png" => Some("image/png"),
104 "svg" => Some("image/svg+xml"),
105 "webp" => Some("image/webp"),
106 "gif" => Some("image/gif"),
107 _ => None,
108 };
109 }
110 let mut response = HttpResponse::Ok();
111 if let Some(m) = mime_type {
112 response.append_header(("content-type", m));
113 }
114 if let Some(filename) = models::file_uploads::get_filename(&mut conn, relative_path)
115 .await
116 .optional()?
117 {
118 response.append_header(("Content-Disposition", format!("filename=\"{}\"", filename)));
119 }
120
121 let token = skip_authorize();
123 token.authorized_ok(response.body(contents))
124}
125
126#[instrument(skip(payload, file_store, app_conf, upload_claim))]
134
135async fn upload_from_exercise_service(
136 pool: web::Data<PgPool>,
137 exercise_service_slug: web::Path<String>,
138 payload: Multipart,
139 file_store: web::Data<dyn FileStore>,
140 user: Option<AuthUser>,
141 upload_claim: Result<UploadClaim<'static>, ControllerError>,
142 app_conf: web::Data<ApplicationConfiguration>,
143) -> ControllerResult<web::Json<HashMap<String, String>>> {
144 let mut conn = pool.acquire().await?;
145 let token = skip_authorize();
148
149 if exercise_service_slug.as_str() != "playground" {
151 match (&upload_claim, &user) {
153 (Ok(upload_claim), _) => {
154 if upload_claim.exercise_service_slug() != exercise_service_slug.as_ref() {
155 return Err(ControllerError::new(
157 ControllerErrorType::BadRequest,
158 "Exercise service slug did not match upload claim".to_string(),
159 None,
160 ));
161 }
162 }
163 (_, Some(_user)) => {
164 }
166 (Err(_), None) => {
167 return Err(ControllerError::new(
168 ControllerErrorType::BadRequest,
169 "Not logged in or missing upload claim".to_string(),
170 None,
171 ));
172 }
173 }
174 }
175
176 let mut paths = HashMap::new();
177 if let Err(outer_err) = file_uploading::process_exercise_service_upload(
178 &mut conn,
179 exercise_service_slug.as_str(),
180 payload,
181 file_store.as_ref(),
182 &mut paths,
183 user,
184 &app_conf.base_url,
185 )
186 .await
187 {
188 for path in paths.values() {
190 if let Err(err) = file_store.delete(Path::new(path)).await {
191 error!("Failed to delete file '{path}' during cleanup: {err}")
192 }
193 }
194 return Err(outer_err);
195 }
196
197 token.authorized_ok(web::Json(paths))
198}
199
200pub fn _add_routes(cfg: &mut ServiceConfig) {
208 cfg.route("/uploads/{tail:.*}", web::get().to(serve_upload))
209 .route(
210 "/{exercise_service_slug}",
211 web::post().to(upload_from_exercise_service),
212 )
213 .route("{tail:.*}", web::get().to(redirect_to_storage_service));
214}