use std::{fs::File, os::unix::fs::MetadataExt, time::SystemTime}; use actix_web::{ body::{self, BoxBody, SizedStream}, http::{ header::{ self, ContentDisposition, DispositionParam, DispositionType, EntityTag, HeaderValue, HttpDate, IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince, }, StatusCode, }, HttpMessage, HttpRequest, HttpResponse, Responder, }; use actix_files::HttpRange; use crate::DownloadableFile; // This is copied substantially from actix-files, with some tweaks pub(crate) struct DownloadingFile { pub(crate) file: File, pub(crate) info: DownloadableFile, } impl DownloadingFile { fn etag(&self) -> EntityTag { let ino = self.file.metadata().map(|md| md.ino()).unwrap_or_default(); EntityTag::new_strong(format!( "{:x}:{:x}:{:x}:{:x}", ino, self.info.size, self.info.modtime.unix_timestamp() as u64, self.info.modtime.nanosecond(), )) } /// Creates an `HttpResponse` with file as a streaming body. pub fn into_response(self, req: &HttpRequest) -> HttpResponse { let etag = self.etag(); let last_modified = HttpDate::from(SystemTime::from(self.info.modtime)); let precondition_failed = precondition_failed(req, &etag, &last_modified); let not_modified = not_modified(req, &etag, &last_modified); let mut res = HttpResponse::build(StatusCode::OK); res.insert_header((header::CONTENT_SECURITY_POLICY, "sandbox")); res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); res.insert_header(( header::CONTENT_DISPOSITION, ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename(self.info.name)], }, )); res.insert_header((header::LAST_MODIFIED, last_modified)); res.insert_header((header::ETAG, etag)); res.insert_header((header::ACCEPT_RANGES, "bytes")); let mut length = self.info.size; let mut offset = 0; // check for range header if let Some(ranges) = req.headers().get(header::RANGE) { if let Ok(ranges_header) = ranges.to_str() { if let Ok(ranges) = HttpRange::parse(ranges_header, length) { length = ranges[0].length; offset = ranges[0].start; // don't allow compression middleware to modify partial content res.insert_header(( header::CONTENT_ENCODING, HeaderValue::from_static("identity"), )); res.insert_header(( header::CONTENT_RANGE, format!( "bytes {}-{}/{}", offset, offset + length - 1, self.info.size ), )); } else { res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); }; } else { return res.status(StatusCode::BAD_REQUEST).finish(); }; }; if precondition_failed { return res.status(StatusCode::PRECONDITION_FAILED).finish(); } else if not_modified { return res .status(StatusCode::NOT_MODIFIED) .body(body::None::new()) .map_into_boxed_body(); } let reader = crate::file::new_live_reader(length, offset, self.file, self.info.uploader); if offset != 0 || length != self.info.size { res.status(StatusCode::PARTIAL_CONTENT); } res.body(SizedStream::new(length, reader)) } } fn precondition_failed(req: &HttpRequest, etag: &EntityTag, last_modified: &HttpDate) -> bool { if let Some(IfMatch::Items(ref items)) = req.get_header() { if !items.iter().any(|item| item.strong_eq(etag)) { return true; } } if let Some(IfUnmodifiedSince(ref since)) = req.get_header() { if last_modified > since { return true; } } false } fn not_modified(req: &HttpRequest, etag: &EntityTag, last_modified: &HttpDate) -> bool { match req.get_header::() { Some(IfNoneMatch::Any) => { return true; } Some(IfNoneMatch::Items(ref items)) => { return items.iter().any(|item| item.weak_eq(etag)); } None => (), } if let Some(IfModifiedSince(ref since)) = req.get_header() { if last_modified < since { return true; } } false } impl Responder for DownloadingFile { type Body = BoxBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { self.into_response(req) } }