From cfbc003737c2e4dd21e508e328bad90b99cfc8b1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 1 May 2025 19:09:43 +0200 Subject: [PATCH] Can force file download --- .../src/controllers/files_controller.rs | 66 ++++++++++++------- moneymgr_web/src/api/FileApi.ts | 7 +- moneymgr_web/src/dialogs/FileViewerDialog.tsx | 4 +- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/moneymgr_backend/src/controllers/files_controller.rs b/moneymgr_backend/src/controllers/files_controller.rs index 2216d6e..d0f657b 100644 --- a/moneymgr_backend/src/controllers/files_controller.rs +++ b/moneymgr_backend/src/controllers/files_controller.rs @@ -7,7 +7,7 @@ use crate::models::files::File; use crate::services::files_service; use crate::utils::time_utils; use actix_web::http::header; -use actix_web::{HttpRequest, HttpResponse}; +use actix_web::{HttpRequest, HttpResponse, web}; use std::ops::Add; use std::time::Duration; @@ -44,40 +44,60 @@ pub async fn get_info(file_extractor: FileIdExtractor) -> HttpResult { Ok(HttpResponse::Ok().json(file_extractor.as_ref())) } +#[derive(serde::Deserialize)] +pub struct DownloadQuery { + #[serde(default)] + download: bool, +} + /// Download an uploaded file -pub async fn download(req: HttpRequest, file_extractor: FileIdExtractor) -> HttpResult { - serve_file(req, file_extractor.as_ref()).await +pub async fn download( + req: HttpRequest, + file_extractor: FileIdExtractor, + query: web::Query, +) -> HttpResult { + serve_file(req, file_extractor.as_ref(), query.download).await } /// Serve a file, returning 304 status code if the requested file already exists -pub async fn serve_file(req: HttpRequest, file: &File) -> HttpResult { - // Check if the browser already knows the etag - if let Some(c) = req.headers().get(header::IF_NONE_MATCH) { - if c.to_str().unwrap_or("") == file.sha512.as_str() { - return Ok(HttpResponse::NotModified().finish()); - } - } - - // Check if the browser already knows the file by date - if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) { - let date_str = c.to_str().unwrap_or(""); - if let Ok(date) = httpdate::parse_http_date(date_str) { - if date.add(Duration::from_secs(1)) - >= time_utils::unix_to_system_time(file.time_create as u64) - { +pub async fn serve_file(req: HttpRequest, file: &File, download_file: bool) -> HttpResult { + if !download_file { + // Check if the browser already knows the etag + if let Some(c) = req.headers().get(header::IF_NONE_MATCH) { + if c.to_str().unwrap_or("") == file.sha512.as_str() { return Ok(HttpResponse::NotModified().finish()); } } - } - Ok(HttpResponse::Ok() - .content_type(file.mime_type.as_str()) + // Check if the browser already knows the file by date + if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) { + let date_str = c.to_str().unwrap_or(""); + if let Ok(date) = httpdate::parse_http_date(date_str) { + if date.add(Duration::from_secs(1)) + >= time_utils::unix_to_system_time(file.time_create as u64) + { + return Ok(HttpResponse::NotModified().finish()); + } + } + } + } + let mut res = HttpResponse::Ok(); + res.content_type(file.mime_type.as_str()) .insert_header(("etag", file.sha512.as_str())) .insert_header(( "last-modified", time_utils::unix_to_http_date(file.time_create as u64), - )) - .body(files_service::get_file_content(file).await?)) + )); + + // Add filename to response headers if requested + if download_file { + res.insert_header(( + "content-disposition", + format!("attachment; filename={}", file.file_name), + )); + } + + Ok(res.body(files_service::get_file_content(file).await?)) } /// Delete an uploaded file diff --git a/moneymgr_web/src/api/FileApi.ts b/moneymgr_web/src/api/FileApi.ts index e9524e0..fef86c0 100644 --- a/moneymgr_web/src/api/FileApi.ts +++ b/moneymgr_web/src/api/FileApi.ts @@ -41,7 +41,10 @@ export class FileApi { /** * Get a file download URL */ - static DownloadURL(file: UploadedFile): string { - return APIClient.backendURL() + `/file/${file.id}/download`; + static DownloadURL(file: UploadedFile, forceDownload = false): string { + return ( + APIClient.backendURL() + + `/file/${file.id}/download${forceDownload ? "?download=true" : ""}` + ); } } diff --git a/moneymgr_web/src/dialogs/FileViewerDialog.tsx b/moneymgr_web/src/dialogs/FileViewerDialog.tsx index 331b9d8..bdf2d44 100644 --- a/moneymgr_web/src/dialogs/FileViewerDialog.tsx +++ b/moneymgr_web/src/dialogs/FileViewerDialog.tsx @@ -25,6 +25,7 @@ export function FileViewerDialog(p: { fileName={p.file.file_name} fileSize={p.file.file_size} url={FileApi.DownloadURL(p.file)} + downloadUrl={FileApi.DownloadURL(p.file, true)} mimetype={p.file.mime_type} /> @@ -38,6 +39,7 @@ interface ViewerProps { fileName: string; fileSize: number; url: string; + downloadUrl: string; mimetype: string; } @@ -65,7 +67,7 @@ function DefaultViewer(p: ViewerProps): React.ReactElement { {filesize(p.fileSize)} - +