Compare commits

..

No commits in common. "62e6d64253f490843a61ca28b3d58acc52828f85" and "007289ffe55ee1fffba9a848169136bed6fa46ed" have entirely different histories.

7 changed files with 47 additions and 112 deletions

View file

@ -16,8 +16,8 @@ use inotify::{Inotify, WatchMask};
use log::trace; use log::trace;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use serde::{de, Deserialize, Deserializer}; use serde::{de, Deserialize, Deserializer};
use std::{os::unix::fs::MetadataExt, time::SystemTime};
use time::OffsetDateTime; use time::OffsetDateTime;
use std::{os::unix::fs::MetadataExt, time::SystemTime};
use actix_web::{ use actix_web::{
body::{self, BoxBody, SizedStream}, body::{self, BoxBody, SizedStream},
@ -64,9 +64,7 @@ impl<'de> de::Visitor<'de> for SelectionVisitor {
} }
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where where E: de::Error {
E: de::Error,
{
if v == "all" { if v == "all" {
Ok(DownloadSelection::All) Ok(DownloadSelection::All)
} else if let Ok(n) = v.parse::<usize>() { } else if let Ok(n) = v.parse::<usize>() {
@ -78,7 +76,9 @@ impl<'de> de::Visitor<'de> for SelectionVisitor {
} }
impl<'de> Deserialize<'de> for DownloadSelection { impl<'de> Deserialize<'de> for DownloadSelection {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<DownloadSelection, D::Error> { fn deserialize<D: Deserializer<'de>>(
de: D
) -> Result<DownloadSelection, D::Error> {
de.deserialize_any(SelectionVisitor) de.deserialize_any(SelectionVisitor)
} }
} }
@ -122,10 +122,8 @@ impl DownloadingFile {
} }
fn baseline_offset(&self) -> u64 { fn baseline_offset(&self) -> u64 {
if let (DownloadSelection::One(n), Some(files)) = if let (DownloadSelection::One(n), Some(files)) = (self.selection, self.info.contents.as_ref()) {
(self.selection, self.info.contents.as_ref()) crate::zip::file_data_offset(&files, n)
{
crate::zip::file_data_offset(files, n)
} else { } else {
0 0
} }
@ -186,7 +184,12 @@ impl DownloadingFile {
res.insert_header(( res.insert_header((
header::CONTENT_RANGE, header::CONTENT_RANGE,
format!("bytes {}-{}/{}", offset, offset + length - 1, total_size,), format!(
"bytes {}-{}/{}",
offset,
offset + length - 1,
total_size,
),
)); ));
} else { } else {
res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
@ -206,12 +209,7 @@ impl DownloadingFile {
.map_into_boxed_body(); .map_into_boxed_body();
} }
let reader = new_live_reader( let reader = new_live_reader(length, self.baseline_offset() + offset, self.file, self.storage_path);
length,
self.baseline_offset() + offset,
self.file,
self.storage_path,
);
if offset != 0 || length != total_size { if offset != 0 || length != total_size {
res.status(StatusCode::PARTIAL_CONTENT); res.status(StatusCode::PARTIAL_CONTENT);

View file

@ -8,8 +8,8 @@ use std::{fmt::Debug, path::PathBuf, str::FromStr};
use actix_files::NamedFile; use actix_files::NamedFile;
use actix_web::{ use actix_web::{
error::InternalError, get, http::StatusCode, middleware::Logger, post, web, App, HttpRequest, get, http::StatusCode, middleware::Logger, post, web, App, HttpRequest, HttpResponse,
HttpResponse, HttpServer, Responder, HttpServer, Responder,
}; };
use actix_web_actors::ws; use actix_web_actors::ws;
use askama::Template; use askama::Template;
@ -51,28 +51,28 @@ pub fn log_auth_failure(ip_addr: &str) {
warn!("Incorrect authentication attempt from {}", ip_addr); warn!("Incorrect authentication attempt from {}", ip_addr);
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct DownloadRequest { struct DownloadRequest {
code: String, code: String,
download: Option<download::DownloadSelection>, download: Option<download::DownloadSelection>,
} }
#[derive(Serialize, Template)] #[derive(Template)]
#[template(path = "download.html")] #[template(path = "download.html")]
struct DownloadInfo { struct DownloadInfo<'a> {
file: StoredFile, file: StoredFile,
code: String, code: &'a str,
available: u64, available: u64,
offsets: Option<Vec<u64>>,
} }
#[get("/download")] #[get("/download")]
async fn handle_download( async fn handle_download(
req: HttpRequest, req: HttpRequest,
query: web::Query<DownloadRequest>, download: web::Query<DownloadRequest>,
data: web::Data<AppState>, data: web::Data<AppState>,
) -> actix_web::Result<HttpResponse> { ) -> actix_web::Result<HttpResponse> {
let code = &query.code; let code = &download.code;
if !store::is_valid_storage_code(code) { if !store::is_valid_storage_code(code) {
return not_found(req, data, true); return not_found(req, data, true);
} }
@ -80,12 +80,12 @@ async fn handle_download(
let info = if let Some(i) = info { let info = if let Some(i) = info {
i i
} else { } else {
return not_found(req, data, true); return not_found(req, data, true)
}; };
let storage_path = data.config.storage_dir.join(code); let storage_path = data.config.storage_dir.join(code);
let file = File::open(&storage_path).await?; let file = File::open(&storage_path).await?;
if let Some(selection) = query.download { if let Some(selection) = download.download {
if let download::DownloadSelection::One(n) = selection { if let download::DownloadSelection::One(n) = selection {
if let Some(ref files) = info.contents { if let Some(ref files) = info.contents {
if n >= files.len() { if n >= files.len() {
@ -103,64 +103,26 @@ async fn handle_download(
} }
.into_response(&req)) .into_response(&req))
} else { } else {
let offsets = info.contents.as_deref().map(zip::file_data_offsets); Ok(HttpResponse::Ok().body(DownloadInfo {
Ok(HttpResponse::Ok().body(
DownloadInfo {
file: info, file: info,
code: code.clone(), code,
available: file.metadata().await?.len(), available: file.metadata().await?.len(),
offsets, }.render().unwrap()))
}
.render()
.unwrap(),
))
} }
} }
#[derive(Deserialize)] fn not_found(
struct InfoQuery {
code: String,
}
#[get("/info")]
async fn download_info(
req: HttpRequest, req: HttpRequest,
query: web::Query<InfoQuery>,
data: web::Data<AppState>, data: web::Data<AppState>,
) -> actix_web::Result<impl Responder> { report: bool,
let code = &query.code; ) -> actix_web::Result<HttpResponse> {
if !store::is_valid_storage_code(code) {
return not_found(req, data, true);
}
let info = data.file_store.read().await.lookup_file(code);
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 offsets = info.contents.as_deref().map(zip::file_data_offsets);
Ok(web::Json(DownloadInfo {
file: info,
code: code.clone(),
available: File::open(&storage_path).await?.metadata().await?.len(),
offsets,
}))
}
fn not_found<T>(req: HttpRequest, data: web::Data<AppState>, report: bool) -> actix_web::Result<T> {
if report { if report {
let ip_addr = get_ip_addr(&req, data.config.reverse_proxy); let ip_addr = get_ip_addr(&req, data.config.reverse_proxy);
log_auth_failure(&ip_addr); log_auth_failure(&ip_addr);
} }
Err(InternalError::from_response( Ok(NamedFile::open(data.config.static_dir.join("404.html"))?
"Download not found",
NamedFile::open(data.config.static_dir.join("404.html"))?
.set_status_code(StatusCode::NOT_FOUND) .set_status_code(StatusCode::NOT_FOUND)
.into_response(&req), .into_response(&req))
)
.into())
} }
#[get("/upload")] #[get("/upload")]
@ -282,7 +244,6 @@ async fn main() -> std::io::Result<()> {
Logger::default() Logger::default()
}) })
.service(handle_download) .service(handle_download)
.service(download_info)
.service(handle_upload) .service(handle_upload)
.service(check_upload_password) .service(check_upload_password)
.service(upload_limits) .service(upload_limits)

View file

@ -3,7 +3,10 @@ use core::fmt;
use serde::{de::Visitor, Deserializer, Serializer}; use serde::{de::Visitor, Deserializer, Serializer};
use time::OffsetDateTime; use time::OffsetDateTime;
pub(crate) fn serialize<S: Serializer>(time: &OffsetDateTime, ser: S) -> Result<S::Ok, S::Error> { pub(crate) fn serialize<S: Serializer>(
time: &OffsetDateTime,
ser: S,
) -> Result<S::Ok, S::Error> {
ser.serialize_i64(time.unix_timestamp()) ser.serialize_i64(time.unix_timestamp())
} }
@ -25,7 +28,9 @@ impl<'de> Visitor<'de> for I64Visitor {
} }
} }
pub(crate) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<OffsetDateTime, D::Error> { pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
de: D,
) -> Result<OffsetDateTime, D::Error> {
Ok( Ok(
OffsetDateTime::from_unix_timestamp(de.deserialize_i64(I64Visitor)?) OffsetDateTime::from_unix_timestamp(de.deserialize_i64(I64Visitor)?)
.unwrap_or_else(|_| OffsetDateTime::now_utc()), .unwrap_or_else(|_| OffsetDateTime::now_utc()),

View file

@ -43,17 +43,6 @@ pub fn file_data_offset(files: &[UploadedFile], idx: usize) -> u64 {
+ files[idx].name.len() as u64 + files[idx].name.len() as u64
} }
pub fn file_data_offsets(files: &[UploadedFile]) -> Vec<u64> {
let mut offsets = Vec::new();
let mut offset: u64 = 0;
for file in files.iter() {
offset += LOCAL_HEADER_SIZE_MINUS_FILENAME + file.name.len() as u64;
offsets.push(offset);
offset += file.size + DATA_DESCRIPTOR_SIZE;
}
offsets
}
fn central_directory_size(files: &[UploadedFile]) -> u64 { fn central_directory_size(files: &[UploadedFile]) -> u64 {
let mut total = 0; let mut total = 0;
for file in files.iter() { for file in files.iter() {

View file

@ -4,7 +4,6 @@ body {
max-width: 512px; max-width: 512px;
margin: 0.5em auto; margin: 0.5em auto;
padding: 0 1em; padding: 0 1em;
overflow-wrap: break-word;
} }
#header h1 { #header h1 {

View file

@ -1,14 +0,0 @@
document.addEventListener("DOMContentLoaded", () => {
const table = document.getElementById("download_contents").getElementsByTagName("tbody")[0];
if (table.children.length === 0) { return; }
setInterval(() => {
fetch(`info?code=${CODE}`)
.then((res) => res.json())
.then((info) => {
for (const [index, offset] of info.offsets.entries()) {
table.children[index].className = (offset > info.available) ? "unavailable" : "";
}
});
}, 5000);
});

View file

@ -4,12 +4,10 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <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/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="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
<link rel="manifest" href="manifest.json"/> <link rel="manifest" href="manifest.json"/>
<script src="js/util.js"></script> <script src="js/util.js"></script>
<script type="text/javascript">
const CODE = "{{ code }}";
</script>
<script src="js/download.js"></script> <script src="js/download.js"></script>
<title>{{ file.name }} - transbeam</title> <title>{{ file.name }} - transbeam</title>
</head> </head>
@ -27,17 +25,16 @@
{% when Some with (files) %} {% when Some with (files) %}
<div id="download_contents" class="section"> <div id="download_contents" class="section">
<h3>Contents</h3> <h3>Contents</h3>
<table><tbody> <table>
{% let offsets = offsets.as_ref().unwrap() %}
{% for f in files %} {% for f in files %}
<tr class="{% if offsets.get(loop.index0.clone()).unwrap().clone() > available %}unavailable{% endif %}"> <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_size">{{ bytesize::to_string(f.size.clone(), false).replace(" ", "") }}</td>
<td class="file_name">{{ f.name }}</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_download"><a class="download_button" href="download?code={{ code }}&download={{ loop.index0 }}"></a></td>
<td class="file_unavailable"></td> <td class="file_unavailable"></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody></table> </table>
</div> </div>
{% else %} {% else %}
{% endmatch %} {% endmatch %}