openvibers/openvibe.media Commits
Media: a private image takes a variant with its signature (the drive's previews), private and no-store (#121)
Changes
@@@ -585,7 +585,8 @@ thumbnail only while its VOD or clip is public. Tests: `test/security-*.test.js``GET /o/:id` serves object bytes by canonical id (`med_…`): public/unlistedobjects openly (and, for a JPEG, PNG, WebP, AVIF or GIF with a local copy, `?w=&fmt=` answers a resized variant exactlyas `/t/:id` does, from its own cache `OBJECT_VARIANTS_PATH`, default `object-variants`; Network's `/avatar/:username?s=`--and the shared navbar ask for avatars this way; without a local copy, or when the cache is busy, the original), private ones only with a valid signature from++and the shared navbar ask for avatars this way; a private image takes a variant only with its signature, served++private and no-store like the original, which is how openvibe.download shows its previews; without a local copy, or when the cache is busy, the original), private ones only with a valid signature from`/api/v2/:app/objects/:id/download`, deleted ones 410 (a deleted private objectwithout a signature is a 404 like any other private one). A native object in theR2 popularity cache redirects to its verified R2 copy; a GET of a native object@@@ -455,7 +455,8 @@ reads are metered by CDN logs. Nothing here bills delivery.VODs, compute placement. (Slice 1 shipped: one on-demand 720p rung, queued by the master playlist / JSON downloadand cut into the timeline. Slice 2 shipped 2026-10-15: image variants. `/t/:id?w=&fmt=` and `/o/:id?w=&fmt=` (publicimages with a local copy) answer WebP, AVIF or JPEG at 64–640 px, kept in bounded LRU caches; openvibe.video's and-- Live's cards and the shared navbar's avatars ask for them. The economics beyond an LRU budget, AV1 and compute++ Live's cards and the shared navbar's avatars ask for them. A private image takes one with a valid signature (private,++ no-store): the Download drive's previews. The economics beyond an LRU budget, AV1 and computeplacement are still open.)- **F5 multi-CDN delivery:** Bunny path (B2 origin), R2 custom-domain path, OpenVibe edge first up to capacity, routeepochs, canary shifts with auto-revert, player QoE beacons, seek SLOs.@@@ -998,11 +998,12 @@ publicRouter.get('/:id', async (req, res) => {'Cache-Control': obj.visibility === 'private' || sandbox ? 'private, no-store' : 'public, max-age=3600','Content-Disposition': contentDisposition(md.filename || obj.id, { inline: INLINE.test(mime) }),};-- // ?w=&fmt= on a public or unlisted image asks for a variant (Media Fabric F4, thumbnails/variants.js): resized-- // from its local copy and cached (an avatar of 18 KB is under 1 KB at 96 px). Without a local copy, or when the-- // cache is busy or the image cannot be decoded, the original answers.++ // ?w=&fmt= on an image asks for a variant (Media Fabric F4, thumbnails/variants.js): resized from its local copy++ // and cached (an avatar of 18 KB is under 1 KB at 96 px). Without a local copy, or when the cache is busy or the++ // image cannot be decoded, the original answers. A private or sandbox image takes one only with a valid++ // signature (the drive's previews, MediaHub), served with the same private, no-store headers as the original.const variants = require('../thumbnails/variants');-- const spec = VARIANT_MIME.test(mime) && obj.visibility !== 'private' && !sandbox ? variants.parse(req.query) : null;++ const spec = VARIANT_MIME.test(mime) && (signed || (obj.visibility !== 'private' && !sandbox)) ? variants.parse(req.query) : null;if (spec && spec.error) return res.status(400).json({ error: spec.error });// A viewer of a native object, counted once a day without being identified (objects/popularity.js); the// object tiering reads these counts. Sandbox objects are never tiered, so they are not counted.@@@ -156,7 +156,24 @@ const crypto = require('crypto');assert.deepStrictEqual([r.status, r.headers.get('content-type')], [200, 'image/png'], 'bytes that do not decode: the original');r = await call('GET', `/o/${imgId}`, { bearer: null });assert.deepStrictEqual([r.status, r.headers.get('content-type')], [200, 'image/png'], 'no query: the original');-- console.log('✅ image variants: a public image in its asked size, the original otherwise');++ // A private image takes a variant only with its signature (the drive's previews), private and no-store as the original.++ r = await call('POST', O, { body: { kind: 'file', visibility: 'private' } });++ const privId = r.body.id;++ await call('PUT', `${O}/${privId}/content`, { raw: realPng, headers: { 'content-type': 'image/png' } });++ r = await call('POST', `${O}/${privId}/complete`);++ assert.strictEqual(r.body.lifecycle_status, 'ready');++ r = await call('GET', `/o/${privId}?w=96&fmt=webp`, { bearer: null });++ assert.strictEqual(r.status, 404, 'unsigned: indistinguishable from missing');++ r = await call('GET', `${O}/${privId}/download`);++ const privSigned = local(r.body.url);++ const pv = await fetch(`${privSigned}&w=96&fmt=webp`);++ const pbuf = Buffer.from(await pv.arrayBuffer());++ assert.deepStrictEqual([pv.status, pv.headers.get('content-type'), pv.headers.get('cache-control')], [200, 'image/webp', 'private, no-store']);++ assert.deepStrictEqual([(await require('sharp')(pbuf).metadata()).width, pbuf.length < realPng.length], [96, true]);++ r = await call('GET', `${privSigned.replace(/sig=[^&]+/, 'sig=AAAA')}&w=96&fmt=webp`, { bearer: null });++ assert.strictEqual(r.status, 404, 'a bad signature gets no variant either');++ await db.run('DELETE FROM media_objects WHERE id = ?', [privId]); // the listing checks below count the objects++ console.log('✅ image variants: a public image in its asked size, the original otherwise; a private one only signed');// ── Service tokens ──r = await call('POST', O, { bearer: tok(), headers: { 'x-ov-subject': `user:${SUBJECT}` }, body: { kind: 'asset', size_bytes: 3 } });
@@ -585,7 +585,8 @@ thumbnail only while its VOD or clip is public. Tests: `test/security-*.test.js`
@@ -585,7 +585,8 @@ thumbnail only while its VOD or clip is public. Tests: `test/security-*.test.js`
`GET /o/:id` serves object bytes by canonical id (`med_…`): public/unlisted
`GET /o/:id` serves object bytes by canonical id (`med_…`): public/unlisted
objects openly (and, for a JPEG, PNG, WebP, AVIF or GIF with a local copy, `?w=&fmt=` answers a resized variant exactly
objects openly (and, for a JPEG, PNG, WebP, AVIF or GIF with a local copy, `?w=&fmt=` answers a resized variant exactly
as `/t/:id` does, from its own cache `OBJECT_VARIANTS_PATH`, default `object-variants`; Network's `/avatar/:username?s=`
as `/t/:id` does, from its own cache `OBJECT_VARIANTS_PATH`, default `object-variants`; Network's `/avatar/:username?s=`
-and the shared navbar ask for avatars this way; without a local copy, or when the cache is busy, the original), private ones only with a valid signature from
+and the shared navbar ask for avatars this way; a private image takes a variant only with its signature, served
+private and no-store like the original, which is how openvibe.download shows its previews; without a local copy, or when the cache is busy, the original), private ones only with a valid signature from
`/api/v2/:app/objects/:id/download`, deleted ones 410 (a deleted private object
`/api/v2/:app/objects/:id/download`, deleted ones 410 (a deleted private object
without a signature is a 404 like any other private one). A native object in the
without a signature is a 404 like any other private one). A native object in the
R2 popularity cache redirects to its verified R2 copy; a GET of a native object
R2 popularity cache redirects to its verified R2 copy; a GET of a native object
@@ -455,7 +455,8 @@ reads are metered by CDN logs. Nothing here bills delivery.
@@ -455,7 +455,8 @@ reads are metered by CDN logs. Nothing here bills delivery.
VODs, compute placement. (Slice 1 shipped: one on-demand 720p rung, queued by the master playlist / JSON download
VODs, compute placement. (Slice 1 shipped: one on-demand 720p rung, queued by the master playlist / JSON download
and cut into the timeline. Slice 2 shipped 2026-10-15: image variants. `/t/:id?w=&fmt=` and `/o/:id?w=&fmt=` (public
and cut into the timeline. Slice 2 shipped 2026-10-15: image variants. `/t/:id?w=&fmt=` and `/o/:id?w=&fmt=` (public
images with a local copy) answer WebP, AVIF or JPEG at 64–640 px, kept in bounded LRU caches; openvibe.video's and
images with a local copy) answer WebP, AVIF or JPEG at 64–640 px, kept in bounded LRU caches; openvibe.video's and
- Live's cards and the shared navbar's avatars ask for them. The economics beyond an LRU budget, AV1 and compute
+ Live's cards and the shared navbar's avatars ask for them. A private image takes one with a valid signature (private,
+ no-store): the Download drive's previews. The economics beyond an LRU budget, AV1 and compute
placement are still open.)
placement are still open.)
- **F5 multi-CDN delivery:** Bunny path (B2 origin), R2 custom-domain path, OpenVibe edge first up to capacity, route
- **F5 multi-CDN delivery:** Bunny path (B2 origin), R2 custom-domain path, OpenVibe edge first up to capacity, route
epochs, canary shifts with auto-revert, player QoE beacons, seek SLOs.
epochs, canary shifts with auto-revert, player QoE beacons, seek SLOs.
@@ -998,11 +998,12 @@ publicRouter.get('/:id', async (req, res) => {
@@ -998,11 +998,12 @@ publicRouter.get('/:id', async (req, res) => {
'Cache-Control': obj.visibility === 'private' || sandbox ? 'private, no-store' : 'public, max-age=3600',
'Cache-Control': obj.visibility === 'private' || sandbox ? 'private, no-store' : 'public, max-age=3600',
'Content-Disposition': contentDisposition(md.filename || obj.id, { inline: INLINE.test(mime) }),
'Content-Disposition': contentDisposition(md.filename || obj.id, { inline: INLINE.test(mime) }),
};
};
- // ?w=&fmt= on a public or unlisted image asks for a variant (Media Fabric F4, thumbnails/variants.js): resized
- // from its local copy and cached (an avatar of 18 KB is under 1 KB at 96 px). Without a local copy, or when the
- // cache is busy or the image cannot be decoded, the original answers.
+ // ?w=&fmt= on an image asks for a variant (Media Fabric F4, thumbnails/variants.js): resized from its local copy
+ // and cached (an avatar of 18 KB is under 1 KB at 96 px). Without a local copy, or when the cache is busy or the
+ // image cannot be decoded, the original answers. A private or sandbox image takes one only with a valid
+ // signature (the drive's previews, MediaHub), served with the same private, no-store headers as the original.
const variants = require('../thumbnails/variants');
const variants = require('../thumbnails/variants');
- const spec = VARIANT_MIME.test(mime) && obj.visibility !== 'private' && !sandbox ? variants.parse(req.query) : null;
+ const spec = VARIANT_MIME.test(mime) && (signed || (obj.visibility !== 'private' && !sandbox)) ? variants.parse(req.query) : null;
if (spec && spec.error) return res.status(400).json({ error: spec.error });
if (spec && spec.error) return res.status(400).json({ error: spec.error });
// A viewer of a native object, counted once a day without being identified (objects/popularity.js); the
// A viewer of a native object, counted once a day without being identified (objects/popularity.js); the
// object tiering reads these counts. Sandbox objects are never tiered, so they are not counted.
// object tiering reads these counts. Sandbox objects are never tiered, so they are not counted.
@@ -156,7 +156,24 @@ const crypto = require('crypto');
@@ -156,7 +156,24 @@ const crypto = require('crypto');
assert.deepStrictEqual([r.status, r.headers.get('content-type')], [200, 'image/png'], 'bytes that do not decode: the original');
assert.deepStrictEqual([r.status, r.headers.get('content-type')], [200, 'image/png'], 'bytes that do not decode: the original');
r = await call('GET', `/o/${imgId}`, { bearer: null });
r = await call('GET', `/o/${imgId}`, { bearer: null });
assert.deepStrictEqual([r.status, r.headers.get('content-type')], [200, 'image/png'], 'no query: the original');
assert.deepStrictEqual([r.status, r.headers.get('content-type')], [200, 'image/png'], 'no query: the original');
- console.log('✅ image variants: a public image in its asked size, the original otherwise');
+ // A private image takes a variant only with its signature (the drive's previews), private and no-store as the original.
+ r = await call('POST', O, { body: { kind: 'file', visibility: 'private' } });
+ const privId = r.body.id;
+ await call('PUT', `${O}/${privId}/content`, { raw: realPng, headers: { 'content-type': 'image/png' } });
+ r = await call('POST', `${O}/${privId}/complete`);
+ assert.strictEqual(r.body.lifecycle_status, 'ready');
+ r = await call('GET', `/o/${privId}?w=96&fmt=webp`, { bearer: null });
+ assert.strictEqual(r.status, 404, 'unsigned: indistinguishable from missing');
+ r = await call('GET', `${O}/${privId}/download`);
+ const privSigned = local(r.body.url);
+ const pv = await fetch(`${privSigned}&w=96&fmt=webp`);
+ const pbuf = Buffer.from(await pv.arrayBuffer());
+ assert.deepStrictEqual([pv.status, pv.headers.get('content-type'), pv.headers.get('cache-control')], [200, 'image/webp', 'private, no-store']);
+ assert.deepStrictEqual([(await require('sharp')(pbuf).metadata()).width, pbuf.length < realPng.length], [96, true]);
+ r = await call('GET', `${privSigned.replace(/sig=[^&]+/, 'sig=AAAA')}&w=96&fmt=webp`, { bearer: null });
+ assert.strictEqual(r.status, 404, 'a bad signature gets no variant either');
+ await db.run('DELETE FROM media_objects WHERE id = ?', [privId]); // the listing checks below count the objects
+ console.log('✅ image variants: a public image in its asked size, the original otherwise; a private one only signed');
// ── Service tokens ──
// ── Service tokens ──
r = await call('POST', O, { bearer: tok(), headers: { 'x-ov-subject': `user:${SUBJECT}` }, body: { kind: 'asset', size_bytes: 3 } });
r = await call('POST', O, { bearer: tok(), headers: { 'x-ov-subject': `user:${SUBJECT}` }, body: { kind: 'asset', size_bytes: 3 } });