openvibers/openvibe.community Commits

Home screenshots: cache public ones like assets (#279)

OP OpenVibers committed 6 days ago · bc8f1716bf · 2 files, +22 −2

The screenshot proxy served every paste's image with "private, no-store", so
each home page visit re-downloaded its six screenshots (3.7 MB of the home's
4 MB, and the LCP element on a phone). A public paste's screenshot cannot
change under /p/<slug>/screenshot, so it now gets the shared immutable asset
policy. Private, burn-after-read and locked pastes keep private, no-store.

Changes

ddiff --git a/server/app.js b/server/app.js
iindex cb15a6d..107abc6 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -633,10 +633,16 @@ async function createApp(opts = {}) {
if (!SLUG_RE.test(req.params.slug)) return notFound(res);
try {
await app.locals.pastes.screenshotUrl(req.viewer, req.params.slug);
-- const row = await db.prepare('SELECT media_ref, screenshot_url FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(req.params.slug);
++ const row = await db.prepare('SELECT media_ref, screenshot_url, visibility, burn_after_read, locked FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(req.params.slug);
const got = row ? await app.locals.pasteMedia.bytes(row) : null;
if (!got) return notFound(res);
-- res.set('X-Content-Type-Options', 'nosniff').set('Cache-Control', cache.htmlHeaders({ private: true }));
++ // A public paste's screenshot never changes under this URL, so it is cached like any other
++ // content-addressed asset: the home page shows six of them and each is several hundred KB,
++ // and re-downloading every one on every visit was most of the home page's 4 MB. Anything
++ // whose audience is a person (private, burn-after-read, locked) stays private and uncached.
++ const shared = row.visibility === 'public' && !Number(row.burn_after_read) && !row.locked;
++ res.set('X-Content-Type-Options', 'nosniff')
++ .set('Cache-Control', shared ? cache.assetHeaders('screenshot.png', { hashed: true }) : cache.htmlHeaders({ private: true }));
res.type(got.type || 'application/octet-stream').send(got.body);
} catch (err) {
if (err.status === 410) return res.status(410).type('text/plain').set('X-Content-Type-Options', 'nosniff').send('This paste has been burned after reading.');
ddiff --git a/test/paste-sharing.test.js b/test/paste-sharing.test.js
iindex 46120d5..b416281 100644
---- a/test/paste-sharing.test.js
++++ b/test/paste-sharing.test.js
@@@ -334,12 +334,26 @@ const pastesApiModule = require('../server/pastes/api');
const bytes = await call(`/p/${slug}/screenshot`, { cookie: maticusJwt });
assert.strictEqual(bytes.status, 200);
assert.strictEqual(bytes.headers.get('content-type'), 'image/jpeg');
++ assert.strictEqual(bytes.headers.get('cache-control'), 'private, no-store', 'a members-only screenshot is never cached');
assert.ok(bytes.text.includes('PIXELS'));
assert.strictEqual((await call(`/p/${slug}/screenshot`, { cookie: samJwt })).status, 404);
assert.strictEqual((await call(`/p/${slug}/screenshot`)).status, 404);
assert.strictEqual((await call(`/p/${slug}/screenshot`, { cookie: alexJwt })).status, 200, 'the owner too');
});
++ await check('a public screenshot is cached like an asset; a members-only one never is', async () => {
++ const jpeg = Buffer.concat([Buffer.from([0xff, 0xd8]), Buffer.from([0xff, 0xda, 0x00, 0x04, 0x00, 0x00]), Buffer.from('PIXELS'), Buffer.from([0xff, 0xd9])]);
++ const fd = new FormData();
++ fd.append('title', 'Shot for everyone');
++ fd.append('audience', 'public');
++ fd.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'p.jpg');
++ const made = await t.get('/api/pastes/screenshot', { method: 'POST', cookies: [`ov_token=${alexJwt}`], body: fd });
++ assert.strictEqual(made.status, 201, made.text);
++ const got = await call(`/p/${made.json().slug}/screenshot`);
++ assert.strictEqual(got.status, 200);
++ assert.strictEqual(got.headers.get('cache-control'), 'public, max-age=31536000, immutable', 'a public screenshot is an immutable asset');
++ });
++
await check('the move script re-hosts, deletes the old object, and retries a failed delete; --keep-old keeps it', async () => {
const { createMediaObjects } = require('../server/media/objects');
const { moveScreenshotsPrivate } = require('../scripts/move-screenshots-private');