transbeam/src/download.rs

153 lines
5 KiB
Rust
Raw Normal View History

use std::{fs::File, os::unix::fs::MetadataExt, time::SystemTime};
2022-04-26 23:54:29 -04:00
use actix_web::{
body::{self, BoxBody, SizedStream},
http::{
header::{
2022-04-28 05:18:35 -04:00
self, ContentDisposition, DispositionParam, DispositionType, EntityTag, HeaderValue,
HttpDate, IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince,
2022-04-26 23:54:29 -04:00
},
StatusCode,
},
HttpMessage, HttpRequest, HttpResponse, Responder,
2022-04-26 23:54:29 -04:00
};
use actix_files::HttpRange;
2022-04-26 23:54:29 -04:00
use crate::DownloadableFile;
2022-04-26 23:54:29 -04:00
// This is copied substantially from actix-files, with some tweaks
2022-04-26 23:54:29 -04:00
pub(crate) struct DownloadingFile {
pub(crate) file: File,
pub(crate) info: DownloadableFile,
2022-04-26 23:54:29 -04:00
}
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(),
))
2022-04-26 23:54:29 -04:00
}
/// Creates an `HttpResponse` with file as a streaming body.
pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
let etag = self.etag();
let last_modified = HttpDate::from(SystemTime::from(self.info.modtime));
2022-04-26 23:54:29 -04:00
let precondition_failed = precondition_failed(req, &etag, &last_modified);
let not_modified = not_modified(req, &etag, &last_modified);
2022-04-26 23:54:29 -04:00
let mut res = HttpResponse::build(StatusCode::OK);
2022-04-26 23:54:29 -04:00
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)],
2022-04-28 05:18:35 -04:00
},
));
res.insert_header((header::LAST_MODIFIED, last_modified));
res.insert_header((header::ETAG, etag));
2022-04-26 23:54:29 -04:00
res.insert_header((header::ACCEPT_RANGES, "bytes"));
let mut length = self.info.size;
2022-04-26 23:54:29 -04:00
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,
2022-04-28 05:18:35 -04:00
format!(
"bytes {}-{}/{}",
offset,
offset + length - 1,
self.info.size
),
2022-04-26 23:54:29 -04:00
));
} 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);
2022-04-26 23:54:29 -04:00
if offset != 0 || length != self.info.size {
2022-04-26 23:54:29 -04:00
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;
2022-04-26 23:54:29 -04:00
}
}
false
2022-04-26 23:54:29 -04:00
}
fn not_modified(req: &HttpRequest, etag: &EntityTag, last_modified: &HttpDate) -> bool {
match req.get_header::<IfNoneMatch>() {
2022-04-28 05:18:35 -04:00
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;
2022-04-26 23:54:29 -04:00
}
}
false
2022-04-26 23:54:29 -04:00
}
impl Responder for DownloadingFile {
2022-04-26 23:54:29 -04:00
type Body = BoxBody;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
self.into_response(req)
}
}