Keep artwork on disk and serve every image from iPX (#111)
The page loaded artwork from each publisher's server, or through /api/art, fetched every time, for http-only hosts. Nothing was kept, every visit asked every publisher, and artwork went when a publisher's server did. - The page draws every image a feed or item names from /api/art. The first time, iPX fetches it (only an address a feed or item names, only an image, up to 5 MB, within the feed timeout) and keeps it in art/ beside the database, under a hash of its address with its type beside it (src/art.rs). Later it comes from disk, which marks it as used. - art_cache_mb, a server setting on the admin page, 500 by default, caps what is kept: the sweep before each scan drops the least recently shown until it fits. 0 keeps nothing, and artwork is fetched through iPX each time. Settings saved before it get the default. - Served from iPX's own address, someone else's image must stay an image: nosniff, and a CSP with sandbox, so an SVG opened on its own runs no script as iPX. - The fixture server sends .jpg as image/jpeg, which /api/art requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -956,6 +956,22 @@ test('reading an item updates its feed\'s count without reloading the list', asy
|
||||
expect(lists, 'the row came back with the read, not by reloading the list').toEqual([]);
|
||||
});
|
||||
|
||||
test('artwork comes from iPX, kept, and only as an image', async ({ page }) => {
|
||||
// Every image a feed names is drawn from iPX's address.
|
||||
expect(await page.evaluate(() => artHTML('https://example.com/a.jpg', 'A'))).toContain('src="/api/art?u=https%3A%2F%2Fexample.com%2Fa.jpg"');
|
||||
// Multi Show's items name art.jpg.
|
||||
await expect(page.locator('.feed', { hasText: 'Multi Show' })).toBeVisible({ timeout: 20_000 });
|
||||
const src = '/api/art?u=' + encodeURIComponent('http://127.0.0.1:8792/art.jpg');
|
||||
for (const _ of [1, 2]) { // fetched, then kept
|
||||
const r = await page.request.get(src);
|
||||
expect(r.status()).toBe(200);
|
||||
expect(r.headers()['content-type']).toMatch(/^image\//);
|
||||
expect(r.headers()['content-security-policy']).toContain('sandbox');
|
||||
}
|
||||
// An address no feed names is not fetched.
|
||||
expect((await page.request.get('/api/art?u=' + encodeURIComponent('http://127.0.0.1:8792/show.xml'))).status()).toBe(404);
|
||||
});
|
||||
|
||||
test('a deleted file looks as if it was never downloaded', async ({ page }) => {
|
||||
// Other people subscribe to Picture Blog by now, so both prompts come; take them.
|
||||
page.on('dialog', d => d.accept());
|
||||
|
||||
@@ -12,7 +12,10 @@ http.createServer((req, res) => {
|
||||
fs.readFile(file, (err, body) => {
|
||||
if (err) { res.writeHead(404).end('no'); return; }
|
||||
const type = file.endsWith('.mp3') ? 'audio/mpeg'
|
||||
: file.endsWith('.opml') ? 'text/x-opml' : 'application/xml';
|
||||
: file.endsWith('.opml') ? 'text/x-opml'
|
||||
// Artwork as an image, or /api/art refuses it as not one.
|
||||
: file.endsWith('.jpg') ? 'image/jpeg'
|
||||
: file.endsWith('.html') ? 'text/html' : 'application/xml';
|
||||
res.writeHead(200, { 'content-type': type, 'content-length': body.length });
|
||||
res.end(body);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user