fix: prewiev Bot expires links

This commit is contained in:
portakal 2026-06-16 15:19:37 +03:00
commit cbf41d5022
2 changed files with 41 additions and 1 deletions

View file

@ -4,7 +4,7 @@ import { pipeline } from 'node:stream/promises';
import express from 'express';
import { queryOne, execute } from '../db.js';
import { rawPath, gzPath } from '../storage.js';
import { gonePage } from '../views.js';
import { gonePage, downloadLandingPage } from '../views.js';
import { ah } from '../util.js';
import log from '../log.js';
@ -15,8 +15,35 @@ function contentDisposition(name) {
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(name)}`;
}
// GET shows a landing page and does NOT consume the link. This is deliberate: chat
// apps and email scanners (WhatsApp, Slack, iMessage, ...) fetch the URL to build a
// preview, and if GET burned the token the link would be "used" before the human
// clicks. The one-time consumption happens on the POST below.
router.get('/d/:token', ah(async (req, res) => {
const token = req.params.token || '';
const row = await queryOne(
`SELECT f.original_name, f.size_bytes, f.is_bundle, dl.used, dl.expires_at > NOW() AS valid
FROM download_links dl JOIN files f ON f.id = dl.file_id
WHERE dl.token = ?`,
[token]
);
if (!row || row.used || !row.valid) {
return res.status(410).send(gonePage());
}
res.send(
downloadLandingPage({
token,
name: row.original_name,
size: row.size_bytes,
isBundle: row.is_bundle,
})
);
}));
// POST performs the actual one-time download (triggered by the button on the landing
// page). Preview bots issue GET, never POST, so the link survives until a real click.
router.post('/d/:token', ah(async (req, res) => {
const token = req.params.token || '';
// Atomically reserve the one-time link: succeeds only if unused AND unexpired.
const result = await execute(

View file

@ -203,6 +203,19 @@ export function dashboardPage({ user, files, disk, flash, newLink }) {
return layout({ title: 'My files', body, user, flash, active: 'dash' });
}
export function downloadLandingPage({ token, name, size, isBundle }) {
const body = `<section class="card narrow">
<h1>You've received a file</h1>
<p class="fname">${esc(name)}</p>
<p class="sub">${humanBytes(size)}${isBundle ? ' · zip bundle' : ''}</p>
<form method="post" action="${u(`/d/${token}`)}">
<button class="btn" type="submit">Download</button>
</form>
<p class="muted" style="margin-top:14px">This is a one-time link it works once, then expires.</p>
</section>`;
return layout({ title: 'Download', body, user: null });
}
export function gonePage() {
return messagePage({
title: 'Link unavailable',