sweeper: release custom URLs on expiry and after 30-day reservation (#29)
CI / test (push) Successful in 17s
CI / docker (push) Skipped

This commit is contained in:
2026-09-08 20:50:40 -05:00
parent 0bbe65ce05
commit 1f162c4003
10 changed files with 233 additions and 17 deletions
+7 -1
View File
@@ -5,7 +5,7 @@
<h1>Public pastes</h1>
<span class="count" id="count"></span>
</div>
<div class="search"><input id="filter" placeholder="Search…"></div>
<div class="search"><input id="filter" placeholder="Search…"><span class="search-spinner" id="search-spinner"></span></div>
<div class="float">
<table>
<colgroup><col style="width:220px"><col><col style="width:156px"><col style="width:138px"><col style="width:111px"><col style="width:165px"></colgroup>
@@ -43,6 +43,9 @@ function matchesFilter(it) {
}
async function load() {
const spinner = document.getElementById('search-spinner');
spinner.style.visibility = 'visible';
try {
const filtered = filter.length > 0;
const off = (page - 1) * PER;
const url = filtered
@@ -95,6 +98,9 @@ async function load() {
add('', page+1, {dis: page===pages});
$('pg').innerHTML = btns.join('');
}
} finally {
spinner.style.visibility = 'hidden';
}
}
$('pg').addEventListener('click', e => {
+35 -10
View File
@@ -4,7 +4,7 @@
<div class="pane-l-col">
<div class="float pane-l-head">
<div class="editor-head">
<input id="title" placeholder="Title (optional)">
<input id="title" placeholder="Title">
<select id="language">
<option value="">auto</option>
<option>go</option><option>python</option><option>javascript</option>
@@ -12,7 +12,7 @@
<option>bash</option><option>sql</option><option>yaml</option><option>json</option>
<option>markdown</option><option>text</option>
</select>
<button class="btn btn-icon" id="reguess" title="Re-detect language" type="button"></button>
<button class="btn btn-icon" id="reguess" title="Re-detect language" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/></svg></button>
</div>
</div>
<div class="float editor-wrap">
@@ -23,7 +23,7 @@
<div class="actionbar">
<span class="hint">Ctrl+Enter to create</span>
<div class="spacer" style="flex:1"></div>
<button class="btn" id="create">Create</button>
<button class="btn" id="create">Create</button>
</div>
</div>
@@ -40,14 +40,17 @@
</div>
<div class="float side-section">
<h3>Protection</h3>
<label class="toggle"><input type="checkbox" id="haspw"> Password lock</label>
<input type="password" id="password" class="pwinput" placeholder="Password" style="display:none; margin: 6px 8px 0; width: auto;">
<label class="toggle"><input type="checkbox" id="burn"> Burn after read</label>
<label class="toggle"><input type="checkbox" id="unlisted"> Unlisted</label>
<div class="protect">
<label class="toggle"><input type="checkbox" id="haspw"> Password lock</label>
<div class="pw-row" id="pwrow" style="display:none"><div class="pw-field"><input type="password" id="password" placeholder="Password" autocomplete="new-password"><button type="button" class="reveal" id="pwreveal" title="Show password" tabindex="-1"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"/><circle cx="12" cy="12" r="3"/></svg></button></div></div>
<label class="toggle"><input type="checkbox" id="burn"> Burn after read</label>
<label class="toggle"><input type="checkbox" id="unlisted"> Unlisted</label>
</div>
</div>
<div class="float side-section">
<h3>Custom URL</h3>
<div class="row" style="justify-content:flex-start; gap:6px; align-items:center;"><span>/</span><input type="text" id="custom" placeholder="my-snippet" style="text-align:left; flex:1; min-width:0;"></div>
<input type="text" id="custom" class="custom-input" placeholder="/my-snippet">
<div class="hint" style="margin-top:6px; font-size:19px;">Stays reserved while the paste exists</div>
</div>
<div class="float side-section" id="result-card" style="display:none">
<h3>Result</h3>
@@ -68,7 +71,21 @@ function updateGutter() {
content.addEventListener('input', updateGutter);
updateGutter();
$('haspw').addEventListener('change', e => { $('password').style.display = e.target.checked ? 'block' : 'none'; });
function toast(msg) {
let t = document.querySelector('.toast');
if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.appendChild(t); }
t.textContent = msg;
t.classList.add('show');
clearTimeout(t._h);
t._h = setTimeout(() => t.classList.remove('show'), 2000);
}
$('haspw').addEventListener('change', e => { $('pwrow').style.display = e.target.checked ? 'block' : 'none'; });
$('pwreveal').addEventListener('click', () => {
const pw = $('password');
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
$('pwreveal').title = show ? 'Hide password' : 'Show password';
});
let guessed = ''; // last auto-detected language, '' = user override
@@ -143,11 +160,19 @@ async function create() {
const url = location.origin + '/' + (data.custom_slug || data.id);
showResult('<a href="' + url + '">' + url + '</a>', false);
$('result').dataset.token = data.deletion_token || '';
try { navigator.clipboard.writeText(url); } catch(e) {}
try { navigator.clipboard.writeText(url); toast('Copied'); } catch(e) {}
// show the paste
location.href = '/' + data.id + '?created=1&token=' + encodeURIComponent(data.deletion_token || '');
}
$('create').addEventListener('click', create);
// reset stale result state when returning via Back (bfcache) (#28)
window.addEventListener('pageshow', e => {
if (!e.persisted) return;
const rc = document.getElementById('result-card');
if (rc) rc.style.display = 'none';
const r = document.getElementById('result');
if (r) { r.innerHTML = 'empty'; delete r.dataset.token; }
});
document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); create(); }
});
+9
View File
@@ -33,8 +33,17 @@
</div>
<input type="hidden" id="raw-content" value="{{.ContentAttr}}">
<script>
function toast(msg) {
let t = document.querySelector('.toast');
if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.appendChild(t); }
t.textContent = msg;
t.classList.add('show');
clearTimeout(t._h);
t._h = setTimeout(() => t.classList.remove('show'), 2000);
}
function copyContent() {
navigator.clipboard.writeText(document.getElementById('raw-content').value);
toast('Copied');
}
function redeem(token) {
if (!confirm('Hard delete this paste immediately?')) return;
+16 -5
View File
@@ -1,18 +1,29 @@
{{template "head" .}}
{{template "topbar" .}}
<div class="center">
<div class="float">
<div class="float unlock-card">
<div class="inner">
<div class="lockring">🔒</div>
<div class="lockring"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<h1>This paste is locked</h1>
<p class="sub">Enter the password to view <span class="slug">/{{.ID}}</span></p>
<form method="post" action="/unlock/{{.ID}}">
<input type="password" name="password" class="pwinput" placeholder="••••••••" autofocus>
{{if .Wrong}}<p class="err" style="display:block">Wrong password. Try again.</p>{{end}}
<form method="post" action="">
<div class="pw-field">
<input type="password" name="password" id="password" placeholder="Password" autocomplete="current-password" autofocus>
<button type="button" class="reveal" id="pwreveal" title="Show password" tabindex="-1"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"/><circle cx="12" cy="12" r="3"/></svg></button>
</div>
{{if .Wrong}}<p class="unlock-err">Wrong password. Try again.</p>{{end}}
<button class="btn" type="submit">Unlock</button>
</form>
</div>
<div class="foot">Created {{.CreatedAgo}}</div>
</div>
</div>
<script>
document.getElementById('pwreveal').addEventListener('click', () => {
const pw = document.getElementById('password');
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
document.getElementById('pwreveal').title = show ? 'Hide password' : 'Show password';
});
</script>
{{template "foot" .}}