2022-04-27 20:15:51 -04:00
|
|
|
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,
|
|
|
|
},
|
2022-04-27 20:15:51 -04:00
|
|
|
HttpMessage, HttpRequest, HttpResponse, Responder,
|
2022-04-26 23:54:29 -04:00
|
|
|
};
|
|
|
|
|
2022-04-27 20:15:51 -04:00
|
|
|
use actix_files::HttpRange;
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-04-27 20:15:51 -04:00
|
|
|
use crate::DownloadableFile;
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-04-27 20:15:51 -04:00
|
|
|
// This is copied substantially from actix-files, with some tweaks
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-04-27 20:15:51 -04:00
|
|
|
pub(crate) struct DownloadingFile {
|
|
|
|
pub(crate) file: File,
|
|
|
|
pub(crate) info: DownloadableFile,
|
2022-04-26 23:54:29 -04:00
|
|
|
}
|
|
|
|
|
2022-04-27 20:15:51 -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> {
|
2022-04-27 20:15:51 -04:00
|
|
|
let etag = self.etag();
|
|
|
|
let last_modified = HttpDate::from(SystemTime::from(self.info.modtime));
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-04-27 20:15:51 -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
|
|
|
|
2022-04-27 20:15:51 -04:00
|
|
|
let mut res = HttpResponse::build(StatusCode::OK);
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-04-28 01:06:39 -04:00
|
|
|
res.insert_header((header::CONTENT_SECURITY_POLICY, "sandbox"));
|
2022-04-27 20:15:51 -04:00
|
|
|
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
|
|
|
},
|
2022-04-27 20:15:51 -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"));
|
|
|
|
|
2022-04-27 20:15:51 -04:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2022-04-27 20:15:51 -04:00
|
|
|
let reader = crate::file::new_live_reader(length, offset, self.file, self.info.uploader);
|
2022-04-26 23:54:29 -04:00
|
|
|
|
2022-04-27 20:15:51 -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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-27 20:15:51 -04:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2022-04-27 20:15:51 -04:00
|
|
|
false
|
2022-04-26 23:54:29 -04:00
|
|
|
}
|
|
|
|
|
2022-04-27 20:15:51 -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;
|
|
|
|
}
|
2022-04-27 20:15:51 -04:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2022-04-27 20:15:51 -04:00
|
|
|
false
|
2022-04-26 23:54:29 -04:00
|
|
|
}
|
|
|
|
|
2022-04-27 20:15:51 -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)
|
|
|
|
}
|
|
|
|
}
|