openvibers/openvibe.tours Commits

Place photos show: accept Media's /o/<id> URLs; one upload pace per process (#110)

OP OpenVibers committed 4 days ago · f105ff04fe · 3 files, +9 −3

Media answers a public object at https://openvibe.media/o/<id>; the views drew a Media
picture only under /api/v2/tours/objects/, so every photo copied on launch day (591) was
dropped at render. The views accept /o/<id> (an id, nothing else). The upload pace is now
one for the whole process (2.5 s), since discovery and the places job each make a cache.

Changes

ddiff --git a/server/places/photo-cache.js b/server/places/photo-cache.js
iindex 38ef499..4b7228e 100644
---- a/server/places/photo-cache.js
++++ b/server/places/photo-cache.js
@@@ -3,10 +3,12 @@
// A checked Commons image is copied once into Media as a public object. The file's Commons page,
// author and licence stay on the place row; Media serves only the smaller image bytes.
// Media takes at most 30 uploads a minute from one caller (media.object.upload); one every UPLOAD_GAP_MS stays under it.
--const UPLOAD_GAP_MS = 2_100;
++const UPLOAD_GAP_MS = 2_500;
++// One pace for the whole process: the places job and on-demand discovery each make a cache, and together they must
++// stay under Media's limit too.
++let nextUploadAt = 0;
function createPhotoCache({ config, s, fetchImpl = globalThis.fetch, log = console, objectsClient = null, uploadGapMs = UPLOAD_GAP_MS }) {
let objects = objectsClient;
-- let nextUploadAt = 0;
async function uploadSlot() {
const now = Date.now();
const at = Math.max(now, nextUploadAt);
ddiff --git a/server/views/index.js b/server/views/index.js
iindex cc37c5d..c64958c 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -34,7 +34,7 @@ const safeImage = (v) => {
try {
const { protocol, hostname, pathname } = new URL(u);
if (protocol !== 'https:' || !IMAGE_HOSTS.test(hostname)) return null;
-- return /wikimedia\.org$/i.test(hostname) || (hostname === 'openvibe.media' && /^\/api\/v2\/tours\/objects\//.test(pathname))
++ return /wikimedia\.org$/i.test(hostname) || (hostname === 'openvibe.media' && (/^\/api\/v2\/tours\/objects\//.test(pathname) || /^\/o\/[A-Za-z0-9_-]{6,64}$/.test(pathname)))
|| /\.(jpe?g|png|webp|gif|avif|svg)$/i.test(pathname) ? u : null;
} catch { return null; }
};
ddiff --git a/test/safe-image.test.js b/test/safe-image.test.js
iindex a279995..b86b590 100644
---- a/test/safe-image.test.js
++++ b/test/safe-image.test.js
@@@ -31,6 +31,10 @@ assert.ok(safeImage('https://upload.wikimedia.org/wikipedia/commons/a/ab/Hachiko
assert.ok(safeImage('https://commons.wikimedia.org/wiki/Special:FilePath/Hachiko.jpg?width=640'), 'Commons file paths');
assert.ok(safeImage('https://openvibe.media/files/tower.webp'), 'an OpenVibe image');
assert.strictEqual(safeImage('https://openvibe.media/gallery'), null, 'a page is not a picture');
++// Media answers a public object at /o/<id> (no extension): the place photos Tours copies there (11-08: all were refused).
++assert.ok(safeImage('https://openvibe.media/o/med_01JCX9S3K2M4N5P6Q7R8S9T0VW'), 'a Media public object');
++assert.strictEqual(safeImage('https://openvibe.media/o/../admin'), null, 'only an object id');
++assert.strictEqual(safeImage('https://openvibe.media/o/'), null, 'an id is required');
// Only hosts the page's img-src allows: anything else would be refused and draw a broken image (2026-11-02).
assert.strictEqual(safeImage('https://images.mapillary.com/6Vri6u81GdjqVKBQTbQYBg/thumb-2048.jpg'), null, 'Mapillary is not allowed');
assert.strictEqual(safeImage('https://example.com/photos/tower.webp'), null, 'an image on another host');