openvibers/openvibe.media Commits
The SQLite era is gone (on PostgreSQL since 2026-09-28; no compatibility kept): the one-time migrate-to-postgres script, the *_DB_PATH settings (config, .env.example, systemd unit, test helpers), the unused better-sqlite3 dependency and its lockfile entries, and docs, docstrings and STATUS lines about SQLite or its rollback window. observability.test.js waits up to 30 s for the real server to boot (a cold PGlite boot plus migrations missed the 10 s window on a loaded machine). Reviewed independently (VERDICT: SHIP). npm test 64/65 test files passed, 1 skipped (n-1.test.js: N-1 SQL: skipped (the recorded N-1 releas.
Changes
@@@ -9,14 +9,15 @@ NODE_ENV=developmentMEDIA_PUBLIC_URL=https://openvibe.media# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh media: the runtime role through PgBouncer,--# the owner on a direct connection (migrations and the one-time import), and Valkey (shared limit counters).--# Without DATABASE_URL, development uses an embedded PGlite database in data/pglite.++# the owner on a direct connection (migrations), and Valkey (shared limit counters).++# Without DATABASE_URL, development uses an embedded PGlite database in data/pglite (MEDIA_PGLITE_DIR overrides it).DATABASE_URL=DATABASE_DIRECT_URL=VALKEY_URL=VALKEY_PREFIX=--# The SQLite file of releases before PostgreSQL: read once by scripts/migrate-to-postgres.js.--DB_PATH=./data/media.db++# The service's data directory: the reports (duration reconcile, storage orphans, H15 repair) and the drill++# artifacts are written under it.++MEDIA_DATA_DIR=./data# Restore-drill mode, set only by `ovhost drill media` (OpenVibe.Host docs/restore-drills.md): reads# only from a restored copy, no stored bytes, no jobs, webhooks, events or ffmpeg; refuses to start@@@ -71,13 +71,15 @@ reads events.```server/index.js boot, route mounts, background jobs, graceful shutdown-- config.js env config (PORT, DB_PATH, *_PATH, MEDIA_B2_*/R2_*, RTP pool, OV_NETWORK_URL)++ config.js env config (PORT, MEDIA_DATA_DIR, *_PATH, DATABASE_URL/DATABASE_DIRECT_URL, MEDIA_B2_*/R2_*,++ RTP pool, OV_NETWORK_URL)auth.js tenancy + auth middleware, JWKS fetch, app seedingwebhooks.js HMAC-signed outbound webhooksdrill.js MEDIA_DRILL: restore-drill mode (reads only, no bytes, no jobs, nothing leaves)-- db/schema.sql apps, vods, clips, pastes(+likes/comments), files, content_views, media_settings,-- media_objects/locations/relationships/variants/jobs/holds/invariant_violationsdb/database.js PostgreSQL helpers (openvibe-sdk/db, async), all app_id-scoped++ db/init.js applies migrations/ (DATABASE_URL/DATABASE_DIRECT_URL, or embedded PGlite)++ migrations/NNNN_*.sql apps, vods, clips, pastes(+likes/comments), files, content_views, media_settings,++ media_objects/locations/relationships/variants/jobs/holds/invariant_violationsvod/recorder.js ffmpeg recording: RTMP pull + RTP (SDP) ingest, codec passthroughvod/media-tools.js probes, seekable remux, DVR sidecar, chunk-segment concatvod/finalize.js finalize pipeline (remux → probe → master recovery → thumbnail → webhook)@@@ -381,9 +383,8 @@ that is deleted; none for sandbox tenants. `test/object-events.test.js`.## Restore drills (`MEDIA_DRILL=1`)A restore drill starts a second Media from this checkout on 127.0.0.1:14100 with `MEDIA_DRILL=1` and--`DATABASE_URL` naming a restored copy of the database (pgBackRest restores the cluster on a spare port; the Host's--`ovhost drill media` still restores the SQLite backups of the releases before PostgreSQL and needs that step before--it drills this one). In that mode (`server/drill.js`) Media:++`DATABASE_URL` naming a restored copy of the database (pgBackRest restores the cluster on a spare port). In that++mode (`server/drill.js`) Media:- refuses to start unless `DATABASE_URL` is set and does not name production's database (`ov_media` on 5432, orPgBouncer's 6432), `DATABASE_DIRECT_URL` is unset (a drill never migrates), `HOST` is loopback and `PORT` is not@@@ -573,16 +574,6 @@ guardian (refuses new recordings when free space is critical), stale live-thumbcleanup, and an on-boot sweep that finalizes recordings orphaned by an uncleanshutdown.--## Moving to PostgreSQL----`scripts/migrate-to-postgres.js` is the one-time import of the SQLite `media.db` (`DB_PATH`) into `ov_media`--(openvibe-sdk `runSqliteMigration`: migrations as the owner, every table copied into emptied tables, counts and--checksums verified; `--pglite` rehearses it in memory). It runs while the service is stopped (OpenVibe.Host--`roles/data/switch-service.sh media /opt/openvibe.media/data/media.db`); the SQLite file stays read-only for 7 days as--the rollback. Timestamps stay SQLite's text (`ov_now()`, `datetime()`, `julianday()` in the migration); the retention--hold guards and the object-change triggers are PL/pgSQL. (The predecessor-DB importer and its bulk-copy recipe were--retired with the move.)--## Not ported- **Song-request media queue** (`media/` yt-dlp downloader/queue): deeply@@@ -610,8 +601,9 @@ Run `npm test` for the policy regression tests.## Acceptance--`npm test` runs every `test/*.test.js` in turn (temp databases, local stubs, no network). What they--prove includes: tenancy and developer-project tenants (`app-tenants`, `app-tenant-abuse`); the object++`npm test` runs every `test/*.test.js` in turn (each process gets a migrated PGlite database, or the++PostgreSQL containers under `npm run test:pg`; local stubs, no network). What they prove includes: tenancy++and developer-project tenants (`app-tenants`, `app-tenant-abuse`); the objectmodel, multipart, reconcile, verification and tiering (`objects-*`, `object-tiering`, `tier-*`); jobswith leases and fencing (`jobs*`); VOD finalize, durations and storage policy (`vod-*`); private itemsstay private and nobody acts on another's ids (`security-private`, `security-idor`,@@@ -656,5 +648,4 @@ writes its settings); the release migrates it at boot; nginx serves `openvibe.mefixtures (`npm run n-1:record`).Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the--restart; afterwards `sudo ovhost rollback media --to <sha>`. A release from before PostgreSQL reads the SQLite--`media.db` it left (read-only for 7 days after the switch); anything written since would need moving back first.++restart; afterwards `sudo ovhost rollback media --to <sha>`.@@@ -76,7 +76,7 @@ the same functions.## Backfill```--node scripts/backfill-objects.js [--dry-run] [--only-missing] [--json] [--out report.json] [--db ./data/media.db]++node scripts/backfill-objects.js [--dry-run] [--only-missing] [--json] [--out report.json]```The backfill creates one object per vod, clip, file, screenshot/avatar paste and thumbnail, with@@@ -90,12 +90,12 @@ relationships and thumbnail variants.Skip reasons today: `clips-only recording (ephemeral, never published)`, `screenshot paste without a file path`, `external thumbnail url`.--The run holds SQLite's write lock for its duration, typically seconds. Prefer the boot backfill, or++The backfill's own transaction holds row and table locks for its duration, typically seconds. Prefer the boot backfill, orrun the script while traffic is quiet.### Keeping the model current: object-first writes--Every write to a projected row writes its object in the same SQLite transaction++Every write to a projected row writes its object in the same PostgreSQL transaction(`model.withObject(kind, ids, write)`; roadmap WS-G task 1, which retires compatibility shim C-75,"write the row, then sync the object"). The row change and the re-projection commit together ornot at all, so a crash or an error between them can no longer leave the object behind its row. A@@@ -119,7 +119,7 @@ after the outcome event.| avatar ingest | `avatars/ingest` || tier moves | `moveToCold`, `moveToHot`, `promoteToR2`, `demoteFromR2`, sweep, `migrateLegacy`: the copy the move verified is marked `present` in the same transaction |--**Deletes need nothing more.** A SQLite trigger on `vods`, `clips`, `files` and `pastes` marks the++**Deletes need nothing more.** A PostgreSQL trigger (defined in `migrations/`, PL/pgSQL) on `vods`, `clips`, `files` and `pastes` marks theobject `deleted` in the same statement whenever its row is deleted, which covers all of theinherited delete paths.@@@ -137,7 +137,7 @@ projects what it inserts.### Drift report```--node scripts/object-drift-report.js [--app live] [--limit 20] [--json] [--out report.json] [--db ./data/media.db]++node scripts/object-drift-report.js [--app live] [--limit 20] [--json] [--out report.json]```For every projected row, and each vod/clip thumbnail, the report derives what the object must say@@@ -183,17 +183,18 @@ Rules for both job and backfill:request uses `INTERNAL_API_KEY`.```--node scripts/backfill-owner-subject.js [--db media.db] [--batch 500] [--json] # dry run: counts per tenant++node scripts/backfill-owner-subject.js [--batch 500] [--json] # dry run: counts per tenantnode scripts/backfill-owner-subject.js --apply --backup <file.json> [--batch 500] # fillnode scripts/backfill-owner-subject.js --rollback <file.json> [--apply] # undo (dry without --apply)```- **The dry run** (the default) opens the database read-only and prints per tenant: objects, alreadyset, to fill, unresolvable, unsupported tenant and no owner.--- **`--apply --backup <file.json>`** first takes an online backup to `<file>.media.db` and requires-- `PRAGMA integrity_check` = `ok` on it. It then writes `<file.json>` (0600), the rows it is about to-- change, and fills them in `--batch` transactions. Finally it rewrites the file to list exactly the-- rows it changed. It refuses an existing backup name, and a re-run fills only what is still missing.++- **`--apply --backup <file.json>`** writes the rollback file `<file.json>` (0600) with the rows it is++ about to change, and fills them in `--batch` transactions. The database's own safety net is++ PostgreSQL's point-in-time recovery (pgBackRest), not an online copy. Finally it rewrites the file to++ list exactly the rows it changed. It refuses an existing backup name, and a re-run fills only what is++ still missing.- **`--rollback <file.json> --apply`** sets `owner_subject` back to `NULL` on each listed row thatstill carries the subject the backfill wrote for the same owner. Rows changed since are counted andleft alone. Without `--apply` it only reports, which also verifies an applied backfill. Stop the job@@@ -539,7 +540,7 @@ admin routes' log), with who, the object, the kind and the reason.## Reconciliation```--node scripts/reconcile-objects.js [--verify] [--hash] [--app live] [--json] [--out report.json] [--db ./data/media.db]++node scripts/reconcile-objects.js [--verify] [--hash] [--app live] [--json] [--out report.json]```**Modes.**@@@ -599,7 +600,7 @@ and nothing is concluded about its keys.restore drill. On demand:```--node scripts/vods-orphans-report.js --storage [--no-remote] [--limit 2000] [--out report.json] [--json] [--db ./data/media.db]++node scripts/vods-orphans-report.js --storage [--no-remote] [--limit 2000] [--out report.json] [--json]````--no-remote` lists no bucket. The detailed per-object recommendations for the `vods-orphans/` prefix@@@ -639,7 +640,7 @@ that only moved to B2/R2 keeps it. Test: `test/content-hash.test.js`.- The report, which lists each one for an operator to decide on:```-- node scripts/no-good-copy-report.js [--app live] [--json] [--out report.json] [--db ./data/media.db]++ node scripts/no-good-copy-report.js [--app live] [--json] [--out report.json]```For each object it prints the owner (app, `owner_user_id`, `owner_subject`), `legacy_ref`, the `vods`/`clips`/`files`/`pastes` rows that point at it, its relationships, variants and holds, every location with its state and `verified_at`, and the last verification. The report opens the database read-only. It exits 1 when there are any.@@@ -662,7 +663,7 @@ A *public playback object* is a `ready` vod or clip whose visibility is `public`- **Report.**```-- node scripts/object-invariant.js [--dry-run] [--app live] [--json] [--db ./data/media.db]++ node scripts/object-invariant.js [--dry-run] [--app live] [--json]```Lists every public playback object above target, largest first, records the rows, and resolves rows that no longer apply. `--dry-run` writes nothing.@@@ -690,7 +691,7 @@ Code: `server/jobs/`. Tests: `test/jobs.test.js`, `test/jobs-invariant.test.js`,30 s, 2 min, 8 min, …, at most 1 h) until `max_attempts`; a handler can mark a failure permanent.**Events.** Every state change and its `media.job.<transition>` event (`proposed`, `queued`,--`started`, `retrying`, `succeeded`, `failed`, `cancelled`; subject `job <id>`) commit in one SQLite++`started`, `retrying`, `succeeded`, `failed`, `cancelled`; subject `job <id>`) commit in one PostgreSQLtransaction through Media's outbox, the same rule as the outcome events (`webhooks.announce()`): nochange without its event, no event for a change that rolled back. Progress events are `low`priority; outcomes and proposals are `important`. Job events go to OpenVibe.Events only, not to app@@@ -809,7 +810,7 @@ still holds the value that was read, and re-projects the object. Every run write```node scripts/vod-duration-reconcile.js [--app live] [--batch 50] [--after <id>] [--all] [--ids 1,2] [--confirm-remote] # dry run--node scripts/vod-duration-reconcile.js --apply --backup <file.json> [same selection] # checked DB backup first; file = rollback++node scripts/vod-duration-reconcile.js --apply --backup <file.json> [same selection] # file = the rollback file (every repair's old values)node scripts/vod-duration-reconcile.js --rollback <file.json> [--apply]```@@@ -134,7 +134,7 @@ const { execFile } = require('child_process');}const record = { generated_at: new Date().toISOString(), applied: false, objects: report.count, plan: steps.map(({ file, ...s }) => s) };if (!APPLY) {-- const out = arg('out') ? path.resolve(arg('out')) : path.join(path.dirname(path.resolve(config.db.path)), 'reports', `h15-repair-${record.generated_at.replace(/[:.]/g, '-')}.json`);++ const out = arg('out') ? path.resolve(arg('out')) : path.join(config.dataDir, 'reports', `h15-repair-${record.generated_at.replace(/[:.]/g, '-')}.json`);fs.mkdirSync(path.dirname(out), { recursive: true });fs.writeFileSync(out, JSON.stringify(record, null, 2));console.log(`\nDry run: nothing changed. Plan written to ${out}`);ddeleted file mode 100755@@@ -1,25 +0,0 @@--#!/usr/bin/env node--'use strict';--/**-- * The one-time move of Media's SQLite database (DB_PATH) into its PostgreSQL schema (ADR-035; the procedure is-- * openvibe-sdk docs/migrating-to-postgresql.md, section 6, carried out by openvibe-sdk/db runSqliteMigration).-- *-- * node scripts/migrate-to-postgres.js [--sqlite <file>] [--pglite] [--json]-- *-- * Applies migrations/ as the owner (DATABASE_DIRECT_URL), copies every table into emptied tables, verifies row counts-- * and checksums, and exits 1 unless everything verified. The SQLite file is opened read-only. Every table keeps its-- * name and columns.-- */--require('dotenv').config();--const { runSqliteMigration } = require('openvibe-sdk/db');--const config = require('../server/config');--const { MIGRATIONS } = require('../server/db/database');----const TABLES = {};----if (require.main === module) {-- runSqliteMigration({ service: 'media', sqlite: config.db.path, directUrl: config.db.directUrl, migrations: MIGRATIONS, tables: TABLES })-- .then((code) => process.exit(code), (err) => { console.error(`migrate-to-postgres failed: ${err.message}`); process.exit(1); });--}----module.exports = { TABLES };@@@ -221,7 +221,7 @@ if (require.main === module) {await db.initDb(); // PostgreSQL (DATABASE_URL), as the serviceawait storage.tierConfig.init(storage.DEFAULTS, { log: { info() {}, warn: (m) => console.error(`[Tiers] ${m}`), error: (m) => console.error(`[Tiers] ${m}`) } });const art = await runDrill(opts, { storage, db, model });-- const out = opts.out || path.join(path.dirname(config.db.path), 'drills', `r2-eviction-${art.vod_id ?? 'none'}-${art.started_at.replace(/[:.]/g, '-')}.json`);++ const out = opts.out || path.join(config.dataDir, 'drills', `r2-eviction-${art.vod_id ?? 'none'}-${art.started_at.replace(/[:.]/g, '-')}.json`);fs.mkdirSync(path.dirname(out), { recursive: true });fs.writeFileSync(out, JSON.stringify(art, null, 2));console.log(opts.json ? JSON.stringify(art, null, 2) : `${summarize(art)}\nartifact: ${out}`);@@@ -37,7 +37,7 @@ ffmpeg -y -f lavfi -i "testsrc=duration=5:size=640x360:rate=30" \echo "-- booting server on :$PORT (temp data dir)"SEED="[{\"app_id\":\"live\",\"name\":\"Live\",\"api_key\":\"$LIVE_KEY\",\"allowed_origins\":[\"http://localhost:3000\"]},{\"app_id\":\"games\",\"name\":\"Games\",\"api_key\":\"$GAMES_KEY\",\"quota_bytes\":10485760}]"env PORT="$PORT" HOST=127.0.0.1 \-- DB_PATH="$WORK/data/media.db" \++ MEDIA_DATA_DIR="$WORK/data" \VOD_PATH="$WORK/data/vods" CLIPS_PATH="$WORK/data/clips" \PASTES_PATH="$WORK/data/pastes" THUMBNAILS_PATH="$WORK/data/thumbnails" \FILES_PATH="$WORK/data/files" \@@@ -70,7 +70,7 @@ const path = require('path');if (has('storage')) { await storageReport(); await db.close(); process.exit(0); }if (!vodStorage.providerConfigured(provider)) usage(`${provider} is not configured (MEDIA_${provider.toUpperCase()}_* not in the environment)`);const report = await orphans.buildReport({ provider, prefix: arg('prefix') || orphans.PREFIX });-- const file = arg('out') ? path.resolve(arg('out')) : path.join(path.dirname(config.db.path), 'reports', `vods-orphans-${report.generated_at.replace(/[:.]/g, '-')}.json`);++ const file = arg('out') ? path.resolve(arg('out')) : path.join(config.dataDir, 'reports', `vods-orphans-${report.generated_at.replace(/[:.]/g, '-')}.json`);fs.mkdirSync(path.dirname(file), { recursive: true });fs.writeFileSync(file, JSON.stringify(report, null, 1));log(`${report.provider}://${report.bucket}/${report.prefix}: ${report.totals.objects} object(s), ${report.totals.gb} GB (read-only report)`);@@@ -113,8 +113,9 @@ router.get('/', async (req, res) => {];const breakdown = directories.map(d => ({ name: d.name, ...dirStatsRecursive(d.path) }));++ // The store's size (PostgreSQL; the server answers 0 when it does not report one).let dbBytes = 0;-- try { dbBytes = fs.statSync(config.db.path).size; } catch { /* */ }++ try { dbBytes = Number((await db.get('SELECT pg_database_size(current_database()) AS bytes')).bytes) || 0; } catch { /* */ }// App-scoped DB statsconst vodStats = await db.get(`@@@ -21,13 +21,13 @@ const config = {publicUrl: (process.env.MEDIA_PUBLIC_URL || 'https://openvibe.media').replace(/\/$/, ''),// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner). Without them,-- // development uses an embedded PGlite database in data/pglite. path: the SQLite file of releases before-- // PostgreSQL, read once by scripts/migrate-to-postgres.js.++ // development uses an embedded PGlite database in data/pglite (MEDIA_PGLITE_DIR overrides it).db: {url: process.env.DATABASE_URL || '',directUrl: process.env.DATABASE_DIRECT_URL || '',-- path: process.env.DB_PATH || './data/media.db',},++ // The service's data directory (MEDIA_DATA_DIR): reports and drills are written under it.++ dataDir: process.env.MEDIA_DATA_DIR || './data',// Valkey (ADR-035): per-actor limit counters shared across processes; without it they count in this process.valkey: { url: process.env.VALKEY_URL || '', prefix: process.env.VALKEY_PREFIX || 'ov:media:' },@@@ -155,6 +155,6 @@ config.assets.path = path.resolve(config.assets.path);config.thumbnails.path = path.resolve(config.thumbnails.path);config.files.path = path.resolve(config.files.path);config.objects.path = path.resolve(config.objects.path);--config.db.path = path.resolve(config.db.path);++config.dataDir = path.resolve(config.dataDir);module.exports = config;@@@ -22,7 +22,8 @@ let database = null;* The serving handle (ADR-035): DATABASE_URL through PgBouncer; in development without it, an embedded PGlite database* in data/pglite (MEDIA_PGLITE_DIR overrides it). Migrations run first, as the owner (DATABASE_DIRECT_URL), or on the* embedded handle. The schema, its triggers (retention holds, object deletes, object change events) and the default-- * settings are migrations/NNNN_*.sql; timestamps stay SQLite's text (ov_now(), datetime() in the migration).++ * settings are migrations/NNNN_*.sql; timestamps stay text in the `YYYY-MM-DD HH:MM:SS` UTC shape (ov_now(), datetime()++ * in the migration).*/async function openDb(cfg = config, { log = console, registry } = {}) {if (!cfg.db.url) {@@@ -265,7 +266,7 @@ const LIST_ORDERS = {function _listOrder(order) { return LIST_ORDERS[order] || LIST_ORDERS.newest; }/**-- * A list's `since` filter as an SQLite datetime ('YYYY-MM-DD HH:MM:SS', UTC), or null. Accepts that++ * A list's `since` filter as a text datetime ('YYYY-MM-DD HH:MM:SS', UTC), or null. Accepts that* form or anything Date.parse reads (ISO 8601); anything else is ignored rather than refused.*/function sinceParam(value) {@@@ -40,7 +40,7 @@ function parse(value) {const enabled = parse(process.env.MEDIA_DRILL);const REPO_ROOT = path.resolve(__dirname, '..');--/** Where production runs (OpenVibe.Host inventory: repo /opt/openvibe.media, database data/media.db). */++/** Where production runs (OpenVibe.Host inventory: repo /opt/openvibe.media, database ov_media). */const PRODUCTION_ROOT = '/opt/openvibe.media';const PRODUCTION_PORT = 4100;// Production's database: ov_media on the host's PostgreSQL (5432) or through PgBouncer (6432).@@@ -16,7 +16,7 @@* (subject object <id>; Events only; payload object id, tenant,* kind, legacy_ref and the times/visibilities, nothing else)*-- * The outbox row is written INSIDE the SQLite transaction that makes the state change it describes++ * The outbox row is written INSIDE the PostgreSQL transaction that makes the state change it describes* (record(), called from webhooks.announce()): the event exists if and only if the change committed,* so a crash can neither lose an outcome nor announce one that rolled back. The webhook for the same* outcome is sent after the commit and carries the envelope's event_id, so a consumer receiving it by@@@ -10,7 +10,7 @@// Restore-drill mode (MEDIA_DRILL=1, `ovhost drill media`): refuse an unsafe environment before the// database is opened, then cut every way out of the process but its own HTTP port. The instance--// serves reads from a restored copy of media.db, never opens a stored file and starts nothing else++// serves reads from a restored copy of the database, never opens a stored file and starts nothing else// (server/drill.js).require('dotenv').config();const drill = require('./drill');@@@ -309,7 +309,7 @@ const ready = (async () => {}if (drill.enabled) { console.log(`[Drill] Ready: http://${config.host}:${config.port} (reads only)`); return; }console.log(`[Media] OpenVibe.Media listening on ${config.host}:${config.port} (${config.nodeEnv})`);-- console.log(`[Media] Data: db=${config.db.path} vods=${config.vod.path} clips=${config.vod.clipsPath}`);++ console.log(`[Media] Data: db=${config.db.url ? 'postgresql' : 'pglite'} vods=${config.vod.path} clips=${config.vod.clipsPath}`);console.log(`[Media] RTP ingest pool: udp ${config.rtp.portMin}-${config.rtp.portMax} (127.0.0.1)`);// Durable events (roadmap Wave 3): webhook outcomes also go to OpenVibe.Events. Off without EVENTS_URL.try { require('./events').init(); } catch (err) { console.warn('[Events] not started:', err.message); }@@@ -8,7 +8,7 @@* running held by the worker (lease_until renewed while it runs; lease_token names the claim)* succeeded | failed | cancelled finished*-- * Every state change and its media.job.<transition> event commit in ONE SQLite transaction (the same++ * Every state change and its media.job.<transition> event commit in ONE PostgreSQL transaction (the same* rule as webhooks.announce(), see events.js): an event exists if and only if its change committed.* The relay is woken after the commit. Job events go to OpenVibe.Events only (no app webhook).*@@@ -133,7 +133,7 @@ function jobPublic(row) {};}--/** A SQLite UTC time ('YYYY-MM-DD HH:MM:SS') as ISO 8601 UTC ('…T…Z'), or null. */++/** An ISO 8601 UTC ('…T…Z') time from a `YYYY-MM-DD HH:MM:SS` (UTC) text timestamp, as the migrations store them, or null. */function isoTime(v) {if (v == null || v === '') return null;const s = String(v);@@@ -247,7 +247,7 @@ async function enqueue({ appId, type, objectId = null, params = {}, status = 'qulet out = null;const id = `mjob_${ids.ulid()}`;await db.getDb().tx(async () => {-- // One enqueue per tenant at a time (SQLite's one writer did this): the key, the identical active job and the++ // One enqueue per tenant at a time (an advisory lock per tenant serialises them): the key, the identical active job and the// active count below are read with every earlier enqueue of the tenant committed.await db.get('SELECT pg_advisory_xact_lock(hashtext(?)) AS locked', [`media.jobs:${appId}`]);if (key) {@@@ -31,7 +31,7 @@ const STATUS_FILTERS = [...model.LIFECYCLES, 'all'];/** Relations whose `from` object was made out of the `to` object (a clip is its own work, not a derivative). */const DERIVED_RELATIONS = ['derived_from', 'thumbnail_of'];--/** SQLite's "YYYY-MM-DD HH:MM:SS" (UTC) or ISO → ISO 8601, or null. */++/** ISO 8601 from a "YYYY-MM-DD HH:MM:SS" (UTC) text timestamp, as the migrations store them, or from ISO, or null. */function iso(v) {if (!v) return null;const s = String(v);@@@ -37,7 +37,7 @@ function expectedFields(p) {};}--// SQLite hands integers back as numbers and TEXT as strings; an app-local user id may be either.++// The store hands bigint back as a number or a string and TEXT as a string; an app-local user id may be either.const same = (a, b) => (a == null ? '' : String(a)) === (b == null ? '' : String(b));/** Every row of `table` (optionally one tenant's) in key order, a page at a time. */@@@ -77,7 +77,7 @@ function plan(item, probes = {}, facts = {}) {}/**-- * Apply one planned change. h = better-sqlite3 handle. facts: { size, sha256 } of the good bytes (rebaseline,++ * Apply one planned change. h = database handle. facts: { size, sha256 } of the good bytes (rebaseline,* regenerate). → the change with old values, or { skipped } when the row moved on.*/async function apply(h, step, facts = {}) {@@@ -49,7 +49,7 @@ function parseJson(s, fallback) {try { return JSON.parse(s); } catch { return fallback; }}--/** SQLite 'YYYY-MM-DD HH:MM:SS' (UTC) → epoch ms, so backfilled ids sort by original creation time. */++/** Epoch ms from a `YYYY-MM-DD HH:MM:SS` (UTC) text timestamp, as the migrations store them, so backfilled ids sort by original creation time. */function toMs(createdAt) {const ms = Date.parse(String(createdAt || '').replace(' ', 'T') + (/[zZ]|[+-]\d\d:?\d\d$/.test(String(createdAt || '')) ? '' : 'Z'));return Number.isFinite(ms) ? ms : Date.now();@@@ -177,7 +177,7 @@ function limitsOf(app, row) {*//*** Inside a transaction, the tenant's quota lock (held to commit): a check and the row it admits commit before the next-- * check of the same tenant reads usage, as SQLite's one writer made them (ADR-035).++ * check of the same tenant reads usage, as the tenant's quota lock makes them (ADR-035).*/async function lockQuota(appId) {if (db.getDb().inTransaction()) await db.get('SELECT pg_advisory_xact_lock(hashtext(?)) AS locked', [`media.quota:${appId}`]);@@@ -128,8 +128,8 @@ function createResolver({// ── Reads ────────────────────────────────────────────────────/** Per-tenant ownership counts (read-only). */--async function summary(sqlite) {-- const rows = await sqlite.prepare(`++async function summary(db) {++ const rows = await db.prepare(`SELECT app_id, count(*) AS objects,sum(owner_subject IS NOT NULL) AS already_set,sum(owner_subject IS NULL AND owner_user_id IS NOT NULL) AS missing,@@@ -141,8 +141,8 @@ async function summary(sqlite) {}/** Distinct (owner_app, owner_user_id) pairs that still lack a subject, with their object counts. */--async function pendingOwners(sqlite) {-- return await sqlite.prepare(`++async function pendingOwners(db) {++ return await db.prepare(`SELECT COALESCE(owner_app, app_id) AS owner_app, owner_user_id, count(*) AS objectsFROM media_objects WHERE owner_subject IS NULL AND owner_user_id IS NOT NULLGROUP BY COALESCE(owner_app, app_id), owner_user_id ORDER BY 1, 2`).all();@@@ -177,8 +177,8 @@ async function resolveOwners(owners, { resolver }) {* The rows the resolved subjects fill, read in id order `batch` at a time.* -> [{ id, app_id, owner_app, owner_user_id, owner_subject }]*/--async function planChanges(sqlite, subjects, { batch = DEFAULT_BATCH } = {}) {-- const page = sqlite.prepare(`++async function planChanges(db, subjects, { batch = DEFAULT_BATCH } = {}) {++ const page = db.prepare(`SELECT id, app_id, COALESCE(owner_app, app_id) AS owner_app, owner_user_id FROM media_objectsWHERE owner_subject IS NULL AND owner_user_id IS NOT NULL AND id > ? ORDER BY id LIMIT ?`);const changes = [];@@@ -201,15 +201,15 @@ async function planChanges(sqlite, subjects, { batch = DEFAULT_BATCH } = {}) {* Fill owner_subject on the planned rows, one transaction per `batch` rows. A row is written only if* it still has no subject and the same owner. -> { applied: [change], skipped: [change], batches }*/--async function applyChanges(sqlite, changes, { batch = DEFAULT_BATCH, onBatch = null } = {}) {-- const set = sqlite.prepare(`++async function applyChanges(db, changes, { batch = DEFAULT_BATCH, onBatch = null } = {}) {++ const set = db.prepare(`UPDATE media_objects SET owner_subject = ?, updated_at = ov_now()WHERE id = ? AND owner_subject IS NULL AND COALESCE(owner_app, app_id) = ? AND owner_user_id = ?`);const applied = [], skipped = [];let batches = 0;for (let i = 0; i < changes.length; i += batch) {const slice = changes.slice(i, i + batch);-- await sqlite.tx(async () => {++ await db.tx(async () => {for (const c of slice) ((await set.run(c.owner_subject, c.id, c.owner_app, c.owner_user_id)).changes ? applied : skipped).push(c);});batches++;@@@ -224,16 +224,16 @@ async function applyChanges(sqlite, changes, { batch = DEFAULT_BATCH, onBatch =* alone and counted. With apply=false nothing is written.* -> { restored, changed_since, missing, rows: { restored: [id], changed_since: [id], missing: [id] } }*/--async function rollbackChanges(sqlite, entries, { apply = false, batch = DEFAULT_BATCH } = {}) {-- const get = sqlite.prepare('SELECT id, owner_subject, COALESCE(owner_app, app_id) AS owner_app, owner_user_id FROM media_objects WHERE id = ?');-- const clear = sqlite.prepare(`++async function rollbackChanges(db, entries, { apply = false, batch = DEFAULT_BATCH } = {}) {++ const get = db.prepare('SELECT id, owner_subject, COALESCE(owner_app, app_id) AS owner_app, owner_user_id FROM media_objects WHERE id = ?');++ const clear = db.prepare(`UPDATE media_objects SET owner_subject = NULL, updated_at = ov_now()WHERE id = ? AND owner_subject = ? AND COALESCE(owner_app, app_id) = ? AND owner_user_id = ?`);const rows = { restored: [], changed_since: [], missing: [] };const unchanged = (row, e) => row.owner_subject === e.owner_subject && row.owner_app === e.owner_app && Number(row.owner_user_id) === Number(e.owner_user_id);for (let i = 0; i < entries.length; i += batch) {const slice = entries.slice(i, i + batch);-- await sqlite.tx(async () => {++ await db.tx(async () => {for (const e of slice) {const row = await get.get(e.id);if (!row) { rows.missing.push(e.id); continue; }@@@ -250,8 +250,8 @@ async function rollbackChanges(sqlite, entries, { apply = false, batch = DEFAULT* One reconcile pass (the service's job): resolve every owner still lacking a subject and fill their* objects. -> { owners, filled, skipped, unresolvable, unsupported, via }*/--async function reconcileOnce(sqlite, { resolver, batch = DEFAULT_BATCH } = {}) {-- const owners = await pendingOwners(sqlite);++async function reconcileOnce(db, { resolver, batch = DEFAULT_BATCH } = {}) {++ const owners = await pendingOwners(db);const out = { owners: owners.length, filled: 0, skipped: 0, unresolvable: 0, unsupported: 0, via: null };if (!owners.length) return out;const callsBefore = resolver.state ? resolver.state.calls : 0;@@@ -260,7 +260,7 @@ async function reconcileOnce(sqlite, { resolver, batch = DEFAULT_BATCH } = {}) {out.unsupported = r.unsupported.reduce((n, o) => n + o.objects, 0);out.via = resolver.state && resolver.state.calls > callsBefore ? resolver.state.via : null;if (!r.subjects.size) return out;-- const applied = await applyChanges(sqlite, await planChanges(sqlite, r.subjects, { batch }), { batch });++ const applied = await applyChanges(db, await planChanges(db, r.subjects, { batch }), { batch });out.filled = applied.applied.length;out.skipped = applied.skipped.length;return out;@@@ -56,7 +56,7 @@ function sha256File(p) {});}--/** SQLite 'YYYY-MM-DD HH:MM:SS' (UTC) or ISO → epoch ms (NaN when unreadable). */++/** Epoch ms from a `YYYY-MM-DD HH:MM:SS` (UTC) text timestamp, as the migrations store them, or ISO (NaN when unreadable). */function msOf(t) {const s = String(t || '');return Date.parse(/[zZ]|[+-]\d\d:?\d\d$/.test(s) ? s : `${s.replace(' ', 'T')}Z`);@@@ -5,7 +5,7 @@* GET /metrics Prometheus text; direct loopback callers only (404 through nginx)* GET /api/ready named checks; 503 only when a required one fails, optional failures → degraded*-- * Required (Media cannot serve without them): the SQLite database answers a real query, and every++ * Required (Media cannot serve without them): the PostgreSQL database answers a real query, and every* local storage directory accepts a write. Optional (one capability degrades, the rest keeps* working): the Network public key (user-JWT and service-token routes; app-key tenants are* unaffected), each CONFIGURED remote tier (B2 cold storage, R2 cache; a HeadBucket at most once a@@@ -39,7 +39,7 @@ const snip = (s, n = 200) => { const t = String(s || '').replace(/\s+/g, ' ').tr/** JSON that is safe inside a <script>: no `</` can end the element early. */const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c');--/** SQLite's "YYYY-MM-DD HH:MM:SS" (UTC) → ISO 8601, or null. */++/** ISO 8601 from a "YYYY-MM-DD HH:MM:SS" (UTC) text timestamp, as the migrations store them, or null. */function isoDate(dt) {if (!dt) return null;const d = new Date(String(dt).includes('T') ? dt : `${dt}Z`.replace(' ', 'T'));@@@ -108,7 +108,7 @@ async function measure(vod, { confirm = false, confirmRemote = false, probeTimeo}function reportsDir() {-- return path.join(path.dirname(config.db.path), 'reports');++ return path.join(config.dataDir, 'reports');}function writeReport(report, file = null) {@@@ -438,7 +438,7 @@ async function buildStorageReport({ providers = null, list = null, listUploads =}function reportsDir() {-- return path.join(path.dirname(config.db.path), 'reports');++ return path.join(config.dataDir, 'reports');}/** Write a storage report as JSON (default <data>/reports/storage-orphans-<time>.json). Returns the path. */@@@ -9,7 +9,7 @@* storage.alert | storage.recovered** Outcomes go through announce(): the state change and its durable OpenVibe.Events outbox row-- * commit in one SQLite transaction, then the webhook is sent with `event_id` = that event's id++ * commit in one PostgreSQL transaction, then the webhook is sent with `event_id` = that event's id* (absent when the outbox is off), so a consumer that also reads Events dedupes the pair.*/'use strict';@@@ -79,7 +79,7 @@ async function sendWebhook(appOrId, event, data, { eventId = null } = {}) {/*** Commit an outcome and announce it. `change()` (synchronous DB writes, optional) and the outbox-- * row for `event` run in ONE SQLite transaction, so the durable event exists if and only if the++ * row for `event` run in ONE PostgreSQL transaction, so the durable event exists if and only if the* change committed: never lost after a commit, never announced for a rollback. `payload()` is read* inside that transaction (the row as committed). After the commit the relay is woken and the* app's webhook is sent with the same event_id. Returns { data, eventId }; throws (nothing changed,@@@ -13,7 +13,6 @@ const path = require('path');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-account-data-'));-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.MEDIA_INBOUND_EVENTS_SECRET = 'm'.repeat(40);const log = console.log;console.log = (...a) => { if (!/^\[/.test(String(a[0]))) log(...a); };@@@ -13,7 +13,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-limits-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.FILES_PATH = dir('files');process.env.OBJECTS_PATH = dir('objects');@@@ -16,7 +16,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-tenantabuse-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.FILES_PATH = dir('files');process.env.OBJECTS_PATH = dir('objects');@@@ -14,7 +14,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-apptenants-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.FILES_PATH = dir('files');process.env.OBJECTS_PATH = dir('objects');@@@ -12,7 +12,6 @@ const { spawnSync } = require('child_process');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-clipjob-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -16,7 +16,6 @@ const http = require('http');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-clip-origin-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -14,7 +14,7 @@ const crypto = require('crypto');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-hash-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),MEDIA_INVARIANT_SCAN_HOURS: '0', MEDIA_HASH_SETTLE_S: '60', MEDIA_HASH_INTERVAL_MIN: '15',});@@@ -13,7 +13,7 @@ const express = require('express');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-crawl-'));-- Object.assign(process.env, { DB_PATH: path.join(tmp, 'media.db'), MEDIA_PUBLIC_URL: 'https://media.test', VOD_PATH: path.join(tmp, 'vods'), CLIPS_PATH: path.join(tmp, 'clips') });++ Object.assign(process.env, { MEDIA_PUBLIC_URL: 'https://media.test', VOD_PATH: path.join(tmp, 'vods'), CLIPS_PATH: path.join(tmp, 'clips') });const db = require('../server/db/database');const pages = require('../server/public/pages');@@@ -12,7 +12,6 @@ const path = require('path');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-dbopen-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');for (const [k, n] of [['VOD_PATH', 'vods'], ['CLIPS_PATH', 'clips'], ['FILES_PATH', 'files'], ['THUMBNAILS_PATH', 'thumbnails'], ['PASTES_PATH', 'pastes'], ['OBJECTS_PATH', 'objects']]) process.env[k] = dir(n);process.env.MEDIA_PUBLIC_URL = 'https://media.test';console.log = () => {};@@@ -61,7 +61,7 @@ const { spawnSync, execFile } = require('child_process');await new Promise((r) => s3.listen(0, '127.0.0.1', r));const endpoint = `http://127.0.0.1:${s3.address().port}`;Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'data', 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ MEDIA_DATA_DIR: path.join(tmp, 'data'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',MEDIA_INVARIANT_SCAN_HOURS: '0', MEDIA_JOBS_ENABLED: '0',@@@ -146,7 +146,7 @@ const { spawnSync, execFile } = require('child_process');assert.strictEqual(j.status, 'succeeded', j.error);let res = JSON.parse(j.result);assert.deepStrictEqual([res.mode, res.counts.checked, res.counts.repaired, res.next_after_id], ['apply', 4, 3, ids.missing]);-- assert.ok(fs.existsSync(path.join(path.dirname(process.env.DB_PATH), 'reports', res.report)), 'the report is written');++ assert.ok(fs.existsSync(path.join(process.env.MEDIA_DATA_DIR, 'reports', res.report)), 'the report is written');assert.deepStrictEqual([(await vod(ids.wallClock)).duration_seconds, (await vod(ids.wallClock)).duration_source], [6, 'probe']);assert.deepStrictEqual([(await vod(ids.remote)).duration_seconds, (await vod(ids.remote)).duration_source], [8, 'probe']);assert.deepStrictEqual([(await vod(ids.missing)).duration_seconds, (await vod(ids.missing)).duration_source], [6, 'probe']);@@@ -156,7 +156,7 @@ const { spawnSync, execFile } = require('child_process');j = (await queue.enqueue({ appId: 'live', type: 'vod.duration.reconcile', params: spec.validate({ obj: null, params: { limit: 4, apply: true } }), createdBy: 'test2' })).job;res = JSON.parse((await worker.runNow(j.id)).result);assert.deepStrictEqual([res.range.after_id, res.counts.checked, res.range.done, res.next_after_id], [ids.missing, 3, true, 0], 'continues, reaches the end, wraps');-- const report = JSON.parse(fs.readFileSync(path.join(path.dirname(process.env.DB_PATH), 'reports', JSON.parse((await queue.list('live', { type: 'vod.duration.reconcile' })).jobs.slice(-1)[0].result).report), 'utf8'));++ const report = JSON.parse(fs.readFileSync(path.join(process.env.MEDIA_DATA_DIR, 'reports', JSON.parse((await queue.list('live', { type: 'vod.duration.reconcile' })).jobs.slice(-1)[0].result).report), 'utf8'));const rb0 = await reconcile.rollback(report.rows);assert.deepStrictEqual(rb0, { restored: 0, would_restore: 3, changed_since: 0 });const rb = await reconcile.rollback(report.rows, { apply: true });@@@ -11,7 +11,6 @@ const http = require('http');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-events-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -15,7 +15,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-export-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.FILES_PATH = dir('files');process.env.OBJECTS_PATH = dir('objects');@@@ -10,8 +10,8 @@ const require = createRequire(import.meta.url);const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');const { createTestDb } = require('openvibe-sdk/testing');const t = await createTestDb({ migrations: path.join(root, 'migrations'), store: process.env.MEDIA_TEST_STORE || 'pglite', service: 'media', max: 4 });--// Tests insert rows with small explicit ids (SQLite's AUTOINCREMENT then continued after them; a PostgreSQL identity--// does not): generated ids start at 100000 here, clear of every id a test names.++// Tests insert rows with small explicit ids. A PostgreSQL identity would continue after them, so generated ids++// are set to start at 100000 here, clear of every id a test names.for (const r of await t.db.prepare(`SELECT pg_get_serial_sequence(quote_ident(table_name), column_name) AS seq FROM information_schema.columnsWHERE table_schema = current_schema() AND is_identity = 'YES'`).all()) {if (r.seq) await t.db.prepare('SELECT setval(?::regclass, 100000)').get(r.seq);@@@ -13,7 +13,6 @@ const path = require('path');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-fencing-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -15,7 +15,6 @@ const { spawnSync } = require('child_process');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-jobinv-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -13,7 +13,6 @@ const { spawnSync } = require('child_process');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-previews-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -15,7 +15,6 @@ const http = require('http');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-jobs-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -238,7 +237,7 @@ const http = require('http');assert.deepStrictEqual(r.body.jobs.map((j) => j.id), [scanId]);r = await call('GET', `/api/v2/live/jobs/${scanId}`);assert.deepStrictEqual([r.status, r.body.job.idempotency_key, r.body.job.params, r.body.job.created_by, r.body.job.created_at.includes('T')], [200, 'scan-1', {}, 'app:live', false],-- 'GET the job answers as before (queue.jobPublic: params, idempotency key, creator, SQLite times)');++ "GET the job answers as before (queue.jobPublic: params, idempotency key, creator, the store's `YYYY-MM-DD HH:MM:SS` UTC text timestamps)");r = await call('GET', `/api/v2/games/jobs/${scanId}`, { key: 'games-key' });assert.strictEqual(r.status, 404, 'tenants are isolated');r = await call('GET', `/api/v2/live/jobs?status=bogus`);@@@ -21,7 +21,7 @@ const http = require('http');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-me-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',});@@@ -12,7 +12,7 @@ const http = require('http');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-modevents-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),});const stub = http.createServer((req, res) => { req.resume(); req.on('end', () => { res.setHeader('Content-Type', 'application/json'); res.statusCode = 404; res.end('{}'); }); });@@@ -77,11 +77,11 @@ async function check(name, fn) {fs.mkdirSync(path.dirname(dbPath), { recursive: true });fs.mkdirSync(dataDir, { recursive: true });let server = null;-- // This release on PostgreSQL (ADR-035) and N-1 on SQLite: they never share a database (the switch stops the-- // service, imports, then starts this release), so N-1's SQL has no schema of this release to run on. The client-- // half still runs, on a freshly migrated and seeded database. Both on PostgreSQL: N keeps every migration N-1 ran.++ // This release on PostgreSQL (ADR-035) and a recorded N-1 that still ran on SQLite: they never share a database,++ // so N-1's SQL has no schema of this release to run on. The client half still runs, on a freshly migrated and++ // seeded database. Once the N-1 fixture is recorded from a PostgreSQL release: N keeps every migration N-1 ran.const pg = fs.existsSync(path.join(ROOT, 'migrations'));-- const sqlSkip = pg && worker.engine !== 'postgresql' ? 'N-1 SQL: skipped (N-1 runs on SQLite and this release on PostgreSQL: no shared database, so no N-1 statement runs on this schema; scripts/migrate-to-postgres.js moves the data and was rehearsed)' : null;++ const sqlSkip = pg && worker.engine !== 'postgresql' ? 'N-1 SQL: skipped (the recorded N-1 release runs on SQLite and this release on PostgreSQL: no shared database, so no N-1 statement runs on this schema)' : null;if (pg && worker.engine === 'postgresql') {await check(`N-1's ${worker.migrations.length} migration file(s) are all here, unchanged`, () => {const dir = path.join(ROOT, 'migrations');@@@ -18,7 +18,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-namespaces-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.FILES_PATH = dir('files');process.env.OBJECTS_PATH = dir('objects');@@@ -51,8 +50,8 @@ const crypto = require('crypto');iss: 'https://openvibe.network', sub: `app:${APP_A}`, actor_type: 'app', aud: ['openvibe.media'], cap, ns, project_id: project, env, iat: now, exp: now + 300, jti: jti(),}, keys.privateKey);-- // Tenants: Live, and developer project PA in production and in its sandbox. (The SQLite release moved older rows-- // into namespaces at boot; on PostgreSQL the schema starts with them, and writes keep them.)++ // Tenants: Live, and developer project PA in production and in its sandbox. (On PostgreSQL the schema starts++ // with the namespaces in place, and writes keep them.)await db.upsertApp({ app_id: 'live', api_key: 'live-key' });await db.ensureProjectTenant(PA, 'production', 1000000);await db.ensureProjectTenant(PA, 'sandbox', 1000000);@@@ -14,7 +14,7 @@ const http = require('http');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objevents-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),});@@@ -1,6 +1,6 @@'use strict';// Object-first writes (WS-G task 1, retiring compatibility shim C-75): every write to an inherited--// vods/clips/files/pastes row makes or updates its media_object in the SAME SQLite transaction++// vods/clips/files/pastes row makes or updates its media_object in the SAME PostgreSQL transaction// (objects/model.js withObject). For each main write path the object is there, linked and agreeing// with the row, as soon as the write returns; when the object cannot be written, the row write// rolls back with it (neither row); and the drift report (server/objects/drift.js,@@@ -18,7 +18,7 @@ const sharp = require('sharp');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-object-first-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_PUBLIC_URL: 'https://media.test',});@@@ -69,7 +69,7 @@ const crypto = require('crypto');await new Promise((r) => s3.listen(0, '127.0.0.1', r));const endpoint = `http://127.0.0.1:${s3.address().port}`;Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',@@@ -13,7 +13,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objapi-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.FILES_PATH = dir('files');process.env.OBJECTS_PATH = dir('objects');@@@ -11,7 +11,6 @@ const path = require('path');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objects-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -13,7 +13,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-multipart-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.FILES_PATH = dir('files');process.env.OBJECTS_PATH = dir('objects');@@@ -13,7 +13,7 @@ const crypto = require('crypto');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-reconcile-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };const env = {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_PUBLIC_URL: 'https://media.test',MEDIA_PUBLIC_OBJECT_TARGET_MB: '1', MEDIA_PUBLIC_OBJECT_WARN_MB: '2', MEDIA_PUBLIC_OBJECT_MAX_MB: '3',};@@@ -15,7 +15,7 @@ const crypto = require('crypto');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-verify-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };const env = {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9',};@@@ -14,7 +14,7 @@ const { spawn } = require('child_process');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-obs-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };const ENV = {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), PASTES_PATH: dir('pastes'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), PASTES_PATH: dir('pastes'),THUMBNAILS_PATH: dir('thumbnails'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9', RELEASE_COMMIT: 'abcdef1234567',};@@@ -132,13 +132,13 @@ const { spawn } = require('child_process');// ── The real index.js boots with both routes ──const port = await new Promise((resolve) => { const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); });const bootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-obs-boot-'));-- const env = { ...process.env, ...ENV, DB_PATH: path.join(bootDir, 'media.db'), MEDIA_PGLITE_DIR: path.join(bootDir, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test' };++ const env = { ...process.env, ...ENV, MEDIA_PGLITE_DIR: path.join(bootDir, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test' };for (const k of ['VOD_PATH', 'CLIPS_PATH', 'PASTES_PATH', 'THUMBNAILS_PATH', 'FILES_PATH', 'OBJECTS_PATH', 'ASSETS_PATH']) env[k] = path.join(bootDir, k.toLowerCase());const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });let log = ''; child.stdout.on('data', (d) => { log += d; }); child.stderr.on('data', (d) => { log += d; });const b2 = `http://127.0.0.1:${port}`;let up = null;-- for (let i = 0; i < 100 && !up; i++) {++ for (let i = 0; i < 300 && !up; i++) { // 30 s: a cold PGlite boot plus migrations is slow on a loaded machineawait new Promise((res) => setTimeout(res, 100));try { up = await request(b2, '/api/ready'); } catch { /* not listening yet */ }}@@@ -1,5 +1,5 @@'use strict';--// Roadmap Wave 3 (audit item 7): Media's outbox row is written in the SAME SQLite transaction as++// Roadmap Wave 3 (audit item 7): Media's outbox row is written in the SAME PostgreSQL transaction as// the state change it describes. An outcome is never lost (the change committed, the event did// not) and never a phantom (the event exists, the change rolled back). The webhook for the same// outcome follows the commit and carries the event's id, so a consumer reading both paths@@@ -15,7 +15,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-outbox-tx-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -15,7 +15,7 @@ const { ids } = require('openvibe-contracts');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-owner-subject-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };const env = {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9', MEDIA_SIGNING_SECRET: 'test-signing-secret',INTERNAL_API_KEY: 'internal-key-0123456789abcdef', OV_OAUTH_CLIENT_ID: 'media', OV_OAUTH_CLIENT_SECRET: 'media-secret',@@@ -97,7 +97,7 @@ const { ids } = require('openvibe-contracts');for (let uid = 1000; uid < 1520; uid++) { subjectOf(uid); many.push(await obj({ owner_user_id: uid, kind: 'clip' })); }const TO_FILL = 3 + many.length;-- // (The one-off backfill script ran in production on SQLite and is retired: the reconcile job below fills what is left.)++ // (The one-off backfill script is retired: the reconcile job below fills what is left.)// A token Network refuses at resolve-batch: the key answers instead.const resolver = ownerSubject.createResolver({ networkUrl: NET });net.grant = true;@@@ -10,7 +10,6 @@ const express = require('express');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-moved-'));-- process.env.DB_PATH = path.join(tmp, 'media.db');const db = require('../server/db/database');await db.upsertApp({ app_id: 'live', api_key: 'live-key' });const shot = path.join(tmp, 'shot.png');@@@ -32,7 +32,7 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');const port = await freePort();const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {cwd: path.join(__dirname, '..'),-- env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test', DB_PATH: path.join(dir, 'media.db'), MEDIA_PGLITE_DIR: path.join(dir, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', VOD_PATH: path.join(dir, 'vods'), CLIPS_PATH: path.join(dir, 'clips'), FILES_PATH: path.join(dir, 'files'), THUMBNAILS_PATH: path.join(dir, 'thumbnails'), PASTES_PATH: path.join(dir, 'pastes'), OBJECTS_PATH: path.join(dir, 'objects'), MEDIA_JOBS_ENABLED: 'off' },++ env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test', MEDIA_PGLITE_DIR: path.join(dir, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', VOD_PATH: path.join(dir, 'vods'), CLIPS_PATH: path.join(dir, 'clips'), FILES_PATH: path.join(dir, 'files'), THUMBNAILS_PATH: path.join(dir, 'thumbnails'), PASTES_PATH: path.join(dir, 'pastes'), OBJECTS_PATH: path.join(dir, 'objects'), MEDIA_JOBS_ENABLED: 'off' },stdio: ['ignore', 'ignore', 'pipe'],});let stderr = '';@@@ -13,7 +13,6 @@ const http = require('http');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-private404-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -9,7 +9,6 @@ const http = require('http');(async () => {const tmp = path.join(os.tmpdir(), `ov-media-pages-${process.pid}.db`);-- process.env.DB_PATH = tmp;process.env.MEDIA_PUBLIC_URL = 'https://media.test';const db = require('../server/db/database');@@@ -73,7 +73,7 @@ const crypto = require('crypto');await new Promise((r) => s3.listen(0, '127.0.0.1', r));const endpoint = `http://127.0.0.1:${s3.address().port}`;Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',@@@ -16,7 +16,7 @@ const http = require('http');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-readiness-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',});@@@ -13,7 +13,7 @@ const { EventEmitter } = require('events');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-rec-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),});delete process.env.MEDIA_RTMP_PULL_ALLOW;@@@ -16,7 +16,7 @@ const http = require('http');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-holds-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',});@@@ -10,7 +10,6 @@ const http = require('http');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-revoke-'));-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.MEDIA_INBOUND_EVENTS_SECRET = 'm'.repeat(40);const express = require('express');const { signDeliveryHeaders } = require('openvibe-sdk/events');@@@ -14,7 +14,7 @@ const express = require('express');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-rtmp-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),});delete process.env.MEDIA_RTMP_PULL_ALLOW;@@@ -23,7 +23,7 @@ const crypto = require('crypto');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-idor-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), ASSETS_PATH: dir('assets'),OV_NETWORK_URL: 'https://openvibe.network', MEDIA_PUBLIC_URL: 'https://media.test', MEDIA_SIGNING_SECRET: 'test-signing-secret',MEDIA_UPLOAD_MIN_FREE_MB: '0', MEDIA_MULTIPART_MIN_PART_MB: '1', MEDIA_JOBS_ENABLED: '0',@@@ -21,7 +21,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-secprivate-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.FILES_PATH = dir('files');@@@ -86,7 +86,7 @@ const crypto = require('crypto');const NET = `http://127.0.0.1:${network.address().port}`;Object.assign(process.env, SECRET_ENV, {PORT: '0', HOST: '127.0.0.1', NODE_ENV: 'test',-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), PASTES_PATH: dir('pastes'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), PASTES_PATH: dir('pastes'),THUMBNAILS_PATH: dir('thumbnails'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: NET, OV_NETWORK_INTERNAL_URL: NET, EVENTS_URL: NET,OV_LIVE_INTERNAL_URL: CLOSED, LIVE_APP_INTERNAL_URL: CLOSED, APP_INTERNAL_URLS: JSON.stringify({ live: CLOSED }),@@@ -21,7 +21,6 @@ const https = require('https');(async () => {const ROOT = path.join(__dirname, '..');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-ssrf-'));-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.PASTES_PATH = path.join(tmp, 'pastes');process.env.MEDIA_PUBLIC_URL = 'https://media.test';delete process.env.MEDIA_RTMP_PULL_ALLOW;@@@ -10,7 +10,6 @@ const crypto = require('crypto');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-svc-'));-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.FILES_PATH = path.join(tmp, 'files');process.env.OV_NETWORK_URL = 'https://openvibe.network';process.env.MEDIA_PUBLIC_URL = 'https://media.test';@@@ -7,7 +7,6 @@ const path = require('path');(async () => {const tmp = path.join(os.tmpdir(), `ov-media-series-${process.pid}.db`);-- process.env.DB_PATH = tmp;const db = require('../server/db/database');const raw = db.getDb();@@@ -53,7 +53,7 @@ const crypto = require('crypto');await new Promise((r) => s3.listen(0, '127.0.0.1', r));const endpoint = `http://127.0.0.1:${s3.address().port}`;const env = {-- DB_PATH: path.join(tmp, 'data', 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ MEDIA_DATA_DIR: path.join(tmp, 'data'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',@@@ -15,7 +15,7 @@ const express = require('express');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-thumbfail-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_INVARIANT_SCAN_HOURS: '0',});@@@ -12,7 +12,7 @@ const path = require('path');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-tiercfg-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),});@@@ -63,7 +63,7 @@ const crypto = require('crypto');await new Promise((r) => s3.listen(0, '127.0.0.1', r));const endpoint = `http://127.0.0.1:${s3.address().port}`;Object.assign(process.env, {-- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',@@@ -57,7 +57,7 @@ const express = require('express');assert.ok(!/set\('trust proxy',\s*true\)/.test(src), 'no trust-everyone left');const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-trust-'));const freePort = await new Promise((resolve) => { const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); });-- const env = { ...process.env, DB_PATH: path.join(tmp, 'media.db'), MEDIA_PGLITE_DIR: path.join(tmp, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', VOD_PATH: path.join(tmp, 'vods'), CLIPS_PATH: path.join(tmp, 'clips'), PASTES_PATH: path.join(tmp, 'pastes'),++ const env = { ...process.env, MEDIA_PGLITE_DIR: path.join(tmp, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', VOD_PATH: path.join(tmp, 'vods'), CLIPS_PATH: path.join(tmp, 'clips'), PASTES_PATH: path.join(tmp, 'pastes'),THUMBNAILS_PATH: path.join(tmp, 'thumbs'), FILES_PATH: path.join(tmp, 'files'), OBJECTS_PATH: path.join(tmp, 'objects'), PORT: String(freePort), HOST: ext ? '0.0.0.0' : '127.0.0.1',NODE_ENV: 'test', MEDIA_JOBS_ENABLED: '0', MEDIA_VERIFY_ENABLED: '0', MEDIA_OWNER_SUBJECT_SYNC: '0', EVENTS_URL: '', MEDIA_APP_KEYS: 'live:live-key-for-trust-test' };const child = spawn(process.execPath, [path.join(__dirname, '../server/index.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });@@@ -13,7 +13,6 @@ const { spawnSync } = require('child_process');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-duration-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.THUMBNAILS_PATH = dir('thumbnails');@@@ -12,7 +12,6 @@ const path = require('path');(async () => {const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-finalize-job-'));const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };-- process.env.DB_PATH = path.join(tmp, 'media.db');process.env.VOD_PATH = dir('vods');process.env.CLIPS_PATH = dir('clips');process.env.THUMBNAILS_PATH = dir('thumbnails');
@@ -9,14 +9,15 @@ NODE_ENV=development
@@ -9,14 +9,15 @@ NODE_ENV=development
MEDIA_PUBLIC_URL=https://openvibe.media
MEDIA_PUBLIC_URL=https://openvibe.media
# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh media: the runtime role through PgBouncer,
# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh media: the runtime role through PgBouncer,
-# the owner on a direct connection (migrations and the one-time import), and Valkey (shared limit counters).
-# Without DATABASE_URL, development uses an embedded PGlite database in data/pglite.
+# the owner on a direct connection (migrations), and Valkey (shared limit counters).
+# Without DATABASE_URL, development uses an embedded PGlite database in data/pglite (MEDIA_PGLITE_DIR overrides it).
DATABASE_URL=
DATABASE_URL=
DATABASE_DIRECT_URL=
DATABASE_DIRECT_URL=
VALKEY_URL=
VALKEY_URL=
VALKEY_PREFIX=
VALKEY_PREFIX=
-# The SQLite file of releases before PostgreSQL: read once by scripts/migrate-to-postgres.js.
-DB_PATH=./data/media.db
+# The service's data directory: the reports (duration reconcile, storage orphans, H15 repair) and the drill
+# artifacts are written under it.
+MEDIA_DATA_DIR=./data
# Restore-drill mode, set only by `ovhost drill media` (OpenVibe.Host docs/restore-drills.md): reads
# Restore-drill mode, set only by `ovhost drill media` (OpenVibe.Host docs/restore-drills.md): reads
# only from a restored copy, no stored bytes, no jobs, webhooks, events or ffmpeg; refuses to start
# only from a restored copy, no stored bytes, no jobs, webhooks, events or ffmpeg; refuses to start
@@ -71,13 +71,15 @@ reads events.
@@ -71,13 +71,15 @@ reads events.
```
```
server/
server/
index.js boot, route mounts, background jobs, graceful shutdown
index.js boot, route mounts, background jobs, graceful shutdown
- config.js env config (PORT, DB_PATH, *_PATH, MEDIA_B2_*/R2_*, RTP pool, OV_NETWORK_URL)
+ config.js env config (PORT, MEDIA_DATA_DIR, *_PATH, DATABASE_URL/DATABASE_DIRECT_URL, MEDIA_B2_*/R2_*,
+ RTP pool, OV_NETWORK_URL)
auth.js tenancy + auth middleware, JWKS fetch, app seeding
auth.js tenancy + auth middleware, JWKS fetch, app seeding
webhooks.js HMAC-signed outbound webhooks
webhooks.js HMAC-signed outbound webhooks
drill.js MEDIA_DRILL: restore-drill mode (reads only, no bytes, no jobs, nothing leaves)
drill.js MEDIA_DRILL: restore-drill mode (reads only, no bytes, no jobs, nothing leaves)
- db/schema.sql apps, vods, clips, pastes(+likes/comments), files, content_views, media_settings,
- media_objects/locations/relationships/variants/jobs/holds/invariant_violations
db/database.js PostgreSQL helpers (openvibe-sdk/db, async), all app_id-scoped
db/database.js PostgreSQL helpers (openvibe-sdk/db, async), all app_id-scoped
+ db/init.js applies migrations/ (DATABASE_URL/DATABASE_DIRECT_URL, or embedded PGlite)
+ migrations/NNNN_*.sql apps, vods, clips, pastes(+likes/comments), files, content_views, media_settings,
+ media_objects/locations/relationships/variants/jobs/holds/invariant_violations
vod/recorder.js ffmpeg recording: RTMP pull + RTP (SDP) ingest, codec passthrough
vod/recorder.js ffmpeg recording: RTMP pull + RTP (SDP) ingest, codec passthrough
vod/media-tools.js probes, seekable remux, DVR sidecar, chunk-segment concat
vod/media-tools.js probes, seekable remux, DVR sidecar, chunk-segment concat
vod/finalize.js finalize pipeline (remux → probe → master recovery → thumbnail → webhook)
vod/finalize.js finalize pipeline (remux → probe → master recovery → thumbnail → webhook)
@@ -381,9 +383,8 @@ that is deleted; none for sandbox tenants. `test/object-events.test.js`.
@@ -381,9 +383,8 @@ that is deleted; none for sandbox tenants. `test/object-events.test.js`.
## Restore drills (`MEDIA_DRILL=1`)
## Restore drills (`MEDIA_DRILL=1`)
A restore drill starts a second Media from this checkout on 127.0.0.1:14100 with `MEDIA_DRILL=1` and
A restore drill starts a second Media from this checkout on 127.0.0.1:14100 with `MEDIA_DRILL=1` and
-`DATABASE_URL` naming a restored copy of the database (pgBackRest restores the cluster on a spare port; the Host's
-`ovhost drill media` still restores the SQLite backups of the releases before PostgreSQL and needs that step before
-it drills this one). In that mode (`server/drill.js`) Media:
+`DATABASE_URL` naming a restored copy of the database (pgBackRest restores the cluster on a spare port). In that
+mode (`server/drill.js`) Media:
- refuses to start unless `DATABASE_URL` is set and does not name production's database (`ov_media` on 5432, or
- refuses to start unless `DATABASE_URL` is set and does not name production's database (`ov_media` on 5432, or
PgBouncer's 6432), `DATABASE_DIRECT_URL` is unset (a drill never migrates), `HOST` is loopback and `PORT` is not
PgBouncer's 6432), `DATABASE_DIRECT_URL` is unset (a drill never migrates), `HOST` is loopback and `PORT` is not
@@ -573,16 +574,6 @@ guardian (refuses new recordings when free space is critical), stale live-thumb
@@ -573,16 +574,6 @@ guardian (refuses new recordings when free space is critical), stale live-thumb
cleanup, and an on-boot sweep that finalizes recordings orphaned by an unclean
cleanup, and an on-boot sweep that finalizes recordings orphaned by an unclean
shutdown.
shutdown.
-## Moving to PostgreSQL
-
-`scripts/migrate-to-postgres.js` is the one-time import of the SQLite `media.db` (`DB_PATH`) into `ov_media`
-(openvibe-sdk `runSqliteMigration`: migrations as the owner, every table copied into emptied tables, counts and
-checksums verified; `--pglite` rehearses it in memory). It runs while the service is stopped (OpenVibe.Host
-`roles/data/switch-service.sh media /opt/openvibe.media/data/media.db`); the SQLite file stays read-only for 7 days as
-the rollback. Timestamps stay SQLite's text (`ov_now()`, `datetime()`, `julianday()` in the migration); the retention
-hold guards and the object-change triggers are PL/pgSQL. (The predecessor-DB importer and its bulk-copy recipe were
-retired with the move.)
-
## Not ported
## Not ported
- **Song-request media queue** (`media/` yt-dlp downloader/queue): deeply
- **Song-request media queue** (`media/` yt-dlp downloader/queue): deeply
@@ -610,8 +601,9 @@ Run `npm test` for the policy regression tests.
@@ -610,8 +601,9 @@ Run `npm test` for the policy regression tests.
## Acceptance
## Acceptance
-`npm test` runs every `test/*.test.js` in turn (temp databases, local stubs, no network). What they
-prove includes: tenancy and developer-project tenants (`app-tenants`, `app-tenant-abuse`); the object
+`npm test` runs every `test/*.test.js` in turn (each process gets a migrated PGlite database, or the
+PostgreSQL containers under `npm run test:pg`; local stubs, no network). What they prove includes: tenancy
+and developer-project tenants (`app-tenants`, `app-tenant-abuse`); the object
model, multipart, reconcile, verification and tiering (`objects-*`, `object-tiering`, `tier-*`); jobs
model, multipart, reconcile, verification and tiering (`objects-*`, `object-tiering`, `tier-*`); jobs
with leases and fencing (`jobs*`); VOD finalize, durations and storage policy (`vod-*`); private items
with leases and fencing (`jobs*`); VOD finalize, durations and storage policy (`vod-*`); private items
stay private and nobody acts on another's ids (`security-private`, `security-idor`,
stay private and nobody acts on another's ids (`security-private`, `security-idor`,
@@ -656,5 +648,4 @@ writes its settings); the release migrates it at boot; nginx serves `openvibe.me
@@ -656,5 +648,4 @@ writes its settings); the release migrates it at boot; nginx serves `openvibe.me
fixtures (`npm run n-1:record`).
fixtures (`npm run n-1:record`).
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
-restart; afterwards `sudo ovhost rollback media --to <sha>`. A release from before PostgreSQL reads the SQLite
-`media.db` it left (read-only for 7 days after the switch); anything written since would need moving back first.
+restart; afterwards `sudo ovhost rollback media --to <sha>`.
@@ -76,7 +76,7 @@ the same functions.
@@ -76,7 +76,7 @@ the same functions.
## Backfill
## Backfill
```
```
-node scripts/backfill-objects.js [--dry-run] [--only-missing] [--json] [--out report.json] [--db ./data/media.db]
+node scripts/backfill-objects.js [--dry-run] [--only-missing] [--json] [--out report.json]
```
```
The backfill creates one object per vod, clip, file, screenshot/avatar paste and thumbnail, with
The backfill creates one object per vod, clip, file, screenshot/avatar paste and thumbnail, with
@@ -90,12 +90,12 @@ relationships and thumbnail variants.
@@ -90,12 +90,12 @@ relationships and thumbnail variants.
Skip reasons today: `clips-only recording (ephemeral, never published)`, `screenshot paste without a file path`, `external thumbnail url`.
Skip reasons today: `clips-only recording (ephemeral, never published)`, `screenshot paste without a file path`, `external thumbnail url`.
-The run holds SQLite's write lock for its duration, typically seconds. Prefer the boot backfill, or
+The backfill's own transaction holds row and table locks for its duration, typically seconds. Prefer the boot backfill, or
run the script while traffic is quiet.
run the script while traffic is quiet.
### Keeping the model current: object-first writes
### Keeping the model current: object-first writes
-Every write to a projected row writes its object in the same SQLite transaction
+Every write to a projected row writes its object in the same PostgreSQL transaction
(`model.withObject(kind, ids, write)`; roadmap WS-G task 1, which retires compatibility shim C-75,
(`model.withObject(kind, ids, write)`; roadmap WS-G task 1, which retires compatibility shim C-75,
"write the row, then sync the object"). The row change and the re-projection commit together or
"write the row, then sync the object"). The row change and the re-projection commit together or
not at all, so a crash or an error between them can no longer leave the object behind its row. A
not at all, so a crash or an error between them can no longer leave the object behind its row. A
@@ -119,7 +119,7 @@ after the outcome event.
@@ -119,7 +119,7 @@ after the outcome event.
| avatar ingest | `avatars/ingest` |
| avatar ingest | `avatars/ingest` |
| tier moves | `moveToCold`, `moveToHot`, `promoteToR2`, `demoteFromR2`, sweep, `migrateLegacy`: the copy the move verified is marked `present` in the same transaction |
| tier moves | `moveToCold`, `moveToHot`, `promoteToR2`, `demoteFromR2`, sweep, `migrateLegacy`: the copy the move verified is marked `present` in the same transaction |
-**Deletes need nothing more.** A SQLite trigger on `vods`, `clips`, `files` and `pastes` marks the
+**Deletes need nothing more.** A PostgreSQL trigger (defined in `migrations/`, PL/pgSQL) on `vods`, `clips`, `files` and `pastes` marks the
object `deleted` in the same statement whenever its row is deleted, which covers all of the
object `deleted` in the same statement whenever its row is deleted, which covers all of the
inherited delete paths.
inherited delete paths.
@@ -137,7 +137,7 @@ projects what it inserts.
@@ -137,7 +137,7 @@ projects what it inserts.
### Drift report
### Drift report
```
```
-node scripts/object-drift-report.js [--app live] [--limit 20] [--json] [--out report.json] [--db ./data/media.db]
+node scripts/object-drift-report.js [--app live] [--limit 20] [--json] [--out report.json]
```
```
For every projected row, and each vod/clip thumbnail, the report derives what the object must say
For every projected row, and each vod/clip thumbnail, the report derives what the object must say
@@ -183,17 +183,18 @@ Rules for both job and backfill:
@@ -183,17 +183,18 @@ Rules for both job and backfill:
request uses `INTERNAL_API_KEY`.
request uses `INTERNAL_API_KEY`.
```
```
-node scripts/backfill-owner-subject.js [--db media.db] [--batch 500] [--json] # dry run: counts per tenant
+node scripts/backfill-owner-subject.js [--batch 500] [--json] # dry run: counts per tenant
node scripts/backfill-owner-subject.js --apply --backup <file.json> [--batch 500] # fill
node scripts/backfill-owner-subject.js --apply --backup <file.json> [--batch 500] # fill
node scripts/backfill-owner-subject.js --rollback <file.json> [--apply] # undo (dry without --apply)
node scripts/backfill-owner-subject.js --rollback <file.json> [--apply] # undo (dry without --apply)
```
```
- **The dry run** (the default) opens the database read-only and prints per tenant: objects, already
- **The dry run** (the default) opens the database read-only and prints per tenant: objects, already
set, to fill, unresolvable, unsupported tenant and no owner.
set, to fill, unresolvable, unsupported tenant and no owner.
-- **`--apply --backup <file.json>`** first takes an online backup to `<file>.media.db` and requires
- `PRAGMA integrity_check` = `ok` on it. It then writes `<file.json>` (0600), the rows it is about to
- change, and fills them in `--batch` transactions. Finally it rewrites the file to list exactly the
- rows it changed. It refuses an existing backup name, and a re-run fills only what is still missing.
+- **`--apply --backup <file.json>`** writes the rollback file `<file.json>` (0600) with the rows it is
+ about to change, and fills them in `--batch` transactions. The database's own safety net is
+ PostgreSQL's point-in-time recovery (pgBackRest), not an online copy. Finally it rewrites the file to
+ list exactly the rows it changed. It refuses an existing backup name, and a re-run fills only what is
+ still missing.
- **`--rollback <file.json> --apply`** sets `owner_subject` back to `NULL` on each listed row that
- **`--rollback <file.json> --apply`** sets `owner_subject` back to `NULL` on each listed row that
still carries the subject the backfill wrote for the same owner. Rows changed since are counted and
still carries the subject the backfill wrote for the same owner. Rows changed since are counted and
left alone. Without `--apply` it only reports, which also verifies an applied backfill. Stop the job
left alone. Without `--apply` it only reports, which also verifies an applied backfill. Stop the job
@@ -539,7 +540,7 @@ admin routes' log), with who, the object, the kind and the reason.
@@ -539,7 +540,7 @@ admin routes' log), with who, the object, the kind and the reason.
## Reconciliation
## Reconciliation
```
```
-node scripts/reconcile-objects.js [--verify] [--hash] [--app live] [--json] [--out report.json] [--db ./data/media.db]
+node scripts/reconcile-objects.js [--verify] [--hash] [--app live] [--json] [--out report.json]
```
```
**Modes.**
**Modes.**
@@ -599,7 +600,7 @@ and nothing is concluded about its keys.
@@ -599,7 +600,7 @@ and nothing is concluded about its keys.
restore drill. On demand:
restore drill. On demand:
```
```
-node scripts/vods-orphans-report.js --storage [--no-remote] [--limit 2000] [--out report.json] [--json] [--db ./data/media.db]
+node scripts/vods-orphans-report.js --storage [--no-remote] [--limit 2000] [--out report.json] [--json]
```
```
`--no-remote` lists no bucket. The detailed per-object recommendations for the `vods-orphans/` prefix
`--no-remote` lists no bucket. The detailed per-object recommendations for the `vods-orphans/` prefix
@@ -639,7 +640,7 @@ that only moved to B2/R2 keeps it. Test: `test/content-hash.test.js`.
@@ -639,7 +640,7 @@ that only moved to B2/R2 keeps it. Test: `test/content-hash.test.js`.
- The report, which lists each one for an operator to decide on:
- The report, which lists each one for an operator to decide on:
```
```
- node scripts/no-good-copy-report.js [--app live] [--json] [--out report.json] [--db ./data/media.db]
+ node scripts/no-good-copy-report.js [--app live] [--json] [--out report.json]
```
```
For each object it prints the owner (app, `owner_user_id`, `owner_subject`), `legacy_ref`, the `vods`/`clips`/`files`/`pastes` rows that point at it, its relationships, variants and holds, every location with its state and `verified_at`, and the last verification. The report opens the database read-only. It exits 1 when there are any.
For each object it prints the owner (app, `owner_user_id`, `owner_subject`), `legacy_ref`, the `vods`/`clips`/`files`/`pastes` rows that point at it, its relationships, variants and holds, every location with its state and `verified_at`, and the last verification. The report opens the database read-only. It exits 1 when there are any.
@@ -662,7 +663,7 @@ A *public playback object* is a `ready` vod or clip whose visibility is `public`
@@ -662,7 +663,7 @@ A *public playback object* is a `ready` vod or clip whose visibility is `public`
- **Report.**
- **Report.**
```
```
- node scripts/object-invariant.js [--dry-run] [--app live] [--json] [--db ./data/media.db]
+ node scripts/object-invariant.js [--dry-run] [--app live] [--json]
```
```
Lists every public playback object above target, largest first, records the rows, and resolves rows that no longer apply. `--dry-run` writes nothing.
Lists every public playback object above target, largest first, records the rows, and resolves rows that no longer apply. `--dry-run` writes nothing.
@@ -690,7 +691,7 @@ Code: `server/jobs/`. Tests: `test/jobs.test.js`, `test/jobs-invariant.test.js`,
@@ -690,7 +691,7 @@ Code: `server/jobs/`. Tests: `test/jobs.test.js`, `test/jobs-invariant.test.js`,
30 s, 2 min, 8 min, …, at most 1 h) until `max_attempts`; a handler can mark a failure permanent.
30 s, 2 min, 8 min, …, at most 1 h) until `max_attempts`; a handler can mark a failure permanent.
**Events.** Every state change and its `media.job.<transition>` event (`proposed`, `queued`,
**Events.** Every state change and its `media.job.<transition>` event (`proposed`, `queued`,
-`started`, `retrying`, `succeeded`, `failed`, `cancelled`; subject `job <id>`) commit in one SQLite
+`started`, `retrying`, `succeeded`, `failed`, `cancelled`; subject `job <id>`) commit in one PostgreSQL
transaction through Media's outbox, the same rule as the outcome events (`webhooks.announce()`): no
transaction through Media's outbox, the same rule as the outcome events (`webhooks.announce()`): no
change without its event, no event for a change that rolled back. Progress events are `low`
change without its event, no event for a change that rolled back. Progress events are `low`
priority; outcomes and proposals are `important`. Job events go to OpenVibe.Events only, not to app
priority; outcomes and proposals are `important`. Job events go to OpenVibe.Events only, not to app
@@ -809,7 +810,7 @@ still holds the value that was read, and re-projects the object. Every run write
@@ -809,7 +810,7 @@ still holds the value that was read, and re-projects the object. Every run write
```
```
node scripts/vod-duration-reconcile.js [--app live] [--batch 50] [--after <id>] [--all] [--ids 1,2] [--confirm-remote] # dry run
node scripts/vod-duration-reconcile.js [--app live] [--batch 50] [--after <id>] [--all] [--ids 1,2] [--confirm-remote] # dry run
-node scripts/vod-duration-reconcile.js --apply --backup <file.json> [same selection] # checked DB backup first; file = rollback
+node scripts/vod-duration-reconcile.js --apply --backup <file.json> [same selection] # file = the rollback file (every repair's old values)
node scripts/vod-duration-reconcile.js --rollback <file.json> [--apply]
node scripts/vod-duration-reconcile.js --rollback <file.json> [--apply]
```
```
@@ -134,7 +134,7 @@ const { execFile } = require('child_process');
@@ -134,7 +134,7 @@ const { execFile } = require('child_process');
}
}
const record = { generated_at: new Date().toISOString(), applied: false, objects: report.count, plan: steps.map(({ file, ...s }) => s) };
const record = { generated_at: new Date().toISOString(), applied: false, objects: report.count, plan: steps.map(({ file, ...s }) => s) };
if (!APPLY) {
if (!APPLY) {
- const out = arg('out') ? path.resolve(arg('out')) : path.join(path.dirname(path.resolve(config.db.path)), 'reports', `h15-repair-${record.generated_at.replace(/[:.]/g, '-')}.json`);
+ const out = arg('out') ? path.resolve(arg('out')) : path.join(config.dataDir, 'reports', `h15-repair-${record.generated_at.replace(/[:.]/g, '-')}.json`);
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, JSON.stringify(record, null, 2));
fs.writeFileSync(out, JSON.stringify(record, null, 2));
console.log(`\nDry run: nothing changed. Plan written to ${out}`);
console.log(`\nDry run: nothing changed. Plan written to ${out}`);
deleted file mode 100755
deleted file mode 100755
@@ -1,25 +0,0 @@
@@ -1,25 +0,0 @@
-#!/usr/bin/env node
-'use strict';
-/**
- * The one-time move of Media's SQLite database (DB_PATH) into its PostgreSQL schema (ADR-035; the procedure is
- * openvibe-sdk docs/migrating-to-postgresql.md, section 6, carried out by openvibe-sdk/db runSqliteMigration).
- *
- * node scripts/migrate-to-postgres.js [--sqlite <file>] [--pglite] [--json]
- *
- * Applies migrations/ as the owner (DATABASE_DIRECT_URL), copies every table into emptied tables, verifies row counts
- * and checksums, and exits 1 unless everything verified. The SQLite file is opened read-only. Every table keeps its
- * name and columns.
- */
-require('dotenv').config();
-const { runSqliteMigration } = require('openvibe-sdk/db');
-const config = require('../server/config');
-const { MIGRATIONS } = require('../server/db/database');
-
-const TABLES = {};
-
-if (require.main === module) {
- runSqliteMigration({ service: 'media', sqlite: config.db.path, directUrl: config.db.directUrl, migrations: MIGRATIONS, tables: TABLES })
- .then((code) => process.exit(code), (err) => { console.error(`migrate-to-postgres failed: ${err.message}`); process.exit(1); });
-}
-
-module.exports = { TABLES };
@@ -221,7 +221,7 @@ if (require.main === module) {
@@ -221,7 +221,7 @@ if (require.main === module) {
await db.initDb(); // PostgreSQL (DATABASE_URL), as the service
await db.initDb(); // PostgreSQL (DATABASE_URL), as the service
await storage.tierConfig.init(storage.DEFAULTS, { log: { info() {}, warn: (m) => console.error(`[Tiers] ${m}`), error: (m) => console.error(`[Tiers] ${m}`) } });
await storage.tierConfig.init(storage.DEFAULTS, { log: { info() {}, warn: (m) => console.error(`[Tiers] ${m}`), error: (m) => console.error(`[Tiers] ${m}`) } });
const art = await runDrill(opts, { storage, db, model });
const art = await runDrill(opts, { storage, db, model });
- const out = opts.out || path.join(path.dirname(config.db.path), 'drills', `r2-eviction-${art.vod_id ?? 'none'}-${art.started_at.replace(/[:.]/g, '-')}.json`);
+ const out = opts.out || path.join(config.dataDir, 'drills', `r2-eviction-${art.vod_id ?? 'none'}-${art.started_at.replace(/[:.]/g, '-')}.json`);
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, JSON.stringify(art, null, 2));
fs.writeFileSync(out, JSON.stringify(art, null, 2));
console.log(opts.json ? JSON.stringify(art, null, 2) : `${summarize(art)}\nartifact: ${out}`);
console.log(opts.json ? JSON.stringify(art, null, 2) : `${summarize(art)}\nartifact: ${out}`);
@@ -37,7 +37,7 @@ ffmpeg -y -f lavfi -i "testsrc=duration=5:size=640x360:rate=30" \
@@ -37,7 +37,7 @@ ffmpeg -y -f lavfi -i "testsrc=duration=5:size=640x360:rate=30" \
echo "-- booting server on :$PORT (temp data dir)"
echo "-- booting server on :$PORT (temp data dir)"
SEED="[{\"app_id\":\"live\",\"name\":\"Live\",\"api_key\":\"$LIVE_KEY\",\"allowed_origins\":[\"http://localhost:3000\"]},{\"app_id\":\"games\",\"name\":\"Games\",\"api_key\":\"$GAMES_KEY\",\"quota_bytes\":10485760}]"
SEED="[{\"app_id\":\"live\",\"name\":\"Live\",\"api_key\":\"$LIVE_KEY\",\"allowed_origins\":[\"http://localhost:3000\"]},{\"app_id\":\"games\",\"name\":\"Games\",\"api_key\":\"$GAMES_KEY\",\"quota_bytes\":10485760}]"
env PORT="$PORT" HOST=127.0.0.1 \
env PORT="$PORT" HOST=127.0.0.1 \
- DB_PATH="$WORK/data/media.db" \
+ MEDIA_DATA_DIR="$WORK/data" \
VOD_PATH="$WORK/data/vods" CLIPS_PATH="$WORK/data/clips" \
VOD_PATH="$WORK/data/vods" CLIPS_PATH="$WORK/data/clips" \
PASTES_PATH="$WORK/data/pastes" THUMBNAILS_PATH="$WORK/data/thumbnails" \
PASTES_PATH="$WORK/data/pastes" THUMBNAILS_PATH="$WORK/data/thumbnails" \
FILES_PATH="$WORK/data/files" \
FILES_PATH="$WORK/data/files" \
@@ -70,7 +70,7 @@ const path = require('path');
@@ -70,7 +70,7 @@ const path = require('path');
if (has('storage')) { await storageReport(); await db.close(); process.exit(0); }
if (has('storage')) { await storageReport(); await db.close(); process.exit(0); }
if (!vodStorage.providerConfigured(provider)) usage(`${provider} is not configured (MEDIA_${provider.toUpperCase()}_* not in the environment)`);
if (!vodStorage.providerConfigured(provider)) usage(`${provider} is not configured (MEDIA_${provider.toUpperCase()}_* not in the environment)`);
const report = await orphans.buildReport({ provider, prefix: arg('prefix') || orphans.PREFIX });
const report = await orphans.buildReport({ provider, prefix: arg('prefix') || orphans.PREFIX });
- const file = arg('out') ? path.resolve(arg('out')) : path.join(path.dirname(config.db.path), 'reports', `vods-orphans-${report.generated_at.replace(/[:.]/g, '-')}.json`);
+ const file = arg('out') ? path.resolve(arg('out')) : path.join(config.dataDir, 'reports', `vods-orphans-${report.generated_at.replace(/[:.]/g, '-')}.json`);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify(report, null, 1));
fs.writeFileSync(file, JSON.stringify(report, null, 1));
log(`${report.provider}://${report.bucket}/${report.prefix}: ${report.totals.objects} object(s), ${report.totals.gb} GB (read-only report)`);
log(`${report.provider}://${report.bucket}/${report.prefix}: ${report.totals.objects} object(s), ${report.totals.gb} GB (read-only report)`);
@@ -113,8 +113,9 @@ router.get('/', async (req, res) => {
@@ -113,8 +113,9 @@ router.get('/', async (req, res) => {
];
];
const breakdown = directories.map(d => ({ name: d.name, ...dirStatsRecursive(d.path) }));
const breakdown = directories.map(d => ({ name: d.name, ...dirStatsRecursive(d.path) }));
+ // The store's size (PostgreSQL; the server answers 0 when it does not report one).
let dbBytes = 0;
let dbBytes = 0;
- try { dbBytes = fs.statSync(config.db.path).size; } catch { /* */ }
+ try { dbBytes = Number((await db.get('SELECT pg_database_size(current_database()) AS bytes')).bytes) || 0; } catch { /* */ }
// App-scoped DB stats
// App-scoped DB stats
const vodStats = await db.get(`
const vodStats = await db.get(`
@@ -21,13 +21,13 @@ const config = {
@@ -21,13 +21,13 @@ const config = {
publicUrl: (process.env.MEDIA_PUBLIC_URL || 'https://openvibe.media').replace(/\/$/, ''),
publicUrl: (process.env.MEDIA_PUBLIC_URL || 'https://openvibe.media').replace(/\/$/, ''),
// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner). Without them,
// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner). Without them,
- // development uses an embedded PGlite database in data/pglite. path: the SQLite file of releases before
- // PostgreSQL, read once by scripts/migrate-to-postgres.js.
+ // development uses an embedded PGlite database in data/pglite (MEDIA_PGLITE_DIR overrides it).
db: {
db: {
url: process.env.DATABASE_URL || '',
url: process.env.DATABASE_URL || '',
directUrl: process.env.DATABASE_DIRECT_URL || '',
directUrl: process.env.DATABASE_DIRECT_URL || '',
- path: process.env.DB_PATH || './data/media.db',
},
},
+ // The service's data directory (MEDIA_DATA_DIR): reports and drills are written under it.
+ dataDir: process.env.MEDIA_DATA_DIR || './data',
// Valkey (ADR-035): per-actor limit counters shared across processes; without it they count in this process.
// Valkey (ADR-035): per-actor limit counters shared across processes; without it they count in this process.
valkey: { url: process.env.VALKEY_URL || '', prefix: process.env.VALKEY_PREFIX || 'ov:media:' },
valkey: { url: process.env.VALKEY_URL || '', prefix: process.env.VALKEY_PREFIX || 'ov:media:' },
@@ -155,6 +155,6 @@ config.assets.path = path.resolve(config.assets.path);
@@ -155,6 +155,6 @@ config.assets.path = path.resolve(config.assets.path);
config.thumbnails.path = path.resolve(config.thumbnails.path);
config.thumbnails.path = path.resolve(config.thumbnails.path);
config.files.path = path.resolve(config.files.path);
config.files.path = path.resolve(config.files.path);
config.objects.path = path.resolve(config.objects.path);
config.objects.path = path.resolve(config.objects.path);
-config.db.path = path.resolve(config.db.path);
+config.dataDir = path.resolve(config.dataDir);
module.exports = config;
module.exports = config;
@@ -22,7 +22,8 @@ let database = null;
@@ -22,7 +22,8 @@ let database = null;
* The serving handle (ADR-035): DATABASE_URL through PgBouncer; in development without it, an embedded PGlite database
* The serving handle (ADR-035): DATABASE_URL through PgBouncer; in development without it, an embedded PGlite database
* in data/pglite (MEDIA_PGLITE_DIR overrides it). Migrations run first, as the owner (DATABASE_DIRECT_URL), or on the
* in data/pglite (MEDIA_PGLITE_DIR overrides it). Migrations run first, as the owner (DATABASE_DIRECT_URL), or on the
* embedded handle. The schema, its triggers (retention holds, object deletes, object change events) and the default
* embedded handle. The schema, its triggers (retention holds, object deletes, object change events) and the default
- * settings are migrations/NNNN_*.sql; timestamps stay SQLite's text (ov_now(), datetime() in the migration).
+ * settings are migrations/NNNN_*.sql; timestamps stay text in the `YYYY-MM-DD HH:MM:SS` UTC shape (ov_now(), datetime()
+ * in the migration).
*/
*/
async function openDb(cfg = config, { log = console, registry } = {}) {
async function openDb(cfg = config, { log = console, registry } = {}) {
if (!cfg.db.url) {
if (!cfg.db.url) {
@@ -265,7 +266,7 @@ const LIST_ORDERS = {
@@ -265,7 +266,7 @@ const LIST_ORDERS = {
function _listOrder(order) { return LIST_ORDERS[order] || LIST_ORDERS.newest; }
function _listOrder(order) { return LIST_ORDERS[order] || LIST_ORDERS.newest; }
/**
/**
- * A list's `since` filter as an SQLite datetime ('YYYY-MM-DD HH:MM:SS', UTC), or null. Accepts that
+ * A list's `since` filter as a text datetime ('YYYY-MM-DD HH:MM:SS', UTC), or null. Accepts that
* form or anything Date.parse reads (ISO 8601); anything else is ignored rather than refused.
* form or anything Date.parse reads (ISO 8601); anything else is ignored rather than refused.
*/
*/
function sinceParam(value) {
function sinceParam(value) {
@@ -40,7 +40,7 @@ function parse(value) {
@@ -40,7 +40,7 @@ function parse(value) {
const enabled = parse(process.env.MEDIA_DRILL);
const enabled = parse(process.env.MEDIA_DRILL);
const REPO_ROOT = path.resolve(__dirname, '..');
const REPO_ROOT = path.resolve(__dirname, '..');
-/** Where production runs (OpenVibe.Host inventory: repo /opt/openvibe.media, database data/media.db). */
+/** Where production runs (OpenVibe.Host inventory: repo /opt/openvibe.media, database ov_media). */
const PRODUCTION_ROOT = '/opt/openvibe.media';
const PRODUCTION_ROOT = '/opt/openvibe.media';
const PRODUCTION_PORT = 4100;
const PRODUCTION_PORT = 4100;
// Production's database: ov_media on the host's PostgreSQL (5432) or through PgBouncer (6432).
// Production's database: ov_media on the host's PostgreSQL (5432) or through PgBouncer (6432).
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
* (subject object <id>; Events only; payload object id, tenant,
* (subject object <id>; Events only; payload object id, tenant,
* kind, legacy_ref and the times/visibilities, nothing else)
* kind, legacy_ref and the times/visibilities, nothing else)
*
*
- * The outbox row is written INSIDE the SQLite transaction that makes the state change it describes
+ * The outbox row is written INSIDE the PostgreSQL transaction that makes the state change it describes
* (record(), called from webhooks.announce()): the event exists if and only if the change committed,
* (record(), called from webhooks.announce()): the event exists if and only if the change committed,
* so a crash can neither lose an outcome nor announce one that rolled back. The webhook for the same
* so a crash can neither lose an outcome nor announce one that rolled back. The webhook for the same
* outcome is sent after the commit and carries the envelope's event_id, so a consumer receiving it by
* outcome is sent after the commit and carries the envelope's event_id, so a consumer receiving it by
@@ -10,7 +10,7 @@
@@ -10,7 +10,7 @@
// Restore-drill mode (MEDIA_DRILL=1, `ovhost drill media`): refuse an unsafe environment before the
// Restore-drill mode (MEDIA_DRILL=1, `ovhost drill media`): refuse an unsafe environment before the
// database is opened, then cut every way out of the process but its own HTTP port. The instance
// database is opened, then cut every way out of the process but its own HTTP port. The instance
-// serves reads from a restored copy of media.db, never opens a stored file and starts nothing else
+// serves reads from a restored copy of the database, never opens a stored file and starts nothing else
// (server/drill.js).
// (server/drill.js).
require('dotenv').config();
require('dotenv').config();
const drill = require('./drill');
const drill = require('./drill');
@@ -309,7 +309,7 @@ const ready = (async () => {
@@ -309,7 +309,7 @@ const ready = (async () => {
}
}
if (drill.enabled) { console.log(`[Drill] Ready: http://${config.host}:${config.port} (reads only)`); return; }
if (drill.enabled) { console.log(`[Drill] Ready: http://${config.host}:${config.port} (reads only)`); return; }
console.log(`[Media] OpenVibe.Media listening on ${config.host}:${config.port} (${config.nodeEnv})`);
console.log(`[Media] OpenVibe.Media listening on ${config.host}:${config.port} (${config.nodeEnv})`);
- console.log(`[Media] Data: db=${config.db.path} vods=${config.vod.path} clips=${config.vod.clipsPath}`);
+ console.log(`[Media] Data: db=${config.db.url ? 'postgresql' : 'pglite'} vods=${config.vod.path} clips=${config.vod.clipsPath}`);
console.log(`[Media] RTP ingest pool: udp ${config.rtp.portMin}-${config.rtp.portMax} (127.0.0.1)`);
console.log(`[Media] RTP ingest pool: udp ${config.rtp.portMin}-${config.rtp.portMax} (127.0.0.1)`);
// Durable events (roadmap Wave 3): webhook outcomes also go to OpenVibe.Events. Off without EVENTS_URL.
// Durable events (roadmap Wave 3): webhook outcomes also go to OpenVibe.Events. Off without EVENTS_URL.
try { require('./events').init(); } catch (err) { console.warn('[Events] not started:', err.message); }
try { require('./events').init(); } catch (err) { console.warn('[Events] not started:', err.message); }
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
* running held by the worker (lease_until renewed while it runs; lease_token names the claim)
* running held by the worker (lease_until renewed while it runs; lease_token names the claim)
* succeeded | failed | cancelled finished
* succeeded | failed | cancelled finished
*
*
- * Every state change and its media.job.<transition> event commit in ONE SQLite transaction (the same
+ * Every state change and its media.job.<transition> event commit in ONE PostgreSQL transaction (the same
* rule as webhooks.announce(), see events.js): an event exists if and only if its change committed.
* rule as webhooks.announce(), see events.js): an event exists if and only if its change committed.
* The relay is woken after the commit. Job events go to OpenVibe.Events only (no app webhook).
* The relay is woken after the commit. Job events go to OpenVibe.Events only (no app webhook).
*
*
@@ -133,7 +133,7 @@ function jobPublic(row) {
@@ -133,7 +133,7 @@ function jobPublic(row) {
};
};
}
}
-/** A SQLite UTC time ('YYYY-MM-DD HH:MM:SS') as ISO 8601 UTC ('…T…Z'), or null. */
+/** An ISO 8601 UTC ('…T…Z') time from a `YYYY-MM-DD HH:MM:SS` (UTC) text timestamp, as the migrations store them, or null. */
function isoTime(v) {
function isoTime(v) {
if (v == null || v === '') return null;
if (v == null || v === '') return null;
const s = String(v);
const s = String(v);
@@ -247,7 +247,7 @@ async function enqueue({ appId, type, objectId = null, params = {}, status = 'qu
@@ -247,7 +247,7 @@ async function enqueue({ appId, type, objectId = null, params = {}, status = 'qu
let out = null;
let out = null;
const id = `mjob_${ids.ulid()}`;
const id = `mjob_${ids.ulid()}`;
await db.getDb().tx(async () => {
await db.getDb().tx(async () => {
- // One enqueue per tenant at a time (SQLite's one writer did this): the key, the identical active job and the
+ // One enqueue per tenant at a time (an advisory lock per tenant serialises them): the key, the identical active job and the
// active count below are read with every earlier enqueue of the tenant committed.
// active count below are read with every earlier enqueue of the tenant committed.
await db.get('SELECT pg_advisory_xact_lock(hashtext(?)) AS locked', [`media.jobs:${appId}`]);
await db.get('SELECT pg_advisory_xact_lock(hashtext(?)) AS locked', [`media.jobs:${appId}`]);
if (key) {
if (key) {
@@ -31,7 +31,7 @@ const STATUS_FILTERS = [...model.LIFECYCLES, 'all'];
@@ -31,7 +31,7 @@ const STATUS_FILTERS = [...model.LIFECYCLES, 'all'];
/** Relations whose `from` object was made out of the `to` object (a clip is its own work, not a derivative). */
/** Relations whose `from` object was made out of the `to` object (a clip is its own work, not a derivative). */
const DERIVED_RELATIONS = ['derived_from', 'thumbnail_of'];
const DERIVED_RELATIONS = ['derived_from', 'thumbnail_of'];
-/** SQLite's "YYYY-MM-DD HH:MM:SS" (UTC) or ISO → ISO 8601, or null. */
+/** ISO 8601 from a "YYYY-MM-DD HH:MM:SS" (UTC) text timestamp, as the migrations store them, or from ISO, or null. */
function iso(v) {
function iso(v) {
if (!v) return null;
if (!v) return null;
const s = String(v);
const s = String(v);
@@ -37,7 +37,7 @@ function expectedFields(p) {
@@ -37,7 +37,7 @@ function expectedFields(p) {
};
};
}
}
-// SQLite hands integers back as numbers and TEXT as strings; an app-local user id may be either.
+// The store hands bigint back as a number or a string and TEXT as a string; an app-local user id may be either.
const same = (a, b) => (a == null ? '' : String(a)) === (b == null ? '' : String(b));
const same = (a, b) => (a == null ? '' : String(a)) === (b == null ? '' : String(b));
/** Every row of `table` (optionally one tenant's) in key order, a page at a time. */
/** Every row of `table` (optionally one tenant's) in key order, a page at a time. */
@@ -77,7 +77,7 @@ function plan(item, probes = {}, facts = {}) {
@@ -77,7 +77,7 @@ function plan(item, probes = {}, facts = {}) {
}
}
/**
/**
- * Apply one planned change. h = better-sqlite3 handle. facts: { size, sha256 } of the good bytes (rebaseline,
+ * Apply one planned change. h = database handle. facts: { size, sha256 } of the good bytes (rebaseline,
* regenerate). → the change with old values, or { skipped } when the row moved on.
* regenerate). → the change with old values, or { skipped } when the row moved on.
*/
*/
async function apply(h, step, facts = {}) {
async function apply(h, step, facts = {}) {
@@ -49,7 +49,7 @@ function parseJson(s, fallback) {
@@ -49,7 +49,7 @@ function parseJson(s, fallback) {
try { return JSON.parse(s); } catch { return fallback; }
try { return JSON.parse(s); } catch { return fallback; }
}
}
-/** SQLite 'YYYY-MM-DD HH:MM:SS' (UTC) → epoch ms, so backfilled ids sort by original creation time. */
+/** Epoch ms from a `YYYY-MM-DD HH:MM:SS` (UTC) text timestamp, as the migrations store them, so backfilled ids sort by original creation time. */
function toMs(createdAt) {
function toMs(createdAt) {
const ms = Date.parse(String(createdAt || '').replace(' ', 'T') + (/[zZ]|[+-]\d\d:?\d\d$/.test(String(createdAt || '')) ? '' : 'Z'));
const ms = Date.parse(String(createdAt || '').replace(' ', 'T') + (/[zZ]|[+-]\d\d:?\d\d$/.test(String(createdAt || '')) ? '' : 'Z'));
return Number.isFinite(ms) ? ms : Date.now();
return Number.isFinite(ms) ? ms : Date.now();
@@ -177,7 +177,7 @@ function limitsOf(app, row) {
@@ -177,7 +177,7 @@ function limitsOf(app, row) {
*/
*/
/**
/**
* Inside a transaction, the tenant's quota lock (held to commit): a check and the row it admits commit before the next
* Inside a transaction, the tenant's quota lock (held to commit): a check and the row it admits commit before the next
- * check of the same tenant reads usage, as SQLite's one writer made them (ADR-035).
+ * check of the same tenant reads usage, as the tenant's quota lock makes them (ADR-035).
*/
*/
async function lockQuota(appId) {
async function lockQuota(appId) {
if (db.getDb().inTransaction()) await db.get('SELECT pg_advisory_xact_lock(hashtext(?)) AS locked', [`media.quota:${appId}`]);
if (db.getDb().inTransaction()) await db.get('SELECT pg_advisory_xact_lock(hashtext(?)) AS locked', [`media.quota:${appId}`]);
@@ -128,8 +128,8 @@ function createResolver({
@@ -128,8 +128,8 @@ function createResolver({
// ── Reads ────────────────────────────────────────────────────
// ── Reads ────────────────────────────────────────────────────
/** Per-tenant ownership counts (read-only). */
/** Per-tenant ownership counts (read-only). */
-async function summary(sqlite) {
- const rows = await sqlite.prepare(`
+async function summary(db) {
+ const rows = await db.prepare(`
SELECT app_id, count(*) AS objects,
SELECT app_id, count(*) AS objects,
sum(owner_subject IS NOT NULL) AS already_set,
sum(owner_subject IS NOT NULL) AS already_set,
sum(owner_subject IS NULL AND owner_user_id IS NOT NULL) AS missing,
sum(owner_subject IS NULL AND owner_user_id IS NOT NULL) AS missing,
@@ -141,8 +141,8 @@ async function summary(sqlite) {
@@ -141,8 +141,8 @@ async function summary(sqlite) {
}
}
/** Distinct (owner_app, owner_user_id) pairs that still lack a subject, with their object counts. */
/** Distinct (owner_app, owner_user_id) pairs that still lack a subject, with their object counts. */
-async function pendingOwners(sqlite) {
- return await sqlite.prepare(`
+async function pendingOwners(db) {
+ return await db.prepare(`
SELECT COALESCE(owner_app, app_id) AS owner_app, owner_user_id, count(*) AS objects
SELECT COALESCE(owner_app, app_id) AS owner_app, owner_user_id, count(*) AS objects
FROM media_objects WHERE owner_subject IS NULL AND owner_user_id IS NOT NULL
FROM media_objects WHERE owner_subject IS NULL AND owner_user_id IS NOT NULL
GROUP BY COALESCE(owner_app, app_id), owner_user_id ORDER BY 1, 2`).all();
GROUP BY COALESCE(owner_app, app_id), owner_user_id ORDER BY 1, 2`).all();
@@ -177,8 +177,8 @@ async function resolveOwners(owners, { resolver }) {
@@ -177,8 +177,8 @@ async function resolveOwners(owners, { resolver }) {
* The rows the resolved subjects fill, read in id order `batch` at a time.
* The rows the resolved subjects fill, read in id order `batch` at a time.
* -> [{ id, app_id, owner_app, owner_user_id, owner_subject }]
* -> [{ id, app_id, owner_app, owner_user_id, owner_subject }]
*/
*/
-async function planChanges(sqlite, subjects, { batch = DEFAULT_BATCH } = {}) {
- const page = sqlite.prepare(`
+async function planChanges(db, subjects, { batch = DEFAULT_BATCH } = {}) {
+ const page = db.prepare(`
SELECT id, app_id, COALESCE(owner_app, app_id) AS owner_app, owner_user_id FROM media_objects
SELECT id, app_id, COALESCE(owner_app, app_id) AS owner_app, owner_user_id FROM media_objects
WHERE owner_subject IS NULL AND owner_user_id IS NOT NULL AND id > ? ORDER BY id LIMIT ?`);
WHERE owner_subject IS NULL AND owner_user_id IS NOT NULL AND id > ? ORDER BY id LIMIT ?`);
const changes = [];
const changes = [];
@@ -201,15 +201,15 @@ async function planChanges(sqlite, subjects, { batch = DEFAULT_BATCH } = {}) {
@@ -201,15 +201,15 @@ async function planChanges(sqlite, subjects, { batch = DEFAULT_BATCH } = {}) {
* Fill owner_subject on the planned rows, one transaction per `batch` rows. A row is written only if
* Fill owner_subject on the planned rows, one transaction per `batch` rows. A row is written only if
* it still has no subject and the same owner. -> { applied: [change], skipped: [change], batches }
* it still has no subject and the same owner. -> { applied: [change], skipped: [change], batches }
*/
*/
-async function applyChanges(sqlite, changes, { batch = DEFAULT_BATCH, onBatch = null } = {}) {
- const set = sqlite.prepare(`
+async function applyChanges(db, changes, { batch = DEFAULT_BATCH, onBatch = null } = {}) {
+ const set = db.prepare(`
UPDATE media_objects SET owner_subject = ?, updated_at = ov_now()
UPDATE media_objects SET owner_subject = ?, updated_at = ov_now()
WHERE id = ? AND owner_subject IS NULL AND COALESCE(owner_app, app_id) = ? AND owner_user_id = ?`);
WHERE id = ? AND owner_subject IS NULL AND COALESCE(owner_app, app_id) = ? AND owner_user_id = ?`);
const applied = [], skipped = [];
const applied = [], skipped = [];
let batches = 0;
let batches = 0;
for (let i = 0; i < changes.length; i += batch) {
for (let i = 0; i < changes.length; i += batch) {
const slice = changes.slice(i, i + batch);
const slice = changes.slice(i, i + batch);
- await sqlite.tx(async () => {
+ await db.tx(async () => {
for (const c of slice) ((await set.run(c.owner_subject, c.id, c.owner_app, c.owner_user_id)).changes ? applied : skipped).push(c);
for (const c of slice) ((await set.run(c.owner_subject, c.id, c.owner_app, c.owner_user_id)).changes ? applied : skipped).push(c);
});
});
batches++;
batches++;
@@ -224,16 +224,16 @@ async function applyChanges(sqlite, changes, { batch = DEFAULT_BATCH, onBatch =
@@ -224,16 +224,16 @@ async function applyChanges(sqlite, changes, { batch = DEFAULT_BATCH, onBatch =
* alone and counted. With apply=false nothing is written.
* alone and counted. With apply=false nothing is written.
* -> { restored, changed_since, missing, rows: { restored: [id], changed_since: [id], missing: [id] } }
* -> { restored, changed_since, missing, rows: { restored: [id], changed_since: [id], missing: [id] } }
*/
*/
-async function rollbackChanges(sqlite, entries, { apply = false, batch = DEFAULT_BATCH } = {}) {
- const get = sqlite.prepare('SELECT id, owner_subject, COALESCE(owner_app, app_id) AS owner_app, owner_user_id FROM media_objects WHERE id = ?');
- const clear = sqlite.prepare(`
+async function rollbackChanges(db, entries, { apply = false, batch = DEFAULT_BATCH } = {}) {
+ const get = db.prepare('SELECT id, owner_subject, COALESCE(owner_app, app_id) AS owner_app, owner_user_id FROM media_objects WHERE id = ?');
+ const clear = db.prepare(`
UPDATE media_objects SET owner_subject = NULL, updated_at = ov_now()
UPDATE media_objects SET owner_subject = NULL, updated_at = ov_now()
WHERE id = ? AND owner_subject = ? AND COALESCE(owner_app, app_id) = ? AND owner_user_id = ?`);
WHERE id = ? AND owner_subject = ? AND COALESCE(owner_app, app_id) = ? AND owner_user_id = ?`);
const rows = { restored: [], changed_since: [], missing: [] };
const rows = { restored: [], changed_since: [], missing: [] };
const unchanged = (row, e) => row.owner_subject === e.owner_subject && row.owner_app === e.owner_app && Number(row.owner_user_id) === Number(e.owner_user_id);
const unchanged = (row, e) => row.owner_subject === e.owner_subject && row.owner_app === e.owner_app && Number(row.owner_user_id) === Number(e.owner_user_id);
for (let i = 0; i < entries.length; i += batch) {
for (let i = 0; i < entries.length; i += batch) {
const slice = entries.slice(i, i + batch);
const slice = entries.slice(i, i + batch);
- await sqlite.tx(async () => {
+ await db.tx(async () => {
for (const e of slice) {
for (const e of slice) {
const row = await get.get(e.id);
const row = await get.get(e.id);
if (!row) { rows.missing.push(e.id); continue; }
if (!row) { rows.missing.push(e.id); continue; }
@@ -250,8 +250,8 @@ async function rollbackChanges(sqlite, entries, { apply = false, batch = DEFAULT
@@ -250,8 +250,8 @@ async function rollbackChanges(sqlite, entries, { apply = false, batch = DEFAULT
* One reconcile pass (the service's job): resolve every owner still lacking a subject and fill their
* One reconcile pass (the service's job): resolve every owner still lacking a subject and fill their
* objects. -> { owners, filled, skipped, unresolvable, unsupported, via }
* objects. -> { owners, filled, skipped, unresolvable, unsupported, via }
*/
*/
-async function reconcileOnce(sqlite, { resolver, batch = DEFAULT_BATCH } = {}) {
- const owners = await pendingOwners(sqlite);
+async function reconcileOnce(db, { resolver, batch = DEFAULT_BATCH } = {}) {
+ const owners = await pendingOwners(db);
const out = { owners: owners.length, filled: 0, skipped: 0, unresolvable: 0, unsupported: 0, via: null };
const out = { owners: owners.length, filled: 0, skipped: 0, unresolvable: 0, unsupported: 0, via: null };
if (!owners.length) return out;
if (!owners.length) return out;
const callsBefore = resolver.state ? resolver.state.calls : 0;
const callsBefore = resolver.state ? resolver.state.calls : 0;
@@ -260,7 +260,7 @@ async function reconcileOnce(sqlite, { resolver, batch = DEFAULT_BATCH } = {}) {
@@ -260,7 +260,7 @@ async function reconcileOnce(sqlite, { resolver, batch = DEFAULT_BATCH } = {}) {
out.unsupported = r.unsupported.reduce((n, o) => n + o.objects, 0);
out.unsupported = r.unsupported.reduce((n, o) => n + o.objects, 0);
out.via = resolver.state && resolver.state.calls > callsBefore ? resolver.state.via : null;
out.via = resolver.state && resolver.state.calls > callsBefore ? resolver.state.via : null;
if (!r.subjects.size) return out;
if (!r.subjects.size) return out;
- const applied = await applyChanges(sqlite, await planChanges(sqlite, r.subjects, { batch }), { batch });
+ const applied = await applyChanges(db, await planChanges(db, r.subjects, { batch }), { batch });
out.filled = applied.applied.length;
out.filled = applied.applied.length;
out.skipped = applied.skipped.length;
out.skipped = applied.skipped.length;
return out;
return out;
@@ -56,7 +56,7 @@ function sha256File(p) {
@@ -56,7 +56,7 @@ function sha256File(p) {
});
});
}
}
-/** SQLite 'YYYY-MM-DD HH:MM:SS' (UTC) or ISO → epoch ms (NaN when unreadable). */
+/** Epoch ms from a `YYYY-MM-DD HH:MM:SS` (UTC) text timestamp, as the migrations store them, or ISO (NaN when unreadable). */
function msOf(t) {
function msOf(t) {
const s = String(t || '');
const s = String(t || '');
return Date.parse(/[zZ]|[+-]\d\d:?\d\d$/.test(s) ? s : `${s.replace(' ', 'T')}Z`);
return Date.parse(/[zZ]|[+-]\d\d:?\d\d$/.test(s) ? s : `${s.replace(' ', 'T')}Z`);
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
* GET /metrics Prometheus text; direct loopback callers only (404 through nginx)
* GET /metrics Prometheus text; direct loopback callers only (404 through nginx)
* GET /api/ready named checks; 503 only when a required one fails, optional failures → degraded
* GET /api/ready named checks; 503 only when a required one fails, optional failures → degraded
*
*
- * Required (Media cannot serve without them): the SQLite database answers a real query, and every
+ * Required (Media cannot serve without them): the PostgreSQL database answers a real query, and every
* local storage directory accepts a write. Optional (one capability degrades, the rest keeps
* local storage directory accepts a write. Optional (one capability degrades, the rest keeps
* working): the Network public key (user-JWT and service-token routes; app-key tenants are
* working): the Network public key (user-JWT and service-token routes; app-key tenants are
* unaffected), each CONFIGURED remote tier (B2 cold storage, R2 cache; a HeadBucket at most once a
* unaffected), each CONFIGURED remote tier (B2 cold storage, R2 cache; a HeadBucket at most once a
@@ -39,7 +39,7 @@ const snip = (s, n = 200) => { const t = String(s || '').replace(/\s+/g, ' ').tr
@@ -39,7 +39,7 @@ const snip = (s, n = 200) => { const t = String(s || '').replace(/\s+/g, ' ').tr
/** JSON that is safe inside a <script>: no `</` can end the element early. */
/** JSON that is safe inside a <script>: no `</` can end the element early. */
const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c');
const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c');
-/** SQLite's "YYYY-MM-DD HH:MM:SS" (UTC) → ISO 8601, or null. */
+/** ISO 8601 from a "YYYY-MM-DD HH:MM:SS" (UTC) text timestamp, as the migrations store them, or null. */
function isoDate(dt) {
function isoDate(dt) {
if (!dt) return null;
if (!dt) return null;
const d = new Date(String(dt).includes('T') ? dt : `${dt}Z`.replace(' ', 'T'));
const d = new Date(String(dt).includes('T') ? dt : `${dt}Z`.replace(' ', 'T'));
@@ -108,7 +108,7 @@ async function measure(vod, { confirm = false, confirmRemote = false, probeTimeo
@@ -108,7 +108,7 @@ async function measure(vod, { confirm = false, confirmRemote = false, probeTimeo
}
}
function reportsDir() {
function reportsDir() {
- return path.join(path.dirname(config.db.path), 'reports');
+ return path.join(config.dataDir, 'reports');
}
}
function writeReport(report, file = null) {
function writeReport(report, file = null) {
@@ -438,7 +438,7 @@ async function buildStorageReport({ providers = null, list = null, listUploads =
@@ -438,7 +438,7 @@ async function buildStorageReport({ providers = null, list = null, listUploads =
}
}
function reportsDir() {
function reportsDir() {
- return path.join(path.dirname(config.db.path), 'reports');
+ return path.join(config.dataDir, 'reports');
}
}
/** Write a storage report as JSON (default <data>/reports/storage-orphans-<time>.json). Returns the path. */
/** Write a storage report as JSON (default <data>/reports/storage-orphans-<time>.json). Returns the path. */
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
* storage.alert | storage.recovered
* storage.alert | storage.recovered
*
*
* Outcomes go through announce(): the state change and its durable OpenVibe.Events outbox row
* Outcomes go through announce(): the state change and its durable OpenVibe.Events outbox row
- * commit in one SQLite transaction, then the webhook is sent with `event_id` = that event's id
+ * commit in one PostgreSQL transaction, then the webhook is sent with `event_id` = that event's id
* (absent when the outbox is off), so a consumer that also reads Events dedupes the pair.
* (absent when the outbox is off), so a consumer that also reads Events dedupes the pair.
*/
*/
'use strict';
'use strict';
@@ -79,7 +79,7 @@ async function sendWebhook(appOrId, event, data, { eventId = null } = {}) {
@@ -79,7 +79,7 @@ async function sendWebhook(appOrId, event, data, { eventId = null } = {}) {
/**
/**
* Commit an outcome and announce it. `change()` (synchronous DB writes, optional) and the outbox
* Commit an outcome and announce it. `change()` (synchronous DB writes, optional) and the outbox
- * row for `event` run in ONE SQLite transaction, so the durable event exists if and only if the
+ * row for `event` run in ONE PostgreSQL transaction, so the durable event exists if and only if the
* change committed: never lost after a commit, never announced for a rollback. `payload()` is read
* change committed: never lost after a commit, never announced for a rollback. `payload()` is read
* inside that transaction (the row as committed). After the commit the relay is woken and the
* inside that transaction (the row as committed). After the commit the relay is woken and the
* app's webhook is sent with the same event_id. Returns { data, eventId }; throws (nothing changed,
* app's webhook is sent with the same event_id. Returns { data, eventId }; throws (nothing changed,
@@ -13,7 +13,6 @@ const path = require('path');
@@ -13,7 +13,6 @@ const path = require('path');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-account-data-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-account-data-'));
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.MEDIA_INBOUND_EVENTS_SECRET = 'm'.repeat(40);
process.env.MEDIA_INBOUND_EVENTS_SECRET = 'm'.repeat(40);
const log = console.log;
const log = console.log;
console.log = (...a) => { if (!/^\[/.test(String(a[0]))) log(...a); };
console.log = (...a) => { if (!/^\[/.test(String(a[0]))) log(...a); };
@@ -13,7 +13,6 @@ const crypto = require('crypto');
@@ -13,7 +13,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-limits-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-limits-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
process.env.OBJECTS_PATH = dir('objects');
process.env.OBJECTS_PATH = dir('objects');
@@ -16,7 +16,6 @@ const crypto = require('crypto');
@@ -16,7 +16,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-tenantabuse-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-tenantabuse-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
process.env.OBJECTS_PATH = dir('objects');
process.env.OBJECTS_PATH = dir('objects');
@@ -14,7 +14,6 @@ const crypto = require('crypto');
@@ -14,7 +14,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-apptenants-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-apptenants-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
process.env.OBJECTS_PATH = dir('objects');
process.env.OBJECTS_PATH = dir('objects');
@@ -12,7 +12,6 @@ const { spawnSync } = require('child_process');
@@ -12,7 +12,6 @@ const { spawnSync } = require('child_process');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-clipjob-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-clipjob-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -16,7 +16,6 @@ const http = require('http');
@@ -16,7 +16,6 @@ const http = require('http');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-clip-origin-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-clip-origin-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -14,7 +14,7 @@ const crypto = require('crypto');
@@ -14,7 +14,7 @@ const crypto = require('crypto');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-hash-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-hash-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
MEDIA_INVARIANT_SCAN_HOURS: '0', MEDIA_HASH_SETTLE_S: '60', MEDIA_HASH_INTERVAL_MIN: '15',
MEDIA_INVARIANT_SCAN_HOURS: '0', MEDIA_HASH_SETTLE_S: '60', MEDIA_HASH_INTERVAL_MIN: '15',
});
});
@@ -13,7 +13,7 @@ const express = require('express');
@@ -13,7 +13,7 @@ const express = require('express');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-crawl-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-crawl-'));
- Object.assign(process.env, { DB_PATH: path.join(tmp, 'media.db'), MEDIA_PUBLIC_URL: 'https://media.test', VOD_PATH: path.join(tmp, 'vods'), CLIPS_PATH: path.join(tmp, 'clips') });
+ Object.assign(process.env, { MEDIA_PUBLIC_URL: 'https://media.test', VOD_PATH: path.join(tmp, 'vods'), CLIPS_PATH: path.join(tmp, 'clips') });
const db = require('../server/db/database');
const db = require('../server/db/database');
const pages = require('../server/public/pages');
const pages = require('../server/public/pages');
@@ -12,7 +12,6 @@ const path = require('path');
@@ -12,7 +12,6 @@ const path = require('path');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-dbopen-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-dbopen-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
for (const [k, n] of [['VOD_PATH', 'vods'], ['CLIPS_PATH', 'clips'], ['FILES_PATH', 'files'], ['THUMBNAILS_PATH', 'thumbnails'], ['PASTES_PATH', 'pastes'], ['OBJECTS_PATH', 'objects']]) process.env[k] = dir(n);
for (const [k, n] of [['VOD_PATH', 'vods'], ['CLIPS_PATH', 'clips'], ['FILES_PATH', 'files'], ['THUMBNAILS_PATH', 'thumbnails'], ['PASTES_PATH', 'pastes'], ['OBJECTS_PATH', 'objects']]) process.env[k] = dir(n);
process.env.MEDIA_PUBLIC_URL = 'https://media.test';
process.env.MEDIA_PUBLIC_URL = 'https://media.test';
console.log = () => {};
console.log = () => {};
@@ -61,7 +61,7 @@ const { spawnSync, execFile } = require('child_process');
@@ -61,7 +61,7 @@ const { spawnSync, execFile } = require('child_process');
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
const endpoint = `http://127.0.0.1:${s3.address().port}`;
const endpoint = `http://127.0.0.1:${s3.address().port}`;
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'data', 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ MEDIA_DATA_DIR: path.join(tmp, 'data'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_INVARIANT_SCAN_HOURS: '0', MEDIA_JOBS_ENABLED: '0',
MEDIA_INVARIANT_SCAN_HOURS: '0', MEDIA_JOBS_ENABLED: '0',
@@ -146,7 +146,7 @@ const { spawnSync, execFile } = require('child_process');
@@ -146,7 +146,7 @@ const { spawnSync, execFile } = require('child_process');
assert.strictEqual(j.status, 'succeeded', j.error);
assert.strictEqual(j.status, 'succeeded', j.error);
let res = JSON.parse(j.result);
let res = JSON.parse(j.result);
assert.deepStrictEqual([res.mode, res.counts.checked, res.counts.repaired, res.next_after_id], ['apply', 4, 3, ids.missing]);
assert.deepStrictEqual([res.mode, res.counts.checked, res.counts.repaired, res.next_after_id], ['apply', 4, 3, ids.missing]);
- assert.ok(fs.existsSync(path.join(path.dirname(process.env.DB_PATH), 'reports', res.report)), 'the report is written');
+ assert.ok(fs.existsSync(path.join(process.env.MEDIA_DATA_DIR, 'reports', res.report)), 'the report is written');
assert.deepStrictEqual([(await vod(ids.wallClock)).duration_seconds, (await vod(ids.wallClock)).duration_source], [6, 'probe']);
assert.deepStrictEqual([(await vod(ids.wallClock)).duration_seconds, (await vod(ids.wallClock)).duration_source], [6, 'probe']);
assert.deepStrictEqual([(await vod(ids.remote)).duration_seconds, (await vod(ids.remote)).duration_source], [8, 'probe']);
assert.deepStrictEqual([(await vod(ids.remote)).duration_seconds, (await vod(ids.remote)).duration_source], [8, 'probe']);
assert.deepStrictEqual([(await vod(ids.missing)).duration_seconds, (await vod(ids.missing)).duration_source], [6, 'probe']);
assert.deepStrictEqual([(await vod(ids.missing)).duration_seconds, (await vod(ids.missing)).duration_source], [6, 'probe']);
@@ -156,7 +156,7 @@ const { spawnSync, execFile } = require('child_process');
@@ -156,7 +156,7 @@ const { spawnSync, execFile } = require('child_process');
j = (await queue.enqueue({ appId: 'live', type: 'vod.duration.reconcile', params: spec.validate({ obj: null, params: { limit: 4, apply: true } }), createdBy: 'test2' })).job;
j = (await queue.enqueue({ appId: 'live', type: 'vod.duration.reconcile', params: spec.validate({ obj: null, params: { limit: 4, apply: true } }), createdBy: 'test2' })).job;
res = JSON.parse((await worker.runNow(j.id)).result);
res = JSON.parse((await worker.runNow(j.id)).result);
assert.deepStrictEqual([res.range.after_id, res.counts.checked, res.range.done, res.next_after_id], [ids.missing, 3, true, 0], 'continues, reaches the end, wraps');
assert.deepStrictEqual([res.range.after_id, res.counts.checked, res.range.done, res.next_after_id], [ids.missing, 3, true, 0], 'continues, reaches the end, wraps');
- const report = JSON.parse(fs.readFileSync(path.join(path.dirname(process.env.DB_PATH), 'reports', JSON.parse((await queue.list('live', { type: 'vod.duration.reconcile' })).jobs.slice(-1)[0].result).report), 'utf8'));
+ const report = JSON.parse(fs.readFileSync(path.join(process.env.MEDIA_DATA_DIR, 'reports', JSON.parse((await queue.list('live', { type: 'vod.duration.reconcile' })).jobs.slice(-1)[0].result).report), 'utf8'));
const rb0 = await reconcile.rollback(report.rows);
const rb0 = await reconcile.rollback(report.rows);
assert.deepStrictEqual(rb0, { restored: 0, would_restore: 3, changed_since: 0 });
assert.deepStrictEqual(rb0, { restored: 0, would_restore: 3, changed_since: 0 });
const rb = await reconcile.rollback(report.rows, { apply: true });
const rb = await reconcile.rollback(report.rows, { apply: true });
@@ -11,7 +11,6 @@ const http = require('http');
@@ -11,7 +11,6 @@ const http = require('http');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-events-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-events-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -15,7 +15,6 @@ const crypto = require('crypto');
@@ -15,7 +15,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-export-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-export-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
process.env.OBJECTS_PATH = dir('objects');
process.env.OBJECTS_PATH = dir('objects');
@@ -10,8 +10,8 @@ const require = createRequire(import.meta.url);
@@ -10,8 +10,8 @@ const require = createRequire(import.meta.url);
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
const { createTestDb } = require('openvibe-sdk/testing');
const { createTestDb } = require('openvibe-sdk/testing');
const t = await createTestDb({ migrations: path.join(root, 'migrations'), store: process.env.MEDIA_TEST_STORE || 'pglite', service: 'media', max: 4 });
const t = await createTestDb({ migrations: path.join(root, 'migrations'), store: process.env.MEDIA_TEST_STORE || 'pglite', service: 'media', max: 4 });
-// Tests insert rows with small explicit ids (SQLite's AUTOINCREMENT then continued after them; a PostgreSQL identity
-// does not): generated ids start at 100000 here, clear of every id a test names.
+// Tests insert rows with small explicit ids. A PostgreSQL identity would continue after them, so generated ids
+// are set to start at 100000 here, clear of every id a test names.
for (const r of await t.db.prepare(`SELECT pg_get_serial_sequence(quote_ident(table_name), column_name) AS seq FROM information_schema.columns
for (const r of await t.db.prepare(`SELECT pg_get_serial_sequence(quote_ident(table_name), column_name) AS seq FROM information_schema.columns
WHERE table_schema = current_schema() AND is_identity = 'YES'`).all()) {
WHERE table_schema = current_schema() AND is_identity = 'YES'`).all()) {
if (r.seq) await t.db.prepare('SELECT setval(?::regclass, 100000)').get(r.seq);
if (r.seq) await t.db.prepare('SELECT setval(?::regclass, 100000)').get(r.seq);
@@ -13,7 +13,6 @@ const path = require('path');
@@ -13,7 +13,6 @@ const path = require('path');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-fencing-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-fencing-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -15,7 +15,6 @@ const { spawnSync } = require('child_process');
@@ -15,7 +15,6 @@ const { spawnSync } = require('child_process');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-jobinv-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-jobinv-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -13,7 +13,6 @@ const { spawnSync } = require('child_process');
@@ -13,7 +13,6 @@ const { spawnSync } = require('child_process');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-previews-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-previews-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -15,7 +15,6 @@ const http = require('http');
@@ -15,7 +15,6 @@ const http = require('http');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-jobs-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-jobs-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -238,7 +237,7 @@ const http = require('http');
@@ -238,7 +237,7 @@ const http = require('http');
assert.deepStrictEqual(r.body.jobs.map((j) => j.id), [scanId]);
assert.deepStrictEqual(r.body.jobs.map((j) => j.id), [scanId]);
r = await call('GET', `/api/v2/live/jobs/${scanId}`);
r = await call('GET', `/api/v2/live/jobs/${scanId}`);
assert.deepStrictEqual([r.status, r.body.job.idempotency_key, r.body.job.params, r.body.job.created_by, r.body.job.created_at.includes('T')], [200, 'scan-1', {}, 'app:live', false],
assert.deepStrictEqual([r.status, r.body.job.idempotency_key, r.body.job.params, r.body.job.created_by, r.body.job.created_at.includes('T')], [200, 'scan-1', {}, 'app:live', false],
- 'GET the job answers as before (queue.jobPublic: params, idempotency key, creator, SQLite times)');
+ "GET the job answers as before (queue.jobPublic: params, idempotency key, creator, the store's `YYYY-MM-DD HH:MM:SS` UTC text timestamps)");
r = await call('GET', `/api/v2/games/jobs/${scanId}`, { key: 'games-key' });
r = await call('GET', `/api/v2/games/jobs/${scanId}`, { key: 'games-key' });
assert.strictEqual(r.status, 404, 'tenants are isolated');
assert.strictEqual(r.status, 404, 'tenants are isolated');
r = await call('GET', `/api/v2/live/jobs?status=bogus`);
r = await call('GET', `/api/v2/live/jobs?status=bogus`);
@@ -21,7 +21,7 @@ const http = require('http');
@@ -21,7 +21,7 @@ const http = require('http');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-me-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-me-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),
OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',
});
});
@@ -12,7 +12,7 @@ const http = require('http');
@@ -12,7 +12,7 @@ const http = require('http');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-modevents-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-modevents-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
});
});
const stub = http.createServer((req, res) => { req.resume(); req.on('end', () => { res.setHeader('Content-Type', 'application/json'); res.statusCode = 404; res.end('{}'); }); });
const stub = http.createServer((req, res) => { req.resume(); req.on('end', () => { res.setHeader('Content-Type', 'application/json'); res.statusCode = 404; res.end('{}'); }); });
@@ -77,11 +77,11 @@ async function check(name, fn) {
@@ -77,11 +77,11 @@ async function check(name, fn) {
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
fs.mkdirSync(dataDir, { recursive: true });
fs.mkdirSync(dataDir, { recursive: true });
let server = null;
let server = null;
- // This release on PostgreSQL (ADR-035) and N-1 on SQLite: they never share a database (the switch stops the
- // service, imports, then starts this release), so N-1's SQL has no schema of this release to run on. The client
- // half still runs, on a freshly migrated and seeded database. Both on PostgreSQL: N keeps every migration N-1 ran.
+ // This release on PostgreSQL (ADR-035) and a recorded N-1 that still ran on SQLite: they never share a database,
+ // so N-1's SQL has no schema of this release to run on. The client half still runs, on a freshly migrated and
+ // seeded database. Once the N-1 fixture is recorded from a PostgreSQL release: N keeps every migration N-1 ran.
const pg = fs.existsSync(path.join(ROOT, 'migrations'));
const pg = fs.existsSync(path.join(ROOT, 'migrations'));
- const sqlSkip = pg && worker.engine !== 'postgresql' ? 'N-1 SQL: skipped (N-1 runs on SQLite and this release on PostgreSQL: no shared database, so no N-1 statement runs on this schema; scripts/migrate-to-postgres.js moves the data and was rehearsed)' : null;
+ const sqlSkip = pg && worker.engine !== 'postgresql' ? 'N-1 SQL: skipped (the recorded N-1 release runs on SQLite and this release on PostgreSQL: no shared database, so no N-1 statement runs on this schema)' : null;
if (pg && worker.engine === 'postgresql') {
if (pg && worker.engine === 'postgresql') {
await check(`N-1's ${worker.migrations.length} migration file(s) are all here, unchanged`, () => {
await check(`N-1's ${worker.migrations.length} migration file(s) are all here, unchanged`, () => {
const dir = path.join(ROOT, 'migrations');
const dir = path.join(ROOT, 'migrations');
@@ -18,7 +18,6 @@ const crypto = require('crypto');
@@ -18,7 +18,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-namespaces-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-namespaces-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
process.env.OBJECTS_PATH = dir('objects');
process.env.OBJECTS_PATH = dir('objects');
@@ -51,8 +50,8 @@ const crypto = require('crypto');
@@ -51,8 +50,8 @@ const crypto = require('crypto');
iss: 'https://openvibe.network', sub: `app:${APP_A}`, actor_type: 'app', aud: ['openvibe.media'], cap, ns, project_id: project, env, iat: now, exp: now + 300, jti: jti(),
iss: 'https://openvibe.network', sub: `app:${APP_A}`, actor_type: 'app', aud: ['openvibe.media'], cap, ns, project_id: project, env, iat: now, exp: now + 300, jti: jti(),
}, keys.privateKey);
}, keys.privateKey);
- // Tenants: Live, and developer project PA in production and in its sandbox. (The SQLite release moved older rows
- // into namespaces at boot; on PostgreSQL the schema starts with them, and writes keep them.)
+ // Tenants: Live, and developer project PA in production and in its sandbox. (On PostgreSQL the schema starts
+ // with the namespaces in place, and writes keep them.)
await db.upsertApp({ app_id: 'live', api_key: 'live-key' });
await db.upsertApp({ app_id: 'live', api_key: 'live-key' });
await db.ensureProjectTenant(PA, 'production', 1000000);
await db.ensureProjectTenant(PA, 'production', 1000000);
await db.ensureProjectTenant(PA, 'sandbox', 1000000);
await db.ensureProjectTenant(PA, 'sandbox', 1000000);
@@ -14,7 +14,7 @@ const http = require('http');
@@ -14,7 +14,7 @@ const http = require('http');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objevents-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objevents-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
});
});
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
'use strict';
'use strict';
// Object-first writes (WS-G task 1, retiring compatibility shim C-75): every write to an inherited
// Object-first writes (WS-G task 1, retiring compatibility shim C-75): every write to an inherited
-// vods/clips/files/pastes row makes or updates its media_object in the SAME SQLite transaction
+// vods/clips/files/pastes row makes or updates its media_object in the SAME PostgreSQL transaction
// (objects/model.js withObject). For each main write path the object is there, linked and agreeing
// (objects/model.js withObject). For each main write path the object is there, linked and agreeing
// with the row, as soon as the write returns; when the object cannot be written, the row write
// with the row, as soon as the write returns; when the object cannot be written, the row write
// rolls back with it (neither row); and the drift report (server/objects/drift.js,
// rolls back with it (neither row); and the drift report (server/objects/drift.js,
@@ -18,7 +18,7 @@ const sharp = require('sharp');
@@ -18,7 +18,7 @@ const sharp = require('sharp');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-object-first-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-object-first-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_PUBLIC_URL: 'https://media.test',
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_PUBLIC_URL: 'https://media.test',
});
});
@@ -69,7 +69,7 @@ const crypto = require('crypto');
@@ -69,7 +69,7 @@ const crypto = require('crypto');
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
const endpoint = `http://127.0.0.1:${s3.address().port}`;
const endpoint = `http://127.0.0.1:${s3.address().port}`;
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',
MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',
@@ -13,7 +13,6 @@ const crypto = require('crypto');
@@ -13,7 +13,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objapi-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objapi-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
process.env.OBJECTS_PATH = dir('objects');
process.env.OBJECTS_PATH = dir('objects');
@@ -11,7 +11,6 @@ const path = require('path');
@@ -11,7 +11,6 @@ const path = require('path');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objects-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-objects-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -13,7 +13,6 @@ const crypto = require('crypto');
@@ -13,7 +13,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-multipart-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-multipart-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
process.env.OBJECTS_PATH = dir('objects');
process.env.OBJECTS_PATH = dir('objects');
@@ -13,7 +13,7 @@ const crypto = require('crypto');
@@ -13,7 +13,7 @@ const crypto = require('crypto');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-reconcile-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-reconcile-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const env = {
const env = {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_PUBLIC_URL: 'https://media.test',
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_PUBLIC_URL: 'https://media.test',
MEDIA_PUBLIC_OBJECT_TARGET_MB: '1', MEDIA_PUBLIC_OBJECT_WARN_MB: '2', MEDIA_PUBLIC_OBJECT_MAX_MB: '3',
MEDIA_PUBLIC_OBJECT_TARGET_MB: '1', MEDIA_PUBLIC_OBJECT_WARN_MB: '2', MEDIA_PUBLIC_OBJECT_MAX_MB: '3',
};
};
@@ -15,7 +15,7 @@ const crypto = require('crypto');
@@ -15,7 +15,7 @@ const crypto = require('crypto');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-verify-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-verify-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const env = {
const env = {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9',
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9',
};
};
@@ -14,7 +14,7 @@ const { spawn } = require('child_process');
@@ -14,7 +14,7 @@ const { spawn } = require('child_process');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-obs-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-obs-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const ENV = {
const ENV = {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), PASTES_PATH: dir('pastes'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), PASTES_PATH: dir('pastes'),
THUMBNAILS_PATH: dir('thumbnails'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
THUMBNAILS_PATH: dir('thumbnails'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9', RELEASE_COMMIT: 'abcdef1234567',
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9', RELEASE_COMMIT: 'abcdef1234567',
};
};
@@ -132,13 +132,13 @@ const { spawn } = require('child_process');
@@ -132,13 +132,13 @@ const { spawn } = require('child_process');
// ── The real index.js boots with both routes ──
// ── The real index.js boots with both routes ──
const port = await new Promise((resolve) => { const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); });
const port = await new Promise((resolve) => { const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); });
const bootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-obs-boot-'));
const bootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-obs-boot-'));
- const env = { ...process.env, ...ENV, DB_PATH: path.join(bootDir, 'media.db'), MEDIA_PGLITE_DIR: path.join(bootDir, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test' };
+ const env = { ...process.env, ...ENV, MEDIA_PGLITE_DIR: path.join(bootDir, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test' };
for (const k of ['VOD_PATH', 'CLIPS_PATH', 'PASTES_PATH', 'THUMBNAILS_PATH', 'FILES_PATH', 'OBJECTS_PATH', 'ASSETS_PATH']) env[k] = path.join(bootDir, k.toLowerCase());
for (const k of ['VOD_PATH', 'CLIPS_PATH', 'PASTES_PATH', 'THUMBNAILS_PATH', 'FILES_PATH', 'OBJECTS_PATH', 'ASSETS_PATH']) env[k] = path.join(bootDir, k.toLowerCase());
const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });
const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });
let log = ''; child.stdout.on('data', (d) => { log += d; }); child.stderr.on('data', (d) => { log += d; });
let log = ''; child.stdout.on('data', (d) => { log += d; }); child.stderr.on('data', (d) => { log += d; });
const b2 = `http://127.0.0.1:${port}`;
const b2 = `http://127.0.0.1:${port}`;
let up = null;
let up = null;
- for (let i = 0; i < 100 && !up; i++) {
+ for (let i = 0; i < 300 && !up; i++) { // 30 s: a cold PGlite boot plus migrations is slow on a loaded machine
await new Promise((res) => setTimeout(res, 100));
await new Promise((res) => setTimeout(res, 100));
try { up = await request(b2, '/api/ready'); } catch { /* not listening yet */ }
try { up = await request(b2, '/api/ready'); } catch { /* not listening yet */ }
}
}
@@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
'use strict';
'use strict';
-// Roadmap Wave 3 (audit item 7): Media's outbox row is written in the SAME SQLite transaction as
+// Roadmap Wave 3 (audit item 7): Media's outbox row is written in the SAME PostgreSQL transaction as
// the state change it describes. An outcome is never lost (the change committed, the event did
// the state change it describes. An outcome is never lost (the change committed, the event did
// not) and never a phantom (the event exists, the change rolled back). The webhook for the same
// not) and never a phantom (the event exists, the change rolled back). The webhook for the same
// outcome follows the commit and carries the event's id, so a consumer reading both paths
// outcome follows the commit and carries the event's id, so a consumer reading both paths
@@ -15,7 +15,6 @@ const crypto = require('crypto');
@@ -15,7 +15,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-outbox-tx-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-outbox-tx-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -15,7 +15,7 @@ const { ids } = require('openvibe-contracts');
@@ -15,7 +15,7 @@ const { ids } = require('openvibe-contracts');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-owner-subject-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-owner-subject-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const env = {
const env = {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9', MEDIA_SIGNING_SECRET: 'test-signing-secret',
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'http://127.0.0.1:9', MEDIA_SIGNING_SECRET: 'test-signing-secret',
INTERNAL_API_KEY: 'internal-key-0123456789abcdef', OV_OAUTH_CLIENT_ID: 'media', OV_OAUTH_CLIENT_SECRET: 'media-secret',
INTERNAL_API_KEY: 'internal-key-0123456789abcdef', OV_OAUTH_CLIENT_ID: 'media', OV_OAUTH_CLIENT_SECRET: 'media-secret',
@@ -97,7 +97,7 @@ const { ids } = require('openvibe-contracts');
@@ -97,7 +97,7 @@ const { ids } = require('openvibe-contracts');
for (let uid = 1000; uid < 1520; uid++) { subjectOf(uid); many.push(await obj({ owner_user_id: uid, kind: 'clip' })); }
for (let uid = 1000; uid < 1520; uid++) { subjectOf(uid); many.push(await obj({ owner_user_id: uid, kind: 'clip' })); }
const TO_FILL = 3 + many.length;
const TO_FILL = 3 + many.length;
- // (The one-off backfill script ran in production on SQLite and is retired: the reconcile job below fills what is left.)
+ // (The one-off backfill script is retired: the reconcile job below fills what is left.)
// A token Network refuses at resolve-batch: the key answers instead.
// A token Network refuses at resolve-batch: the key answers instead.
const resolver = ownerSubject.createResolver({ networkUrl: NET });
const resolver = ownerSubject.createResolver({ networkUrl: NET });
net.grant = true;
net.grant = true;
@@ -10,7 +10,6 @@ const express = require('express');
@@ -10,7 +10,6 @@ const express = require('express');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-moved-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-moved-'));
- process.env.DB_PATH = path.join(tmp, 'media.db');
const db = require('../server/db/database');
const db = require('../server/db/database');
await db.upsertApp({ app_id: 'live', api_key: 'live-key' });
await db.upsertApp({ app_id: 'live', api_key: 'live-key' });
const shot = path.join(tmp, 'shot.png');
const shot = path.join(tmp, 'shot.png');
@@ -32,7 +32,7 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');
@@ -32,7 +32,7 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');
const port = await freePort();
const port = await freePort();
const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {
const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {
cwd: path.join(__dirname, '..'),
cwd: path.join(__dirname, '..'),
- env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test', DB_PATH: path.join(dir, 'media.db'), MEDIA_PGLITE_DIR: path.join(dir, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', VOD_PATH: path.join(dir, 'vods'), CLIPS_PATH: path.join(dir, 'clips'), FILES_PATH: path.join(dir, 'files'), THUMBNAILS_PATH: path.join(dir, 'thumbnails'), PASTES_PATH: path.join(dir, 'pastes'), OBJECTS_PATH: path.join(dir, 'objects'), MEDIA_JOBS_ENABLED: 'off' },
+ env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test', MEDIA_PGLITE_DIR: path.join(dir, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', VOD_PATH: path.join(dir, 'vods'), CLIPS_PATH: path.join(dir, 'clips'), FILES_PATH: path.join(dir, 'files'), THUMBNAILS_PATH: path.join(dir, 'thumbnails'), PASTES_PATH: path.join(dir, 'pastes'), OBJECTS_PATH: path.join(dir, 'objects'), MEDIA_JOBS_ENABLED: 'off' },
stdio: ['ignore', 'ignore', 'pipe'],
stdio: ['ignore', 'ignore', 'pipe'],
});
});
let stderr = '';
let stderr = '';
@@ -13,7 +13,6 @@ const http = require('http');
@@ -13,7 +13,6 @@ const http = require('http');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-private404-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-private404-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -9,7 +9,6 @@ const http = require('http');
@@ -9,7 +9,6 @@ const http = require('http');
(async () => {
(async () => {
const tmp = path.join(os.tmpdir(), `ov-media-pages-${process.pid}.db`);
const tmp = path.join(os.tmpdir(), `ov-media-pages-${process.pid}.db`);
- process.env.DB_PATH = tmp;
process.env.MEDIA_PUBLIC_URL = 'https://media.test';
process.env.MEDIA_PUBLIC_URL = 'https://media.test';
const db = require('../server/db/database');
const db = require('../server/db/database');
@@ -73,7 +73,7 @@ const crypto = require('crypto');
@@ -73,7 +73,7 @@ const crypto = require('crypto');
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
const endpoint = `http://127.0.0.1:${s3.address().port}`;
const endpoint = `http://127.0.0.1:${s3.address().port}`;
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',
MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',
@@ -16,7 +16,7 @@ const http = require('http');
@@ -16,7 +16,7 @@ const http = require('http');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-readiness-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-readiness-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),
OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',
});
});
@@ -13,7 +13,7 @@ const { EventEmitter } = require('events');
@@ -13,7 +13,7 @@ const { EventEmitter } = require('events');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-rec-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-rec-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
});
});
delete process.env.MEDIA_RTMP_PULL_ALLOW;
delete process.env.MEDIA_RTMP_PULL_ALLOW;
@@ -16,7 +16,7 @@ const http = require('http');
@@ -16,7 +16,7 @@ const http = require('http');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-holds-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-holds-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),
OBJECTS_PATH: dir('objects'), THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'),
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: 'https://openvibe.network',
});
});
@@ -10,7 +10,6 @@ const http = require('http');
@@ -10,7 +10,6 @@ const http = require('http');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-revoke-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-revoke-'));
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.MEDIA_INBOUND_EVENTS_SECRET = 'm'.repeat(40);
process.env.MEDIA_INBOUND_EVENTS_SECRET = 'm'.repeat(40);
const express = require('express');
const express = require('express');
const { signDeliveryHeaders } = require('openvibe-sdk/events');
const { signDeliveryHeaders } = require('openvibe-sdk/events');
@@ -14,7 +14,7 @@ const express = require('express');
@@ -14,7 +14,7 @@ const express = require('express');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-rtmp-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-rtmp-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
});
});
delete process.env.MEDIA_RTMP_PULL_ALLOW;
delete process.env.MEDIA_RTMP_PULL_ALLOW;
@@ -23,7 +23,7 @@ const crypto = require('crypto');
@@ -23,7 +23,7 @@ const crypto = require('crypto');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-idor-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-idor-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), ASSETS_PATH: dir('assets'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), ASSETS_PATH: dir('assets'),
OV_NETWORK_URL: 'https://openvibe.network', MEDIA_PUBLIC_URL: 'https://media.test', MEDIA_SIGNING_SECRET: 'test-signing-secret',
OV_NETWORK_URL: 'https://openvibe.network', MEDIA_PUBLIC_URL: 'https://media.test', MEDIA_SIGNING_SECRET: 'test-signing-secret',
MEDIA_UPLOAD_MIN_FREE_MB: '0', MEDIA_MULTIPART_MIN_PART_MB: '1', MEDIA_JOBS_ENABLED: '0',
MEDIA_UPLOAD_MIN_FREE_MB: '0', MEDIA_MULTIPART_MIN_PART_MB: '1', MEDIA_JOBS_ENABLED: '0',
@@ -21,7 +21,6 @@ const crypto = require('crypto');
@@ -21,7 +21,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-secprivate-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-secprivate-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.FILES_PATH = dir('files');
process.env.FILES_PATH = dir('files');
@@ -86,7 +86,7 @@ const crypto = require('crypto');
@@ -86,7 +86,7 @@ const crypto = require('crypto');
const NET = `http://127.0.0.1:${network.address().port}`;
const NET = `http://127.0.0.1:${network.address().port}`;
Object.assign(process.env, SECRET_ENV, {
Object.assign(process.env, SECRET_ENV, {
PORT: '0', HOST: '127.0.0.1', NODE_ENV: 'test',
PORT: '0', HOST: '127.0.0.1', NODE_ENV: 'test',
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), PASTES_PATH: dir('pastes'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), PASTES_PATH: dir('pastes'),
THUMBNAILS_PATH: dir('thumbnails'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
THUMBNAILS_PATH: dir('thumbnails'), FILES_PATH: dir('files'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: NET, OV_NETWORK_INTERNAL_URL: NET, EVENTS_URL: NET,
MEDIA_PUBLIC_URL: 'https://media.test', OV_NETWORK_URL: NET, OV_NETWORK_INTERNAL_URL: NET, EVENTS_URL: NET,
OV_LIVE_INTERNAL_URL: CLOSED, LIVE_APP_INTERNAL_URL: CLOSED, APP_INTERNAL_URLS: JSON.stringify({ live: CLOSED }),
OV_LIVE_INTERNAL_URL: CLOSED, LIVE_APP_INTERNAL_URL: CLOSED, APP_INTERNAL_URLS: JSON.stringify({ live: CLOSED }),
@@ -21,7 +21,6 @@ const https = require('https');
@@ -21,7 +21,6 @@ const https = require('https');
(async () => {
(async () => {
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-ssrf-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-ssrf-'));
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.PASTES_PATH = path.join(tmp, 'pastes');
process.env.PASTES_PATH = path.join(tmp, 'pastes');
process.env.MEDIA_PUBLIC_URL = 'https://media.test';
process.env.MEDIA_PUBLIC_URL = 'https://media.test';
delete process.env.MEDIA_RTMP_PULL_ALLOW;
delete process.env.MEDIA_RTMP_PULL_ALLOW;
@@ -10,7 +10,6 @@ const crypto = require('crypto');
@@ -10,7 +10,6 @@ const crypto = require('crypto');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-svc-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-svc-'));
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.FILES_PATH = path.join(tmp, 'files');
process.env.FILES_PATH = path.join(tmp, 'files');
process.env.OV_NETWORK_URL = 'https://openvibe.network';
process.env.OV_NETWORK_URL = 'https://openvibe.network';
process.env.MEDIA_PUBLIC_URL = 'https://media.test';
process.env.MEDIA_PUBLIC_URL = 'https://media.test';
@@ -7,7 +7,6 @@ const path = require('path');
@@ -7,7 +7,6 @@ const path = require('path');
(async () => {
(async () => {
const tmp = path.join(os.tmpdir(), `ov-media-series-${process.pid}.db`);
const tmp = path.join(os.tmpdir(), `ov-media-series-${process.pid}.db`);
- process.env.DB_PATH = tmp;
const db = require('../server/db/database');
const db = require('../server/db/database');
const raw = db.getDb();
const raw = db.getDb();
@@ -53,7 +53,7 @@ const crypto = require('crypto');
@@ -53,7 +53,7 @@ const crypto = require('crypto');
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
const endpoint = `http://127.0.0.1:${s3.address().port}`;
const endpoint = `http://127.0.0.1:${s3.address().port}`;
const env = {
const env = {
- DB_PATH: path.join(tmp, 'data', 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ MEDIA_DATA_DIR: path.join(tmp, 'data'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), ASSETS_PATH: dir('assets'),
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',
MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',
@@ -15,7 +15,7 @@ const express = require('express');
@@ -15,7 +15,7 @@ const express = require('express');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-thumbfail-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-thumbfail-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_INVARIANT_SCAN_HOURS: '0',
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_INVARIANT_SCAN_HOURS: '0',
});
});
@@ -12,7 +12,7 @@ const path = require('path');
@@ -12,7 +12,7 @@ const path = require('path');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-tiercfg-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-tiercfg-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
});
});
@@ -63,7 +63,7 @@ const crypto = require('crypto');
@@ -63,7 +63,7 @@ const crypto = require('crypto');
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
await new Promise((r) => s3.listen(0, '127.0.0.1', r));
const endpoint = `http://127.0.0.1:${s3.address().port}`;
const endpoint = `http://127.0.0.1:${s3.address().port}`;
Object.assign(process.env, {
Object.assign(process.env, {
- DB_PATH: path.join(tmp, 'media.db'), VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'),
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_B2_ENDPOINT: endpoint, MEDIA_B2_BUCKET: 'b2-bucket', MEDIA_B2_KEY_ID: 'k', MEDIA_B2_APP_KEY: 's', MEDIA_B2_REGION: 'us-west-004',
MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',
MEDIA_R2_ENDPOINT: endpoint, MEDIA_R2_BUCKET: 'r2-bucket', MEDIA_R2_ACCESS_KEY_ID: 'k', MEDIA_R2_SECRET_ACCESS_KEY: 's',
@@ -57,7 +57,7 @@ const express = require('express');
@@ -57,7 +57,7 @@ const express = require('express');
assert.ok(!/set\('trust proxy',\s*true\)/.test(src), 'no trust-everyone left');
assert.ok(!/set\('trust proxy',\s*true\)/.test(src), 'no trust-everyone left');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-trust-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-trust-'));
const freePort = await new Promise((resolve) => { const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); });
const freePort = await new Promise((resolve) => { const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); });
- const env = { ...process.env, DB_PATH: path.join(tmp, 'media.db'), MEDIA_PGLITE_DIR: path.join(tmp, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', VOD_PATH: path.join(tmp, 'vods'), CLIPS_PATH: path.join(tmp, 'clips'), PASTES_PATH: path.join(tmp, 'pastes'),
+ const env = { ...process.env, MEDIA_PGLITE_DIR: path.join(tmp, 'pglite'), DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', VOD_PATH: path.join(tmp, 'vods'), CLIPS_PATH: path.join(tmp, 'clips'), PASTES_PATH: path.join(tmp, 'pastes'),
THUMBNAILS_PATH: path.join(tmp, 'thumbs'), FILES_PATH: path.join(tmp, 'files'), OBJECTS_PATH: path.join(tmp, 'objects'), PORT: String(freePort), HOST: ext ? '0.0.0.0' : '127.0.0.1',
THUMBNAILS_PATH: path.join(tmp, 'thumbs'), FILES_PATH: path.join(tmp, 'files'), OBJECTS_PATH: path.join(tmp, 'objects'), PORT: String(freePort), HOST: ext ? '0.0.0.0' : '127.0.0.1',
NODE_ENV: 'test', MEDIA_JOBS_ENABLED: '0', MEDIA_VERIFY_ENABLED: '0', MEDIA_OWNER_SUBJECT_SYNC: '0', EVENTS_URL: '', MEDIA_APP_KEYS: 'live:live-key-for-trust-test' };
NODE_ENV: 'test', MEDIA_JOBS_ENABLED: '0', MEDIA_VERIFY_ENABLED: '0', MEDIA_OWNER_SUBJECT_SYNC: '0', EVENTS_URL: '', MEDIA_APP_KEYS: 'live:live-key-for-trust-test' };
const child = spawn(process.execPath, [path.join(__dirname, '../server/index.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });
const child = spawn(process.execPath, [path.join(__dirname, '../server/index.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });
@@ -13,7 +13,6 @@ const { spawnSync } = require('child_process');
@@ -13,7 +13,6 @@ const { spawnSync } = require('child_process');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-duration-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-duration-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.THUMBNAILS_PATH = dir('thumbnails');
process.env.THUMBNAILS_PATH = dir('thumbnails');
@@ -12,7 +12,6 @@ const path = require('path');
@@ -12,7 +12,6 @@ const path = require('path');
(async () => {
(async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-finalize-job-'));
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-finalize-job-'));
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
- process.env.DB_PATH = path.join(tmp, 'media.db');
process.env.VOD_PATH = dir('vods');
process.env.VOD_PATH = dir('vods');
process.env.CLIPS_PATH = dir('clips');
process.env.CLIPS_PATH = dir('clips');
process.env.THUMBNAILS_PATH = dir('thumbnails');
process.env.THUMBNAILS_PATH = dir('thumbnails');