allow downloading individual files from bundle
This commit is contained in:
parent
43d03869ab
commit
007289ffe5
160
Cargo.lock
generated
160
Cargo.lock
generated
|
@ -309,12 +309,54 @@ dependencies = [
|
|||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"askama_escape",
|
||||
"askama_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71"
|
||||
dependencies = [
|
||||
"askama_shared",
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_escape"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
||||
|
||||
[[package]]
|
||||
name = "askama_shared"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0"
|
||||
dependencies = [
|
||||
"askama_escape",
|
||||
"humansize",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"percent-encoding",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
|
@ -523,6 +565,41 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
|
@ -732,12 +809,24 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
|
@ -879,6 +968,12 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.1"
|
||||
|
@ -921,6 +1016,16 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.3.7"
|
||||
|
@ -930,6 +1035,15 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.13.1"
|
||||
|
@ -1126,6 +1240,12 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.9"
|
||||
|
@ -1193,6 +1313,29 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b827f2113224f3f19a665136f006709194bdfdcb1fdc1e4b2b5cbac8e0cced54"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_with_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.10.0"
|
||||
|
@ -1257,6 +1400,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
|
@ -1381,6 +1530,15 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.34"
|
||||
|
@ -1423,6 +1581,7 @@ dependencies = [
|
|||
"actix-http",
|
||||
"actix-web",
|
||||
"actix-web-actors",
|
||||
"askama",
|
||||
"bytes",
|
||||
"bytesize",
|
||||
"crc32fast",
|
||||
|
@ -1438,6 +1597,7 @@ dependencies = [
|
|||
"sanitise-file-name",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
|
|
|
@ -13,6 +13,7 @@ actix-files = "0.6.0"
|
|||
actix-http = "3.0.4"
|
||||
actix-web = "4.0.1"
|
||||
actix-web-actors = "4.1.0"
|
||||
askama = "0.11.1"
|
||||
bytes = "1.1.0"
|
||||
bytesize = "1.1.0"
|
||||
crc32fast = "1.3.2"
|
||||
|
@ -28,6 +29,7 @@ rand = "0.8.5"
|
|||
sanitise-file-name = "1.0.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_with = "1.13.0"
|
||||
thiserror = "1"
|
||||
time = "0.3.9"
|
||||
tokio = { version = "1.17.0", features = ["full"] }
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
sender has gone offline
|
||||
- Easy to send multiple files at once - they're bundled into a zip
|
||||
file for receivers, with zero compression so extraction is quick
|
||||
- Can also download individual files out of an uploaded collection
|
||||
- Sanitises filenames, using sensible non-obnoxious defaults that
|
||||
should be safe across platforms
|
||||
- Rudimentary password authentication for uploading files
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
|
|
53
src/main.rs
53
src/main.rs
|
@ -1,9 +1,10 @@
|
|||
mod download;
|
||||
mod store;
|
||||
mod timestamp;
|
||||
mod upload;
|
||||
mod zip;
|
||||
|
||||
use std::{fmt::Debug, fs::File, path::PathBuf, str::FromStr};
|
||||
use std::{fmt::Debug, path::PathBuf, str::FromStr};
|
||||
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{
|
||||
|
@ -11,11 +12,12 @@ use actix_web::{
|
|||
HttpServer, Responder,
|
||||
};
|
||||
use actix_web_actors::ws;
|
||||
use askama::Template;
|
||||
use bytesize::ByteSize;
|
||||
use log::{error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use store::FileStore;
|
||||
use tokio::sync::RwLock;
|
||||
use store::{FileStore, StoredFile};
|
||||
use tokio::{fs::File, sync::RwLock};
|
||||
|
||||
const APP_NAME: &str = "transbeam";
|
||||
|
||||
|
@ -49,9 +51,19 @@ pub fn log_auth_failure(ip_addr: &str) {
|
|||
warn!("Incorrect authentication attempt from {}", ip_addr);
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DownloadRequest {
|
||||
code: String,
|
||||
download: Option<download::DownloadSelection>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "download.html")]
|
||||
struct DownloadInfo<'a> {
|
||||
file: StoredFile,
|
||||
code: &'a str,
|
||||
available: u64,
|
||||
}
|
||||
|
||||
#[get("/download")]
|
||||
|
@ -62,29 +74,52 @@ async fn handle_download(
|
|||
) -> actix_web::Result<HttpResponse> {
|
||||
let code = &download.code;
|
||||
if !store::is_valid_storage_code(code) {
|
||||
return download_not_found(req, data);
|
||||
return not_found(req, data, true);
|
||||
}
|
||||
let info = data.file_store.read().await.lookup_file(code);
|
||||
if let Some(info) = info {
|
||||
let info = if let Some(i) = info {
|
||||
i
|
||||
} else {
|
||||
return not_found(req, data, true)
|
||||
};
|
||||
|
||||
let storage_path = data.config.storage_dir.join(code);
|
||||
let file = File::open(&storage_path)?;
|
||||
let file = File::open(&storage_path).await?;
|
||||
if let Some(selection) = download.download {
|
||||
if let download::DownloadSelection::One(n) = selection {
|
||||
if let Some(ref files) = info.contents {
|
||||
if n >= files.len() {
|
||||
return not_found(req, data, false);
|
||||
}
|
||||
} else {
|
||||
return not_found(req, data, false);
|
||||
}
|
||||
}
|
||||
Ok(download::DownloadingFile {
|
||||
file,
|
||||
file: file.into_std().await,
|
||||
storage_path,
|
||||
info,
|
||||
selection,
|
||||
}
|
||||
.into_response(&req))
|
||||
} else {
|
||||
download_not_found(req, data)
|
||||
Ok(HttpResponse::Ok().body(DownloadInfo {
|
||||
file: info,
|
||||
code,
|
||||
available: file.metadata().await?.len(),
|
||||
}.render().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
fn download_not_found(
|
||||
fn not_found(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppState>,
|
||||
report: bool,
|
||||
) -> actix_web::Result<HttpResponse> {
|
||||
if report {
|
||||
let ip_addr = get_ip_addr(&req, data.config.reverse_proxy);
|
||||
log_auth_failure(&ip_addr);
|
||||
}
|
||||
Ok(NamedFile::open(data.config.static_dir.join("404.html"))?
|
||||
.set_status_code(StatusCode::NOT_FOUND)
|
||||
.into_response(&req))
|
||||
|
|
50
src/store.rs
50
src/store.rs
|
@ -10,12 +10,15 @@ use rand::{
|
|||
thread_rng, Rng,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
};
|
||||
|
||||
use crate::upload::UploadedFile;
|
||||
|
||||
const STATE_FILE_NAME: &str = "files.json";
|
||||
const MAX_STORAGE_FILES: usize = 1024;
|
||||
|
||||
|
@ -33,55 +36,16 @@ pub fn is_valid_storage_code(s: &str) -> bool {
|
|||
.all(|c| c.is_ascii_alphanumeric() || c == &b'-')
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct StoredFile {
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
#[serde(with = "timestamp")]
|
||||
#[serde(with = "crate::timestamp")]
|
||||
pub modtime: OffsetDateTime,
|
||||
#[serde(with = "timestamp")]
|
||||
#[serde(with = "crate::timestamp")]
|
||||
pub expiry: OffsetDateTime,
|
||||
}
|
||||
|
||||
pub(crate) mod timestamp {
|
||||
use core::fmt;
|
||||
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub(crate) fn serialize<S: Serializer>(
|
||||
time: &OffsetDateTime,
|
||||
ser: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
ser.serialize_i64(time.unix_timestamp())
|
||||
}
|
||||
|
||||
struct I64Visitor;
|
||||
|
||||
impl<'de> Visitor<'de> for I64Visitor {
|
||||
type Value = i64;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "an integer")
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
|
||||
Ok(v as i64)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
|
||||
de: D,
|
||||
) -> Result<OffsetDateTime, D::Error> {
|
||||
Ok(
|
||||
OffsetDateTime::from_unix_timestamp(de.deserialize_i64(I64Visitor)?)
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc()),
|
||||
)
|
||||
}
|
||||
pub contents: Option<Vec<UploadedFile>>,
|
||||
}
|
||||
|
||||
async fn is_valid_entry(key: &str, info: &StoredFile, storage_dir: &Path) -> bool {
|
||||
|
|
38
src/timestamp.rs
Normal file
38
src/timestamp.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use core::fmt;
|
||||
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub(crate) fn serialize<S: Serializer>(
|
||||
time: &OffsetDateTime,
|
||||
ser: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
ser.serialize_i64(time.unix_timestamp())
|
||||
}
|
||||
|
||||
struct I64Visitor;
|
||||
|
||||
impl<'de> Visitor<'de> for I64Visitor {
|
||||
type Value = i64;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "an integer")
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
|
||||
Ok(v as i64)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
|
||||
de: D,
|
||||
) -> Result<OffsetDateTime, D::Error> {
|
||||
Ok(
|
||||
OffsetDateTime::from_unix_timestamp(de.deserialize_i64(I64Visitor)?)
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc()),
|
||||
)
|
||||
}
|
|
@ -93,9 +93,11 @@ impl Actor for Uploader {
|
|||
|
||||
type Context = <Uploader as Actor>::Context;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct UploadedFile {
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
#[serde(with = "crate::timestamp")]
|
||||
pub modtime: OffsetDateTime,
|
||||
}
|
||||
|
||||
|
@ -273,7 +275,7 @@ impl Uploader {
|
|||
let (writer, name, size, modtime): (Box<dyn Write>, _, _, _) = if files.len() > 1 {
|
||||
info!("Wrapping in zipfile generator");
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let zip_writer = super::zip::ZipGenerator::new(files, writer);
|
||||
let zip_writer = super::zip::ZipGenerator::new(files.clone(), writer);
|
||||
let size = zip_writer.total_size();
|
||||
let download_filename = super::APP_NAME.to_owned()
|
||||
+ &now.format(FILENAME_DATE_FORMAT).unwrap()
|
||||
|
@ -293,6 +295,7 @@ impl Uploader {
|
|||
size,
|
||||
modtime,
|
||||
expiry: OffsetDateTime::now_utc() + lifetime * time::Duration::DAY,
|
||||
contents: if files.len() > 1 { Some(files) } else { None },
|
||||
};
|
||||
let state = self.app_state.clone();
|
||||
let storage_filename = self.storage_filename.clone();
|
||||
|
|
|
@ -37,6 +37,12 @@ fn file_entries_size(files: &[UploadedFile]) -> u64 {
|
|||
total
|
||||
}
|
||||
|
||||
pub fn file_data_offset(files: &[UploadedFile], idx: usize) -> u64 {
|
||||
file_entries_size(&files[..idx])
|
||||
+ LOCAL_HEADER_SIZE_MINUS_FILENAME
|
||||
+ files[idx].name.len() as u64
|
||||
}
|
||||
|
||||
fn central_directory_size(files: &[UploadedFile]) -> u64 {
|
||||
let mut total = 0;
|
||||
for file in files.iter() {
|
||||
|
|
|
@ -127,6 +127,18 @@ td {
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
td.file_unavailable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tr.unavailable td.file_unavailable {
|
||||
display: revert;
|
||||
}
|
||||
|
||||
tr.unavailable td.file_download {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.delete_button {
|
||||
background-color: #888;
|
||||
mask-image: url("../images/feather-icons/x.svg");
|
||||
|
@ -142,6 +154,75 @@ td {
|
|||
background-color: #f00;
|
||||
}
|
||||
|
||||
#download_toplevel {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#download_toplevel div {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#download_toplevel .file_name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#download_toplevel .file_download {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#download_contents h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#download_contents table {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.file_download {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file_download .download_button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
td.file_download {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.download_button {
|
||||
background-color: #33f;
|
||||
mask-image: url("../images/feather-icons/download.svg");
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.download_button:hover {
|
||||
background-color: #00b;
|
||||
}
|
||||
|
||||
.file_unavailable {
|
||||
background-color: #999;
|
||||
mask-image: url("../images/feather-icons/clock.svg");
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
td.file_size {
|
||||
text-align: right;
|
||||
}
|
||||
|
@ -162,6 +243,7 @@ button, .fake_button, input[type="submit"] {
|
|||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button:hover, .fake_button:hover, input[type="submit"]:hover {
|
||||
|
|
1
static/images/feather-icons/clock.svg
Normal file
1
static/images/feather-icons/clock.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
After Width: | Height: | Size: 304 B |
1
static/images/feather-icons/download.svg
Normal file
1
static/images/feather-icons/download.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
After Width: | Height: | Size: 370 B |
|
@ -8,7 +8,7 @@
|
|||
<link rel="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
|
||||
<link rel="manifest" href="manifest.json"/>
|
||||
<script src="js/util.js"></script>
|
||||
<script src="js/download.js"></script>
|
||||
<script src="js/download-landing.js"></script>
|
||||
<script src="js/upload.js"></script>
|
||||
<title>transbeam</title>
|
||||
</head>
|
||||
|
|
46
templates/download.html
Normal file
46
templates/download.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="stylesheet" type="text/css" href="css/transbeam.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="css/download.css"/>
|
||||
<link rel="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
|
||||
<link rel="manifest" href="manifest.json"/>
|
||||
<script src="js/util.js"></script>
|
||||
<script src="js/download.js"></script>
|
||||
<title>{{ file.name }} - transbeam</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img src="images/site-icons/transbeam.svg" height="128">
|
||||
<h1>transbeam</h1>
|
||||
</div>
|
||||
<div id="download_toplevel" class="section">
|
||||
<div class="file_name">{{ file.name }}</div>
|
||||
<div class="file_size">{{ bytesize::to_string(file.size.clone(), false).replace(" ", "") }}</div>
|
||||
<div class="file_download"><a class="download_button" href="download?code={{ code }}&download=all"></a></div>
|
||||
</div>
|
||||
{% match file.contents %}
|
||||
{% when Some with (files) %}
|
||||
<div id="download_contents" class="section">
|
||||
<h3>Contents</h3>
|
||||
<table>
|
||||
{% for f in files %}
|
||||
<tr class="{% if zip::file_data_offset(files.as_ref(), loop.index0.clone()) > available %}unavailable{% endif %}">
|
||||
<td class="file_size">{{ bytesize::to_string(f.size.clone(), false).replace(" ", "") }}</td>
|
||||
<td class="file_name">{{ f.name }}</td>
|
||||
<td class="file_download"><a class="download_button" href="download?code={{ code }}&download={{ loop.index0 }}"></a></td>
|
||||
<td class="file_unavailable"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
{% endmatch %}
|
||||
<div id="footer">
|
||||
<h5>(c) 2022 xenofem, MIT licensed</h5>
|
||||
<h5><a target="_blank" href="https://git.xeno.science/xenofem/transbeam">source</a></h5>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue