allow downloading individual files from bundle
This commit is contained in:
parent
43d03869ab
commit
007289ffe5
15 changed files with 499 additions and 69 deletions
113
src/download.rs
113
src/download.rs
|
|
@ -1,3 +1,4 @@
|
|||
use core::fmt;
|
||||
use std::{
|
||||
cmp,
|
||||
fs::File,
|
||||
|
|
@ -14,6 +15,8 @@ use futures_core::{ready, Stream};
|
|||
use inotify::{Inotify, WatchMask};
|
||||
use log::trace;
|
||||
use pin_project_lite::pin_project;
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
use time::OffsetDateTime;
|
||||
use std::{os::unix::fs::MetadataExt, time::SystemTime};
|
||||
|
||||
use actix_web::{
|
||||
|
|
@ -30,7 +33,55 @@ use actix_web::{
|
|||
|
||||
use actix_files::HttpRange;
|
||||
|
||||
use crate::store::StoredFile;
|
||||
use crate::{store::StoredFile, upload::UploadedFile};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum DownloadSelection {
|
||||
One(usize),
|
||||
All,
|
||||
}
|
||||
|
||||
impl fmt::Display for DownloadSelection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DownloadSelection::All => write!(f, "all"),
|
||||
DownloadSelection::One(n) => n.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectionVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for SelectionVisitor {
|
||||
type Value = DownloadSelection;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, r#"a nonnegative integer or the string "all""#)
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
|
||||
Ok(DownloadSelection::One(v as usize))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where E: de::Error {
|
||||
if v == "all" {
|
||||
Ok(DownloadSelection::All)
|
||||
} else if let Ok(n) = v.parse::<usize>() {
|
||||
Ok(DownloadSelection::One(n))
|
||||
} else {
|
||||
Err(de::Error::invalid_value(de::Unexpected::Str(v), &self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DownloadSelection {
|
||||
fn deserialize<D: Deserializer<'de>>(
|
||||
de: D
|
||||
) -> Result<DownloadSelection, D::Error> {
|
||||
de.deserialize_any(SelectionVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
// This is copied substantially from actix-files, with some tweaks
|
||||
|
||||
|
|
@ -38,24 +89,64 @@ pub(crate) struct DownloadingFile {
|
|||
pub(crate) file: File,
|
||||
pub(crate) storage_path: PathBuf,
|
||||
pub(crate) info: StoredFile,
|
||||
pub(crate) selection: DownloadSelection,
|
||||
}
|
||||
|
||||
impl DownloadingFile {
|
||||
fn selected(&self) -> Option<&UploadedFile> {
|
||||
match self.selection {
|
||||
DownloadSelection::All => None,
|
||||
DownloadSelection::One(n) => Some(self.info.contents.as_ref()?.get(n)?),
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
match self.selected() {
|
||||
None => &self.info.name,
|
||||
Some(f) => &f.name,
|
||||
}
|
||||
}
|
||||
|
||||
fn size(&self) -> u64 {
|
||||
match self.selected() {
|
||||
None => self.info.size,
|
||||
Some(f) => f.size,
|
||||
}
|
||||
}
|
||||
|
||||
fn modtime(&self) -> OffsetDateTime {
|
||||
match self.selected() {
|
||||
None => self.info.modtime,
|
||||
Some(f) => f.modtime,
|
||||
}
|
||||
}
|
||||
|
||||
fn baseline_offset(&self) -> u64 {
|
||||
if let (DownloadSelection::One(n), Some(files)) = (self.selection, self.info.contents.as_ref()) {
|
||||
crate::zip::file_data_offset(&files, n)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn etag(&self) -> EntityTag {
|
||||
let ino = self.file.metadata().map(|md| md.ino()).unwrap_or_default();
|
||||
let modtime = self.modtime();
|
||||
EntityTag::new_strong(format!(
|
||||
"{:x}:{:x}:{:x}:{:x}",
|
||||
"{:x}:{}:{:x}:{:x}:{:x}",
|
||||
ino,
|
||||
self.info.size,
|
||||
self.info.modtime.unix_timestamp() as u64,
|
||||
self.info.modtime.nanosecond(),
|
||||
self.selection,
|
||||
self.size(),
|
||||
modtime.unix_timestamp() as u64,
|
||||
modtime.nanosecond(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Creates an `HttpResponse` with file as a streaming body.
|
||||
pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
|
||||
let total_size = self.size();
|
||||
let etag = self.etag();
|
||||
let last_modified = HttpDate::from(SystemTime::from(self.info.modtime));
|
||||
let last_modified = HttpDate::from(SystemTime::from(self.modtime()));
|
||||
|
||||
let precondition_failed = precondition_failed(req, &etag, &last_modified);
|
||||
let not_modified = not_modified(req, &etag, &last_modified);
|
||||
|
|
@ -68,14 +159,14 @@ impl DownloadingFile {
|
|||
header::CONTENT_DISPOSITION,
|
||||
ContentDisposition {
|
||||
disposition: DispositionType::Attachment,
|
||||
parameters: vec![DispositionParam::Filename(self.info.name)],
|
||||
parameters: vec![DispositionParam::Filename(self.name().to_string())],
|
||||
},
|
||||
));
|
||||
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 length = total_size;
|
||||
let mut offset = 0;
|
||||
|
||||
// check for range header
|
||||
|
|
@ -97,7 +188,7 @@ impl DownloadingFile {
|
|||
"bytes {}-{}/{}",
|
||||
offset,
|
||||
offset + length - 1,
|
||||
self.info.size
|
||||
total_size,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
|
|
@ -118,9 +209,9 @@ impl DownloadingFile {
|
|||
.map_into_boxed_body();
|
||||
}
|
||||
|
||||
let reader = new_live_reader(length, offset, self.file, self.storage_path);
|
||||
let reader = new_live_reader(length, self.baseline_offset() + offset, self.file, self.storage_path);
|
||||
|
||||
if offset != 0 || length != self.info.size {
|
||||
if offset != 0 || length != total_size {
|
||||
res.status(StatusCode::PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue