openvibers/openvibe.community Commits
Home screenshots: cache public ones like assets (#279)
Changes
@@@ -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.');@@@ -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');
@@ -633,10 +633,16 @@ async function createApp(opts = {}) {
@@ -633,10 +633,16 @@ async function createApp(opts = {}) {
if (!SLUG_RE.test(req.params.slug)) return notFound(res);
if (!SLUG_RE.test(req.params.slug)) return notFound(res);
try {
try {
await app.locals.pastes.screenshotUrl(req.viewer, req.params.slug);
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;
const got = row ? await app.locals.pasteMedia.bytes(row) : null;
if (!got) return notFound(res);
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);
res.type(got.type || 'application/octet-stream').send(got.body);
} catch (err) {
} 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.');
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.');
@@ -334,12 +334,26 @@ const pastesApiModule = require('../server/pastes/api');
@@ -334,12 +334,26 @@ const pastesApiModule = require('../server/pastes/api');
const bytes = await call(`/p/${slug}/screenshot`, { cookie: maticusJwt });
const bytes = await call(`/p/${slug}/screenshot`, { cookie: maticusJwt });
assert.strictEqual(bytes.status, 200);
assert.strictEqual(bytes.status, 200);
assert.strictEqual(bytes.headers.get('content-type'), 'image/jpeg');
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.ok(bytes.text.includes('PIXELS'));
assert.strictEqual((await call(`/p/${slug}/screenshot`, { cookie: samJwt })).status, 404);
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`)).status, 404);
assert.strictEqual((await call(`/p/${slug}/screenshot`, { cookie: alexJwt })).status, 200, 'the owner too');
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 () => {
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 { createMediaObjects } = require('../server/media/objects');
const { moveScreenshotsPrivate } = require('../scripts/move-screenshots-private');
const { moveScreenshotsPrivate } = require('../scripts/move-screenshots-private');