Compare commits

..

No commits in common. "70e6b8bec65b84f004675bb51ddf8e5889626262" and "b90220da22a4b4f1435a8130efa9cf47357d82b6" have entirely different histories.

10 changed files with 234 additions and 239 deletions

View file

@ -42,8 +42,7 @@
buildInputs = [ pkgs.makeWrapper ]; buildInputs = [ pkgs.makeWrapper ];
postBuild = '' postBuild = ''
wrapProgram $out/bin/${name} \ wrapProgram $out/bin/${name} \
--set TRANSBEAM_STATIC_DIR ${./static} \ --set TRANSBEAM_STATIC_DIR ${./static}
--set TRANSBEAM_CACHEBUSTER ${builtins.substring 0 8 (builtins.hashString "sha256" (toString ./static))}
''; '';
}; };

View file

@ -6,8 +6,9 @@ mod zip;
use std::{fmt::Debug, path::PathBuf, str::FromStr}; use std::{fmt::Debug, path::PathBuf, str::FromStr};
use actix_files::NamedFile;
use actix_web::{ use actix_web::{
error::InternalError, get, middleware::Logger, post, web, App, HttpRequest, error::InternalError, get, http::StatusCode, middleware::Logger, post, web, App, HttpRequest,
HttpResponse, HttpServer, Responder, HttpResponse, HttpServer, Responder,
}; };
use actix_web_actors::ws; use actix_web_actors::ws;
@ -30,9 +31,9 @@ struct Config {
max_lifetime: u16, max_lifetime: u16,
upload_password: String, upload_password: String,
storage_dir: PathBuf, storage_dir: PathBuf,
static_dir: PathBuf,
reverse_proxy: bool, reverse_proxy: bool,
mnemonic_codes: bool, mnemonic_codes: bool,
cachebuster: String,
} }
pub fn get_ip_addr(req: &HttpRequest, reverse_proxy: bool) -> String { pub fn get_ip_addr(req: &HttpRequest, reverse_proxy: bool) -> String {
@ -50,29 +51,14 @@ pub fn log_auth_failure(ip_addr: &str) {
warn!("Incorrect authentication attempt from {}", ip_addr); warn!("Incorrect authentication attempt from {}", ip_addr);
} }
#[derive(Template)]
#[template(path = "index.html")]
struct IndexPage<'a> { cachebuster: &'a str }
#[get("/")]
async fn index(data: web::Data<AppState>) -> impl Responder {
HttpResponse::Ok().body(IndexPage { cachebuster: &data.config.cachebuster }.render().unwrap())
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct DownloadRequest { struct DownloadRequest {
code: String, code: String,
download: Option<download::DownloadSelection>, download: Option<download::DownloadSelection>,
} }
#[derive(Template)] #[derive(Serialize, Template)]
#[template(path = "download.html")] #[template(path = "download.html")]
struct DownloadPage<'a> {
info: DownloadInfo,
cachebuster: &'a str,
}
#[derive(Serialize)]
struct DownloadInfo { struct DownloadInfo {
file: StoredFile, file: StoredFile,
code: String, code: String,
@ -119,14 +105,11 @@ async fn handle_download(
} else { } else {
let offsets = info.contents.as_deref().map(zip::file_data_offsets); let offsets = info.contents.as_deref().map(zip::file_data_offsets);
Ok(HttpResponse::Ok().body( Ok(HttpResponse::Ok().body(
DownloadPage { DownloadInfo {
info: DownloadInfo { file: info,
file: info, code: code.clone(),
code: code.clone(), available: file.metadata().await?.len(),
available: file.metadata().await?.len(), offsets,
offsets,
},
cachebuster: &data.config.cachebuster,
} }
.render() .render()
.unwrap(), .unwrap(),
@ -166,10 +149,6 @@ async fn download_info(
})) }))
} }
#[derive(Template)]
#[template(path = "404.html")]
struct NotFoundPage<'a> { cachebuster: &'a str }
fn not_found<T>(req: HttpRequest, data: web::Data<AppState>, report: bool) -> actix_web::Result<T> { 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);
@ -177,7 +156,9 @@ fn not_found<T>(req: HttpRequest, data: web::Data<AppState>, report: bool) -> ac
} }
Err(InternalError::from_response( Err(InternalError::from_response(
"Download not found", "Download not found",
HttpResponse::NotFound().body(NotFoundPage { cachebuster: &data.config.cachebuster }.render().unwrap()), NamedFile::open(data.config.static_dir.join("404.html"))?
.set_status_code(StatusCode::NOT_FOUND)
.into_response(&req),
) )
.into()) .into())
} }
@ -277,7 +258,6 @@ async fn main() -> std::io::Result<()> {
env_or::<ByteSize>("TRANSBEAM_MAX_STORAGE_SIZE", ByteSize(64 * bytesize::GB)).as_u64(); env_or::<ByteSize>("TRANSBEAM_MAX_STORAGE_SIZE", ByteSize(64 * bytesize::GB)).as_u64();
let upload_password: String = let upload_password: String =
std::env::var("TRANSBEAM_UPLOAD_PASSWORD").expect("TRANSBEAM_UPLOAD_PASSWORD must be set!"); std::env::var("TRANSBEAM_UPLOAD_PASSWORD").expect("TRANSBEAM_UPLOAD_PASSWORD must be set!");
let cachebuster: String = env_or_else("TRANSBEAM_CACHEBUSTER", String::new);
let data = web::Data::new(AppState { let data = web::Data::new(AppState {
file_store: RwLock::new(FileStore::load(storage_dir.clone(), max_storage_size).await?), file_store: RwLock::new(FileStore::load(storage_dir.clone(), max_storage_size).await?),
@ -286,9 +266,9 @@ async fn main() -> std::io::Result<()> {
max_lifetime, max_lifetime,
upload_password, upload_password,
storage_dir, storage_dir,
static_dir: static_dir.clone(),
reverse_proxy, reverse_proxy,
mnemonic_codes, mnemonic_codes,
cachebuster,
}, },
}); });
start_reaper(data.clone()); start_reaper(data.clone());
@ -301,13 +281,12 @@ async fn main() -> std::io::Result<()> {
} else { } else {
Logger::default() Logger::default()
}) })
.service(index)
.service(handle_download) .service(handle_download)
.service(download_info) .service(download_info)
.service(handle_upload) .service(handle_upload)
.service(check_upload_password) .service(check_upload_password)
.service(upload_limits) .service(upload_limits)
.service(actix_files::Files::new("/", static_dir.clone())) .service(actix_files::Files::new("/", static_dir.clone()).index_file("index.html"))
}); });
if reverse_proxy { if reverse_proxy {

39
static/404.html Normal file
View file

@ -0,0 +1,39 @@
<!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/colors.css"/>
<link rel="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
<link rel="manifest" href="manifest.json"/>
<script src="js/download.js"></script>
<title>transbeam</title>
</head>
<body>
<div id="header">
<a href="./">
<img src="images/site-icons/transbeam.svg" height="128">
<h1>transbeam</h1>
</a>
</div>
<div id="download" class="section">
<h3>The download code you entered wasn't found. The download may have expired.</h3>
<form id="download_form" action="download" method="get">
<div>
<label>
<input type="text" id="download_code_input" name="code" placeholder="Download code"/>
</label>
</div>
<input id="download_button" type="submit" value="Download"/>
</form>
</div>
<div class="section">
<a href="./"><h3>&lt; Back</h3></a>
</div>
<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>

View file

@ -1,31 +1,35 @@
:root { @media not all and (prefers-color-scheme: dark) {
--success-primary: #060; :root {
--success-secondary: #0a0; color-scheme: light;
--failure-primary: #d00; --success-primary: #060;
--failure-secondary: #f24; --success-secondary: #0a0;
--progress-primary: #27f; --failure-primary: #d00;
--progress-secondary: #48f; --failure-secondary: #f24;
--progress-tertiary: #7af; --progress-primary: #27f;
--border: #ddd; --progress-secondary: #48f;
--border-active: #777; --progress-tertiary: #7af;
--icon-primary: #333; --border: #ddd;
--icon-primary-active: #000; --border-active: #777;
--icon-secondary: #888; --icon-primary: #333;
--icon-warning: #f00; --icon-primary-active: #000;
--icon-ready: #33f; --icon-secondary: #888;
--icon-ready-active: #00b; --icon-warning: #f00;
--icon-unavailable: #999; --icon-ready: #33f;
--button-text: #000; --icon-ready-active: #00b;
--button-background: #ccc; --icon-unavailable: #999;
--button-background-active: #aaa; --button-text: #000;
--button-border: #bbb; --button-background: #ccc;
--button-disabled-text: #666; --button-background-active: #aaa;
--button-disabled-background: #eee; --button-border: #bbb;
--button-disabled-border: #ddd; --button-disabled-text: #666;
--button-disabled-background: #eee;
--button-disabled-border: #ddd;
}
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
color-scheme: dark;
--success-primary: #0d0; --success-primary: #0d0;
--success-secondary: #080; --success-secondary: #080;
--failure-primary: #f34; --failure-primary: #f34;

View file

@ -9,7 +9,6 @@ body {
#header h1 { #header h1 {
margin-top: 5px; margin-top: 5px;
color: CanvasText;
} }
#header a { #header a {

100
static/index.html Normal file
View file

@ -0,0 +1,100 @@
<!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/states.css"/>
<link rel="stylesheet" type="text/css" href="css/colors.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-landing.js"></script>
<script src="js/upload.js"></script>
<title>transbeam</title>
</head>
<body class="noscript landing">
<div id="header">
<img src="images/site-icons/transbeam.svg" height="128">
<h1>transbeam</h1>
</div>
<div id="download" class="section">
<h3 class="section_heading">Download</h3>
<form id="download_form" action="download" method="get">
<div>
<label>
<input type="text" id="download_code_input" name="code" placeholder="Download code"/>
</label>
</div>
<input id="download_button" type="submit" value="Download"/>
</form>
</div>
<noscript>Javascript is required to upload files :(</noscript>
<div id="uploads_closed_notice" class="section">
<h4>Uploading is currently closed.</h4>
</div>
<div id="upload" class="section">
<h3 class="section_heading">Upload</h3>
<div id="message"></div>
<div>
<form id="upload_password_form">
<div>
<label>
<input id="upload_password" type="password" placeholder="Password"/>
</label>
</div>
<div>
<input type="submit" id="submit_upload_password" value="Submit" />
</div>
</form>
</div>
<div id="upload_controls">
<div id="upload_settings">
<div>
<button id="upload_button">Upload</button>
</div>
<div id="lifetime_container">
<label>
Keep files for:
<select id="lifetime">
<option value="1">1 day</option>
<option value="7">1 week</option>
<option value="14" selected>2 weeks</option>
<option value="30">1 month</option>
<option value="60">2 months</option>
<option value="90">3 months</option>
<option value="180">6 months</option>
<option value="365">1 year</option>
</select>
</label>
</div>
</div>
<div id="download_code_container">
<div id="download_code_main">
<div>Download code: <span id="download_code"></span></div><div class="copy_button"></div>
</div>
<div id="copied_message">Link copied!</div>
</div>
<div id="progress_container">
<div id="progress">
<div id="progress_percentage"></div>
<div id="progress_size"></div>
<div id="progress_rate"></div>
<div id="progress_eta"></div>
</div>
<div id="progress_bar"></div>
</div>
<table id="file_list">
</table>
<label id="file_input_container">
<input type="file" multiple id="file_input"/>
<span class="fake_button" id="file_input_message">Select files to upload...</span>
</label>
</div>
</div>
<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>

View file

@ -1,24 +0,0 @@
{% extends "base.html" %}
{% block title %}Download not found - transbeam{% endblock %}
{% block head %}
<script src="js/download-landing.js?{{ cachebuster }}"></script>
{% endblock %}
{% block body %}
<div id="download" class="section">
<h3>The download code you entered wasn't found. The download may have expired.</h3>
<form id="download_form" action="download" method="get">
<div>
<label>
<input type="text" id="download_code_input" name="code" placeholder="Download code"/>
</label>
</div>
<input id="download_button" type="submit" value="Download"/>
</form>
</div>
<div class="section">
<a href="./"><h3>&lt; Back</h3></a>
</div>
{% endblock %}

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" type="text/css" href="css/transbeam.css?{{ cachebuster }}"/>
<link rel="stylesheet" type="text/css" href="css/colors.css?{{ cachebuster }}"/>
<link rel="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
<link rel="manifest" href="manifest.json"/>
<title>{% block title %}transbeam{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body {% block body_attrs %}{% endblock %}>
<div id="header">
<a href="./">
<img src="images/site-icons/transbeam.svg" height="128">
<h1>transbeam</h1>
</a>
</div>
{% block body %}{% endblock %}
<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>

View file

@ -1,39 +1,52 @@
{% extends "base.html" %} <!DOCTYPE html>
<html lang="en">
{% block title %}{{ info.file.name }} - transbeam{% endblock %} <head>
<meta charset="utf-8"/>
{% block head %} <meta name="viewport" content="width=device-width, initial-scale=1"/>
<script src="js/util.js?{{ cachebuster }}"></script> <link rel="stylesheet" type="text/css" href="css/transbeam.css"/>
<script type="text/javascript"> <link rel="stylesheet" type="text/css" href="css/colors.css"/>
const CODE = "{{ info.code }}"; <link rel="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
</script> <link rel="manifest" href="manifest.json"/>
<script src="js/download.js?{{ cachebuster }}"></script> <script src="js/util.js"></script>
{% endblock %} <script type="text/javascript">
const CODE = "{{ code }}";
{% block body %} </script>
<div id="download_toplevel" class="section"> <script src="js/download.js"></script>
<div class="file_name">{{ info.file.name }}</div> <title>{{ file.name }} - transbeam</title>
<div class="file_size">{{ bytesize::to_string(info.file.size.clone(), false).replace(" ", "") }}</div> </head>
<div class="file_download"><a class="download_button" href="download?code={{ info.code }}&download=all"></a></div> <body>
</div> <div id="header">
{% match info.file.contents %} <img src="images/site-icons/transbeam.svg" height="128">
{% when Some with (files) %} <h1>transbeam</h1>
<div id="download_contents" class="section"> </div>
<details> <div id="download_toplevel" class="section">
<summary>Show file list</summary> <div class="file_name">{{ file.name }}</div>
<table><tbody> <div class="file_size">{{ bytesize::to_string(file.size.clone(), false).replace(" ", "") }}</div>
{% let offsets = info.offsets.as_ref().unwrap() %} <div class="file_download"><a class="download_button" href="download?code={{ code }}&download=all"></a></div>
{% for f in files %} </div>
<tr class="{% if offsets.get(loop.index0.clone()).unwrap().clone() > info.available %}unavailable{% endif %}"> {% match file.contents %}
<td class="file_size">{{ bytesize::to_string(f.size.clone(), false).replace(" ", "") }}</td> {% when Some with (files) %}
<td class="file_name">{{ f.name }}</td> <div id="download_contents" class="section">
<td class="file_download"><a class="download_button" href="download?code={{ info.code }}&download={{ loop.index0 }}"></a></td> <details>
<td class="file_unavailable"></td> <summary>Show file list</summary>
</tr> <table><tbody>
{% endfor %} {% let offsets = offsets.as_ref().unwrap() %}
</tbody></table> {% for f in files %}
</details> <tr class="{% if offsets.get(loop.index0.clone()).unwrap().clone() > available %}unavailable{% endif %}">
</div> <td class="file_size">{{ bytesize::to_string(f.size.clone(), false).replace(" ", "") }}</td>
{% else %} <td class="file_name">{{ f.name }}</td>
{% endmatch %} <td class="file_download"><a class="download_button" href="download?code={{ code }}&download={{ loop.index0 }}"></a></td>
{% endblock %} <td class="file_unavailable"></td>
</tr>
{% endfor %}
</tbody></table>
</details>
</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>

View file

@ -1,87 +0,0 @@
{% extends "base.html" %}
{% block head %}
<link rel="stylesheet" type="text/css" href="css/states.css?{{ cachebuster }}"/>
<script src="js/util.js?{{ cachebuster }}"></script>
<script src="js/download-landing.js?{{ cachebuster }}"></script>
<script src="js/upload.js?{{ cachebuster }}"></script>
{% endblock %}
{% block body_attrs %}class="noscript landing"{% endblock %}
{% block body %}
<div id="download" class="section">
<h3 class="section_heading">Download</h3>
<form id="download_form" action="download" method="get">
<div>
<label>
<input type="text" id="download_code_input" name="code" placeholder="Download code"/>
</label>
</div>
<input id="download_button" type="submit" value="Download"/>
</form>
</div>
<noscript>Javascript is required to upload files :(</noscript>
<div id="uploads_closed_notice" class="section">
<h4>Uploading is currently closed.</h4>
</div>
<div id="upload" class="section">
<h3 class="section_heading">Upload</h3>
<div id="message"></div>
<div>
<form id="upload_password_form">
<div>
<label>
<input id="upload_password" type="password" placeholder="Password"/>
</label>
</div>
<div>
<input type="submit" id="submit_upload_password" value="Submit" />
</div>
</form>
</div>
<div id="upload_controls">
<div id="upload_settings">
<div>
<button id="upload_button">Upload</button>
</div>
<div id="lifetime_container">
<label>
Keep files for:
<select id="lifetime">
<option value="1">1 day</option>
<option value="7">1 week</option>
<option value="14" selected>2 weeks</option>
<option value="30">1 month</option>
<option value="60">2 months</option>
<option value="90">3 months</option>
<option value="180">6 months</option>
<option value="365">1 year</option>
</select>
</label>
</div>
</div>
<div id="download_code_container">
<div id="download_code_main">
<div>Download code: <span id="download_code"></span></div><div class="copy_button"></div>
</div>
<div id="copied_message">Link copied!</div>
</div>
<div id="progress_container">
<div id="progress">
<div id="progress_percentage"></div>
<div id="progress_size"></div>
<div id="progress_rate"></div>
<div id="progress_eta"></div>
</div>
<div id="progress_bar"></div>
</div>
<table id="file_list">
</table>
<label id="file_input_container">
<input type="file" multiple id="file_input"/>
<span class="fake_button" id="file_input_message">Select files to upload...</span>
</label>
</div>
</div>
{% endblock %}