update download contents as more files become available

main
xenofem 2022-05-24 16:49:48 -04:00
parent 007289ffe5
commit be4decde12
4 changed files with 80 additions and 16 deletions

View File

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

View File

@ -43,6 +43,17 @@ pub fn file_data_offset(files: &[UploadedFile], idx: usize) -> 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 {
let mut total = 0;
for file in files.iter() {

14
static/js/download.js Normal file
View File

@ -0,0 +1,14 @@
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,10 +4,12 @@
<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 type="text/javascript">
const CODE = "{{ code }}";
</script>
<script src="js/download.js"></script>
<title>{{ file.name }} - transbeam</title>
</head>
@ -25,16 +27,17 @@
{% when Some with (files) %}
<div id="download_contents" class="section">
<h3>Contents</h3>
<table>
<table><tbody>
{% let offsets = offsets.as_ref().unwrap() %}
{% for f in files %}
<tr class="{% if zip::file_data_offset(files.as_ref(), loop.index0.clone()) > available %}unavailable{% endif %}">
<tr class="{% if offsets.get(loop.index0.clone()).unwrap().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>
</tbody></table>
</div>
{% else %}
{% endmatch %}