feat(config): support *_MB env vars for upload size limits

- Add `applyMegabytesOrBytesEnv` to accept size settings in either bytes or MB
- Prefer `*_BYTES` when set, otherwise convert `*_MB` to bytes with overflow guard
- Add coverage for MB-based environment overrides
- Introduce `static/js/upload-popups.js` to lazy-load and cache popup templatesfeat(config): support *_MB env vars for upload size limits

- Add `applyMegabytesOrBytesEnv` to accept size settings in either bytes or MB
- Prefer `*_BYTES` when set, otherwise convert `*_MB` to bytes with overflow guard
- Add coverage for MB-based environment overrides
- Introduce `static/js/upload-popups.js` to lazy-load and cache popup templates
This commit is contained in:
2026-04-29 01:42:41 +03:00
parent 82acaffdd8
commit 6035ea1eb2
20 changed files with 544 additions and 166 deletions

3
static/popups/about.html Normal file
View File

@@ -0,0 +1,3 @@
<h3>WarpBox</h3>
<p><strong>WarpBox</strong> was made by <strong>Daniel Legt</strong>.</p>
<p>Temporary file boxes, terminal-friendly uploads, and old-web UI charm.</p>

6
static/popups/clear.html Normal file
View File

@@ -0,0 +1,6 @@
<h3>Confirm clear</h3>
<p>This removes the current queue, resets progress, and unlocks the Start upload button.</p>
<div class="copy-fallback-actions">
<button class="win98-button" type="button" id="confirm-clear-yes">Clear</button>
<button class="win98-button" type="button" id="confirm-clear-no">Cancel</button>
</div>

9
static/popups/cli.html Normal file
View File

@@ -0,0 +1,9 @@
<h3>Upload with cURL</h3>
<p>WarpBox accepts normal multipart form uploads through the compatibility endpoint:</p>
<pre>curl \
-F 'files=@./my-file.zip' \
-F 'retention=1h' \
{{ origin }}/upload
</pre>
<h4>Browser flow</h4>
<p>The browser uses the manifest API: it creates a box, uploads each file, and marks failed uploads so the download page does not wait forever.</p>

View File

@@ -0,0 +1,7 @@
<h3>Clipboard access failed</h3>
<p>The browser refused clipboard access. Copy it manually from the field below.</p>
<textarea class="copy-fallback-text" readonly>{{ value }}</textarea>
<div class="copy-fallback-actions">
{{ openLink }}
<button class="win98-button" type="button" id="fallback-close">Close</button>
</div>

View File

@@ -0,0 +1,8 @@
<h3>Duplicate file names detected</h3>
<p>These files have the same names as files already in the queue.</p>
<ol class="duplicate-list">{{ list }}</ol>
<p>Skip them, or append numbers so they become names like <code>file (2).zip</code>.</p>
<div class="copy-fallback-actions">
<button class="win98-button" type="button" id="duplicate-append">Append numbers</button>
<button class="win98-button" type="button" id="duplicate-skip">Skip duplicates</button>
</div>

102
static/popups/examples.html Normal file
View File

@@ -0,0 +1,102 @@
<h3>Upload examples</h3>
<h4>Basic CLI upload</h4>
<pre>curl \
-F 'files=@./photo.png' \
-F 'retention=24h' \
{{ origin }}/upload
</pre>
<h4>Multiple files with password</h4>
<pre>curl \
-F 'files=@./one.png' \
-F 'files=@./two.zip' \
-F 'retention=1h' \
-F 'password=secret-pass' \
{{ origin }}/upload
</pre>
<h4>Go</h4>
<pre>package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
file, err := os.Open("photo.png")
if err != nil { panic(err) }
defer file.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("files", "photo.png")
if err != nil { panic(err) }
if _, err := io.Copy(part, file); err != nil { panic(err) }
_ = writer.WriteField("retention", "1h")
writer.Close()
req, err := http.NewRequest("POST", "{{ origin }}/upload", &body)
if err != nil { panic(err) }
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
}
</pre>
<h4>Java 11+ HttpClient</h4>
<pre>import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class UploadWarpBox {
public static void main(String[] args) throws Exception {
String boundary = "----WarpBoxBoundary" + System.currentTimeMillis();
Path file = Path.of("photo.png");
byte[] prefix = ("--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"retention\"\r\n\r\n" +
"1h\r\n" +
"--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"files\"; filename=\"photo.png\"\r\n" +
"Content-Type: application/octet-stream\r\n\r\n").getBytes();
byte[] suffix = ("\r\n--" + boundary + "--\r\n").getBytes();
byte[] fileBytes = Files.readAllBytes(file);
byte[] body = new byte[prefix.length + fileBytes.length + suffix.length];
System.arraycopy(prefix, 0, body, 0, prefix.length);
System.arraycopy(fileBytes, 0, body, prefix.length, fileBytes.length);
System.arraycopy(suffix, 0, body, prefix.length + fileBytes.length, suffix.length);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("{{ origin }}/upload"))
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build();
HttpResponse&lt;String&gt; response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
</pre>
<h4>JavaScript Node.js</h4>
<pre>import { openAsBlob } from 'node:fs';
const file = await openAsBlob('./photo.png');
const form = new FormData();
form.append('files', file, 'photo.png');
form.append('retention', '1h');
const res = await fetch('{{ origin }}/upload', {
method: 'POST',
body: form
});
console.log(await res.text());
</pre>

17
static/popups/faq.html Normal file
View File

@@ -0,0 +1,17 @@
<h3>Help &amp; FAQ</h3>
<section class="shortcut-section">
<h4>Keyboard shortcuts</h4>
<ul class="shortcut-list">
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">O</span></span><span>Browse for files.</span></li>
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">U</span></span><span>Start the current upload.</span></li>
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">K</span></span><span>Copy the full cURL command.</span></li>
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">L</span></span><span>Copy the share URL after upload.</span></li>
<li><span><span class="kbd">F1</span></span><span>Open this window.</span></li>
<li><span><span class="kbd">Esc</span></span><span>Close menus and popups.</span></li>
</ul>
</section>
<div class="faq-list">
<div class="faq-item"><p><strong>Can I password protect uploads?</strong></p><p>Yes. Set a password in Box Options before starting the upload.</p></div>
<div class="faq-item"><p><strong>What happens if one file fails?</strong></p><p>The failed row stays red, successful files remain available, and WarpBox marks the failed file in the manifest.</p></div>
<div class="faq-item"><p><strong>Are all options server-backed?</strong></p><p>Expiry, password, ZIP download, and one-time download are sent to the backend. Notes like box name, custom slug, and API key mode are saved locally until backend support exists.</p></div>
</div>

View File

@@ -0,0 +1,12 @@
<h3>Upload limits</h3>
<div class="quota-meter-list">
<div class="quota-meter">
<div class="quota-meter-head"><span>Box size</span><span>{{ boxLimit }}</span></div>
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:{{ boxPercent }}%"></span></div>
</div>
<div class="quota-meter">
<div class="quota-meter-head"><span>Single file</span><span>{{ fileLimit }}</span></div>
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:{{ filePercent }}%"></span></div>
</div>
</div>
<p class="quota-note">These values come from the running WarpBox configuration.</p>

View File

@@ -0,0 +1,5 @@
<h3>{{ title }}</h3>
{{ content }}
<div class="copy-fallback-actions">
<button class="win98-button" type="button" id="fallback-close">OK</button>
</div>