Can download files
This commit is contained in:
		
							
								
								
									
										1
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -2098,6 +2098,7 @@ dependencies = [
 | 
			
		||||
 "diesel_migrations",
 | 
			
		||||
 "env_logger",
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "httpdate",
 | 
			
		||||
 "ipnet",
 | 
			
		||||
 "jwt-simple",
 | 
			
		||||
 "lazy-regex",
 | 
			
		||||
 
 | 
			
		||||
@@ -30,3 +30,4 @@ jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-
 | 
			
		||||
mime_guess = "2.0.5"
 | 
			
		||||
rust-embed = { version = "8.6.0" }
 | 
			
		||||
sha2 = "0.10.8"
 | 
			
		||||
httpdate = "1.0.3"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,14 @@
 | 
			
		||||
use crate::controllers::HttpResult;
 | 
			
		||||
use crate::extractors::auth_extractor::AuthExtractor;
 | 
			
		||||
use crate::extractors::file_extractor::FileExtractor;
 | 
			
		||||
use crate::extractors::file_id_extractor::FileIdExtractor;
 | 
			
		||||
use crate::models::files::File;
 | 
			
		||||
use crate::services::files_service;
 | 
			
		||||
use actix_web::HttpResponse;
 | 
			
		||||
use crate::utils::time_utils;
 | 
			
		||||
use actix_web::http::header;
 | 
			
		||||
use actix_web::{HttpRequest, HttpResponse};
 | 
			
		||||
use std::ops::Add;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
 | 
			
		||||
/// Upload a new file
 | 
			
		||||
pub async fn upload(auth: AuthExtractor, file: FileExtractor) -> HttpResult {
 | 
			
		||||
@@ -16,3 +22,39 @@ pub async fn upload(auth: AuthExtractor, file: FileExtractor) -> HttpResult {
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json(file))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Download an uploaded file
 | 
			
		||||
pub async fn download(req: HttpRequest, file_extractor: FileIdExtractor) -> HttpResult {
 | 
			
		||||
    serve_file(req, file_extractor.as_ref()).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)
 | 
			
		||||
            {
 | 
			
		||||
                return Ok(HttpResponse::NotModified().finish());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok()
 | 
			
		||||
        .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?))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ pub struct CreateTokenBody {
 | 
			
		||||
    right_account: bool,
 | 
			
		||||
    right_movement: bool,
 | 
			
		||||
    right_inbox: bool,
 | 
			
		||||
    right_attachment: bool,
 | 
			
		||||
    right_file: bool,
 | 
			
		||||
    right_auth: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -59,7 +59,7 @@ pub async fn create(auth: AuthExtractor, req: web::Json<CreateTokenBody>) -> Htt
 | 
			
		||||
        right_account: req.right_account,
 | 
			
		||||
        right_movement: req.right_movement,
 | 
			
		||||
        right_inbox: req.right_inbox,
 | 
			
		||||
        right_file: req.right_attachment,
 | 
			
		||||
        right_file: req.right_file,
 | 
			
		||||
        right_auth: req.right_auth,
 | 
			
		||||
    })
 | 
			
		||||
    .await?;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								moneymgr_backend/src/extractors/file_id_extractor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								moneymgr_backend/src/extractors/file_id_extractor.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
use crate::extractors::auth_extractor::AuthExtractor;
 | 
			
		||||
use crate::models::files::{File, FileID};
 | 
			
		||||
use crate::services::files_service;
 | 
			
		||||
use actix_web::dev::Payload;
 | 
			
		||||
use actix_web::{FromRequest, HttpRequest};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct FileIdInPath {
 | 
			
		||||
    file_id: FileID,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
enum FileIdExtractorError {
 | 
			
		||||
    #[error("Current user does not own the file!")]
 | 
			
		||||
    UserDoesNotOwnFile,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct FileIdExtractor(File);
 | 
			
		||||
 | 
			
		||||
impl FileIdExtractor {
 | 
			
		||||
    pub async fn load_file_from_path(auth: &AuthExtractor, id: FileID) -> anyhow::Result<Self> {
 | 
			
		||||
        let file = files_service::get_file_with_id(id)?;
 | 
			
		||||
 | 
			
		||||
        if file.user_id() != auth.user_id() {
 | 
			
		||||
            return Err(FileIdExtractorError::UserDoesNotOwnFile.into());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(Self(file))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FromRequest for FileIdExtractor {
 | 
			
		||||
    type Error = actix_web::Error;
 | 
			
		||||
    type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
 | 
			
		||||
        let req = req.clone();
 | 
			
		||||
        Box::pin(async move {
 | 
			
		||||
            let auth = AuthExtractor::extract(&req).await?;
 | 
			
		||||
 | 
			
		||||
            let file_id =
 | 
			
		||||
                actix_web::web::Path::<FileIdInPath>::from_request(&req, &mut Payload::None)
 | 
			
		||||
                    .await?
 | 
			
		||||
                    .file_id;
 | 
			
		||||
 | 
			
		||||
            Self::load_file_from_path(&auth, file_id)
 | 
			
		||||
                .await
 | 
			
		||||
                .map_err(|e| {
 | 
			
		||||
                    log::error!("Failed to extract file ID from URL! {}", e);
 | 
			
		||||
                    actix_web::error::ErrorNotFound("Could not fetch file information!")
 | 
			
		||||
                })
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AsRef<File> for FileIdExtractor {
 | 
			
		||||
    fn as_ref(&self) -> &File {
 | 
			
		||||
        &self.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
pub mod account_extractor;
 | 
			
		||||
pub mod auth_extractor;
 | 
			
		||||
pub mod file_extractor;
 | 
			
		||||
pub mod file_id_extractor;
 | 
			
		||||
pub mod money_session;
 | 
			
		||||
 
 | 
			
		||||
@@ -121,6 +121,11 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
            )
 | 
			
		||||
            // Files controller
 | 
			
		||||
            .route("/api/file", web::post().to(files_controller::upload))
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/file/{file_id}",
 | 
			
		||||
                web::get().to(files_controller::download),
 | 
			
		||||
            )
 | 
			
		||||
            // TODO Delete file
 | 
			
		||||
            // Static assets
 | 
			
		||||
            .route("/", web::get().to(static_controller::root_index))
 | 
			
		||||
            .route(
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ impl File {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn user_id(&self) -> UserID {
 | 
			
		||||
        UserID(self.id)
 | 
			
		||||
        UserID(self.user_id)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
//! # Time utilities
 | 
			
		||||
 | 
			
		||||
use std::time::{SystemTime, UNIX_EPOCH};
 | 
			
		||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
 | 
			
		||||
 | 
			
		||||
/// Get the current time since epoch
 | 
			
		||||
pub fn time() -> u64 {
 | 
			
		||||
@@ -9,3 +9,13 @@ pub fn time() -> u64 {
 | 
			
		||||
        .unwrap()
 | 
			
		||||
        .as_secs()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Format UNIX time to HTTP date
 | 
			
		||||
pub fn unix_to_system_time(time: u64) -> SystemTime {
 | 
			
		||||
    UNIX_EPOCH + Duration::from_secs(time)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Format UNIX time to HTTP date
 | 
			
		||||
pub fn unix_to_http_date(time: u64) -> String {
 | 
			
		||||
    httpdate::fmt_http_date(unix_to_system_time(time))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user