From 4f9d46cc28cddd7171949e9003dade37ece0216e Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Thu, 26 May 2022 14:43:03 -0400
Subject: [PATCH 1/2] cachebusting and templating

---
 flake.nix               |   3 +-
 src/main.rs             |  49 ++++++++++++++------
 static/404.html         |  39 ----------------
 static/index.html       | 100 ----------------------------------------
 templates/404.html      |  24 ++++++++++
 templates/base.html     |  27 +++++++++++
 templates/download.html |  91 ++++++++++++++++--------------------
 templates/index.html    |  87 ++++++++++++++++++++++++++++++++++
 8 files changed, 214 insertions(+), 206 deletions(-)
 delete mode 100644 static/404.html
 delete mode 100644 static/index.html
 create mode 100644 templates/404.html
 create mode 100644 templates/base.html
 create mode 100644 templates/index.html

diff --git a/flake.nix b/flake.nix
index ece7d09..f6fdb6f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -42,7 +42,8 @@
             buildInputs = [ pkgs.makeWrapper ];
             postBuild = ''
               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))}
             '';
           };
 
diff --git a/src/main.rs b/src/main.rs
index 2fa3456..76ff8d9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,9 +6,8 @@ mod zip;
 
 use std::{fmt::Debug, path::PathBuf, str::FromStr};
 
-use actix_files::NamedFile;
 use actix_web::{
-    error::InternalError, get, http::StatusCode, middleware::Logger, post, web, App, HttpRequest,
+    error::InternalError, get, middleware::Logger, post, web, App, HttpRequest,
     HttpResponse, HttpServer, Responder,
 };
 use actix_web_actors::ws;
@@ -31,9 +30,9 @@ struct Config {
     max_lifetime: u16,
     upload_password: String,
     storage_dir: PathBuf,
-    static_dir: PathBuf,
     reverse_proxy: bool,
     mnemonic_codes: bool,
+    cachebuster: String,
 }
 
 pub fn get_ip_addr(req: &HttpRequest, reverse_proxy: bool) -> String {
@@ -51,14 +50,29 @@ pub fn log_auth_failure(ip_addr: &str) {
     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)]
 struct DownloadRequest {
     code: String,
     download: Option<download::DownloadSelection>,
 }
 
-#[derive(Serialize, Template)]
+#[derive(Template)]
 #[template(path = "download.html")]
+struct DownloadPage<'a> {
+    info: DownloadInfo,
+    cachebuster: &'a str,
+}
+
+#[derive(Serialize)]
 struct DownloadInfo {
     file: StoredFile,
     code: String,
@@ -105,11 +119,14 @@ async fn handle_download(
     } else {
         let offsets = info.contents.as_deref().map(zip::file_data_offsets);
         Ok(HttpResponse::Ok().body(
-            DownloadInfo {
-                file: info,
-                code: code.clone(),
-                available: file.metadata().await?.len(),
-                offsets,
+            DownloadPage {
+                info: DownloadInfo {
+                    file: info,
+                    code: code.clone(),
+                    available: file.metadata().await?.len(),
+                    offsets,
+                },
+                cachebuster: &data.config.cachebuster,
             }
             .render()
             .unwrap(),
@@ -149,6 +166,10 @@ 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> {
     if report {
         let ip_addr = get_ip_addr(&req, data.config.reverse_proxy);
@@ -156,9 +177,7 @@ fn not_found<T>(req: HttpRequest, data: web::Data<AppState>, report: bool) -> ac
     }
     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),
+        HttpResponse::NotFound().body(NotFoundPage { cachebuster: &data.config.cachebuster }.render().unwrap()),
     )
     .into())
 }
@@ -258,6 +277,7 @@ async fn main() -> std::io::Result<()> {
         env_or::<ByteSize>("TRANSBEAM_MAX_STORAGE_SIZE", ByteSize(64 * bytesize::GB)).as_u64();
     let upload_password: String =
         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 {
         file_store: RwLock::new(FileStore::load(storage_dir.clone(), max_storage_size).await?),
@@ -266,9 +286,9 @@ async fn main() -> std::io::Result<()> {
             max_lifetime,
             upload_password,
             storage_dir,
-            static_dir: static_dir.clone(),
             reverse_proxy,
             mnemonic_codes,
+            cachebuster,
         },
     });
     start_reaper(data.clone());
@@ -281,12 +301,13 @@ async fn main() -> std::io::Result<()> {
             } else {
                 Logger::default()
             })
+            .service(index)
             .service(handle_download)
             .service(download_info)
             .service(handle_upload)
             .service(check_upload_password)
             .service(upload_limits)
-            .service(actix_files::Files::new("/", static_dir.clone()).index_file("index.html"))
+            .service(actix_files::Files::new("/", static_dir.clone()))
     });
 
     if reverse_proxy {
diff --git a/static/404.html b/static/404.html
deleted file mode 100644
index bfc07c8..0000000
--- a/static/404.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!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>
diff --git a/static/index.html b/static/index.html
deleted file mode 100644
index 3719884..0000000
--- a/static/index.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!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>
diff --git a/templates/404.html b/templates/404.html
new file mode 100644
index 0000000..55979fa
--- /dev/null
+++ b/templates/404.html
@@ -0,0 +1,24 @@
+{% 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 %}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..0bc7050
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,27 @@
+<!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>
diff --git a/templates/download.html b/templates/download.html
index a834c2a..8675972 100644
--- a/templates/download.html
+++ b/templates/download.html
@@ -1,52 +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/util.js"></script>
-    <script type="text/javascript">
-      const CODE = "{{ code }}";
-    </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">
-          <details>
-            <summary>Show file list</summary>
-            <table><tbody>
-              {% let offsets = offsets.as_ref().unwrap() %}
-              {% for f in files %}
-                <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 %}
-            </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>
+{% extends "base.html" %}
+
+{% block title %}{{ info.file.name }} - transbeam{% endblock %}
+
+{% block head %}
+  <script src="js/util.js?{{ cachebuster }}"></script>
+  <script type="text/javascript">
+    const CODE = "{{ info.code }}";
+  </script>
+  <script src="js/download.js?{{ cachebuster }}"></script>
+{% endblock %}
+
+{% block body %}
+  <div id="download_toplevel" class="section">
+    <div class="file_name">{{ info.file.name }}</div>
+    <div class="file_size">{{ bytesize::to_string(info.file.size.clone(), false).replace(" ", "") }}</div>
+    <div class="file_download"><a class="download_button" href="download?code={{ info.code }}&download=all"></a></div>
+  </div>
+  {% match info.file.contents %}
+    {% when Some with (files) %}
+      <div id="download_contents" class="section">
+        <details>
+          <summary>Show file list</summary>
+          <table><tbody>
+            {% let offsets = info.offsets.as_ref().unwrap() %}
+            {% for f in files %}
+              <tr class="{% if offsets.get(loop.index0.clone()).unwrap().clone() > info.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={{ info.code }}&download={{ loop.index0 }}"></a></td>
+                <td class="file_unavailable"></td>
+              </tr>
+            {% endfor %}
+          </tbody></table>
+        </details>
+      </div>
+    {% else %}
+  {% endmatch %}
+{% endblock %}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..81b3612
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,87 @@
+{% 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 %}

From 70e6b8bec65b84f004675bb51ddf8e5889626262 Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Thu, 26 May 2022 14:43:21 -0400
Subject: [PATCH 2/2] clean up css color scheme stuff a bit

---
 static/css/colors.css    | 52 +++++++++++++++++++---------------------
 static/css/transbeam.css |  1 +
 2 files changed, 25 insertions(+), 28 deletions(-)

diff --git a/static/css/colors.css b/static/css/colors.css
index 65bd95a..e9914ac 100644
--- a/static/css/colors.css
+++ b/static/css/colors.css
@@ -1,35 +1,31 @@
-@media not all and (prefers-color-scheme: dark) {
-    :root {
-        color-scheme: light;
-        --success-primary: #060;
-        --success-secondary: #0a0;
-        --failure-primary: #d00;
-        --failure-secondary: #f24;
-        --progress-primary: #27f;
-        --progress-secondary: #48f;
-        --progress-tertiary: #7af;
-        --border: #ddd;
-        --border-active: #777;
-        --icon-primary: #333;
-        --icon-primary-active: #000;
-        --icon-secondary: #888;
-        --icon-warning: #f00;
-        --icon-ready: #33f;
-        --icon-ready-active: #00b;
-        --icon-unavailable: #999;
-        --button-text: #000;
-        --button-background: #ccc;
-        --button-background-active: #aaa;
-        --button-border: #bbb;
-        --button-disabled-text: #666;
-        --button-disabled-background: #eee;
-        --button-disabled-border: #ddd;
-    }
+:root {
+    --success-primary: #060;
+    --success-secondary: #0a0;
+    --failure-primary: #d00;
+    --failure-secondary: #f24;
+    --progress-primary: #27f;
+    --progress-secondary: #48f;
+    --progress-tertiary: #7af;
+    --border: #ddd;
+    --border-active: #777;
+    --icon-primary: #333;
+    --icon-primary-active: #000;
+    --icon-secondary: #888;
+    --icon-warning: #f00;
+    --icon-ready: #33f;
+    --icon-ready-active: #00b;
+    --icon-unavailable: #999;
+    --button-text: #000;
+    --button-background: #ccc;
+    --button-background-active: #aaa;
+    --button-border: #bbb;
+    --button-disabled-text: #666;
+    --button-disabled-background: #eee;
+    --button-disabled-border: #ddd;
 }
 
 @media (prefers-color-scheme: dark) {
     :root {
-        color-scheme: dark;
         --success-primary: #0d0;
         --success-secondary: #080;
         --failure-primary: #f34;
diff --git a/static/css/transbeam.css b/static/css/transbeam.css
index 92361c0..c0d48c9 100644
--- a/static/css/transbeam.css
+++ b/static/css/transbeam.css
@@ -9,6 +9,7 @@ body {
 
 #header h1 {
     margin-top: 5px;
+    color: CanvasText;
 }
 
 #header a {