api/ui: Allow per-scratch TTL down to 1m
Format / gofmt (push) Successful in 13s
CI / Build (push) Successful in 22s
CI / Go Tests (push) Successful in 23s
Format / gofmt (pull_request) Successful in 12s
CI / Build (pull_request) Successful in 20s
CI / Go Tests (pull_request) Successful in 20s

#4
This commit is contained in:
s1d3sw1ped_bot
2026-09-01 21:16:36 +00:00
parent 1d301d29b6
commit de465d6818
6 changed files with 439 additions and 12 deletions
+106 -2
View File
@@ -7,6 +7,8 @@
let maxUploadSizeBytes = $state(0);
let defaultTTL = $state("15m");
let maxTTL = $state("24h");
let selectedTTL = $state("15m");
let uploadAllowed = $state(true);
let configError = $state("");
@@ -29,6 +31,8 @@
const maxUploadSizeDisplay = $derived(formatBytes(maxUploadSizeBytes, true));
const defaultTTLDisplay = $derived(formatTTLDisplay(defaultTTL));
const maxTTLDisplay = $derived(formatTTLDisplay(maxTTL));
const ttlOptions = $derived(buildTTLOptions(defaultTTL, maxTTL));
const viewPageTitle = $derived(
viewMeta?.filename
@@ -101,11 +105,15 @@
maxUploadSizeBytes = Number.isFinite(max) ? max : 0;
uploadAllowed = Boolean(data.upload_allowed);
defaultTTL = String(data.default_ttl ?? "").trim() || "15m";
maxTTL = String(data.max_ttl ?? "").trim() || "24h";
selectedTTL = defaultTTL;
} catch (err) {
configError = err instanceof Error ? err.message : String(err);
maxUploadSizeBytes = 0;
uploadAllowed = true;
defaultTTL = "15m";
maxTTL = "24h";
selectedTTL = defaultTTL;
}
}
@@ -305,6 +313,85 @@
}
});
function parseTTLToSeconds(rawTTL) {
const source = String(rawTTL ?? "").trim().toLowerCase();
if (!source) {
return NaN;
}
const matches = Array.from(source.matchAll(/(\d+)\s*([hms])/g));
if (matches.length === 0) {
return NaN;
}
let total = 0;
for (const match of matches) {
const value = Number.parseInt(match[1], 10);
if (!Number.isFinite(value) || value < 0) {
continue;
}
const unit = match[2];
if (unit === "h") {
total += value * 3600;
} else if (unit === "m") {
total += value * 60;
} else if (unit === "s") {
total += value;
}
}
return total;
}
function buildTTLOptions(defaultRaw, maxRaw) {
const presets = ["1m", "5m", "15m", "30m", "1h", "6h", "12h", "24h"];
const maxSeconds = parseTTLToSeconds(maxRaw);
const defaultSeconds = parseTTLToSeconds(defaultRaw);
const seen = new Set();
const options = [];
function addOption(value) {
const seconds = parseTTLToSeconds(value);
if (!Number.isFinite(seconds) || seconds < 60) {
return;
}
if (Number.isFinite(maxSeconds) && seconds > maxSeconds) {
return;
}
if (seen.has(seconds)) {
return;
}
seen.add(seconds);
options.push({
value,
label: formatTTLDisplay(value),
seconds
});
}
for (const preset of presets) {
addOption(preset);
}
addOption(defaultRaw);
addOption(maxRaw);
options.sort((a, b) => a.seconds - b.seconds);
// Ensure selected/default is present even if parsing failed above.
if (defaultRaw && !options.some((opt) => opt.value === defaultRaw)) {
options.unshift({
value: defaultRaw,
label: formatTTLDisplay(defaultRaw),
seconds: Number.isFinite(defaultSeconds) ? defaultSeconds : 0
});
}
return options.map((opt) => ({
value: opt.value,
label:
opt.value === defaultRaw
? `${opt.label} (default)`
: opt.label
}));
}
function expiresInText(expiresAt, now) {
const expiry = Date.parse(String(expiresAt ?? ""));
if (!Number.isFinite(expiry)) {
@@ -393,6 +480,8 @@
try {
const formData = new FormData();
formData.append("file", selectedFile);
const ttlValue = String(selectedTTL ?? "").trim() || defaultTTL;
formData.append("ttl", ttlValue);
const response = await fetch("/api/scratch", {
method: "POST",
body: formData
@@ -509,7 +598,7 @@
{:else if routeMode === "upload"}
<section class="panel">
<p class="helper">Size limit: {maxUploadSizeDisplay}</p>
<p class="helper">Expiration: {defaultTTLDisplay}</p>
<p class="helper">Default expiration: {defaultTTLDisplay} (max {maxTTLDisplay})</p>
{#if configError}
<p class="helper warning">Could not load UI config: {configError}</p>
{/if}
@@ -530,6 +619,20 @@
/>
</section>
<section id="ttl-panel" class="input-panel">
<label for="ttl">Expiration</label>
<select
id="ttl"
name="ttl"
bind:value={selectedTTL}
disabled={loading}
>
{#each ttlOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</section>
<button class="link-button nav-button action-button submit-button" type="submit" disabled={loading}>
{#if loading}Creating...{:else}Create scratch{/if}
</button>
@@ -657,7 +760,8 @@
margin-top: 1rem;
}
input[type="file"] {
input[type="file"],
select {
margin-top: 0.5rem;
width: 100%;
background: #121826;