Changeset 6248497 in Klonkt


Ignore:
Timestamp:
08/07/2026 04:37:37 PM (5 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
a4fea5e
Parents:
7d7b36d
git-author:
roboburr <roboburr@…> (08/07/2026 04:37:26 PM)
git-committer:
roboburr <roboburr@…> (08/07/2026 04:37:37 PM)
Message:

Export en import kiezen de juiste instance, en tijdstempels zijn echt UTC

Twee fouten, allebei op productie gevonden en allebei van het soort dat SUCCES
meldt.

## 1. Het script opende de verkeerde database

Op een split install deelt elke Klonkt de code in /opt/klonkt maar staat zijn data
onder /var/lib/klonkt/<slug>/ met een eigen .env. De scripts lazen die .env niet,
dus database.js viel terug op storage/database.sqlite IN DE CODE-MAP. Op een
server die ooit de enkelvoudige opzet draaide ligt daar een achtergebleven oude
database.

Waargenomen: export-archive.mjs boiert schreef een archief van 444 bytes met
"posts: 0" en meldde dat het gelukt was, terwijl het in een oude lege database
keek. liz en sood gaven "onbekende site". Het script raakte die oude database
ook nog aan (boot-migraties), terwijl het alleen hoort te lezen.

Nu leest scripts/instance-env.mjs de .env van de instance (standaard
/var/lib/klonkt/<slug>/.env, of --data-root / --env) VOORDAT de service geladen
wordt -- database.js opent de database namelijk bij import. De uitvoer noemt
voortaan welke .env en welke database gebruikt zijn.

En zonder PUBLIC_BASE_URL stopt hij nu HARD in plaats van het als bijzin te
melden: zonder origin krijgt het archief een lege origin, en dan maakt een import
nieuwe AP-ids. Dan is het een kopie van de tekst en geen herstel -- precies de
belofte waar het formaat om draait.

Voor de importer weegt dit zwaarder dan voor de exporter: in de verkeerde
database schrijven draai je niet terug.

## 2. Tijdstempels schoven met de tijdzone van de machine

Vraag van Bart: normaliseert de export wel naar UTC? Nee.

SQLite schrijft CURRENT_TIMESTAMP als "2026-07-01 12:56:10" -- in UTC, maar zonder
zone. Date.parse leest die vorm als LOKALE tijd. Op een machine in Amsterdam komt
daar 10:56:10Z uit: twee uur verschoven, in elk archief.

Geen enkele test kon dat zien, want de testmachine draait op UTC. En mijn eigen
rondgang-vergelijking plakte er zelf een Z achter, waarmee ik er precies overheen
keek.

toISO leest een zoneloze vorm nu expliciet als UTC. Nieuwe test die alleen onder
een niet-UTC zone iets bewijst; gecontroleerd dat hij onder Europe/Amsterdam
omvalt zonder de fix. Hele suite 577/577 onder zowel UTC als Europe/Amsterdam.

Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • docs/EXPORT-FORMAT.md

    r7d7b36d r6248497  
    9999| `summary` | `posts.content_warning` | AS2 summary is the content warning |
    100100| `sensitive` | `posts.nsfw` | |
    101 | `published` | `posts.published_at` ?? `created_at` | ISO 8601, UTC. Klonkt stores timestamps in two spellings (`YYYY-MM-DD HH:MM:SS` and full ISO); the archive normalises to ISO. The **instant** survives a round trip, the spelling does not. |
     101| `published` | `posts.published_at` ?? `created_at` | ISO 8601, UTC. SQLite writes `CURRENT_TIMESTAMP` in UTC but without a zone, and `Date.parse` reads that shape as *local* time — so a timezone-less value is explicitly read as UTC before conversion. Klonkt stores timestamps in two spellings (`YYYY-MM-DD HH:MM:SS` and full ISO); the archive normalises to ISO. The **instant** survives a round trip, the spelling does not. |
    102102| `updated` | `posts.updated_at` | omitted when equal to `published` |
    103103| `url` | `<origin>/<slug>` | the human permalink |
  • scripts/export-archive.mjs

    r7d7b36d r6248497  
    66//     node scripts/export-archive.mjs <slug> --out boiert.zip
    77//     node scripts/export-archive.mjs <slug> --dir ./archief
     8//
     9// Op een machine met meerdere instances leest hij de .env van die instance zelf
     10// (standaard /var/lib/klonkt/<slug>/.env). Dat is niet netjesheid maar noodzaak:
     11// zonder die .env opent hij de database die toevallig in de code-map ligt, en op
     12// een server die ooit de enkelvoudige opzet draaide is dat een oude lege. Dan
     13// komt er een archief uit dat MELDT dat het gelukt is en nul posts bevat.
     14//
     15//     --data-root <map>   waar de instances staan (standaard /var/lib/klonkt)
     16//     --env <pad>         een .env rechtstreeks aanwijzen
    817//
    918// Het formaat staat in docs/EXPORT-FORMAT.md.
     
    1524import fs from 'fs';
    1625import path from 'path';
    17 import { buildArchive, zipArchive, writeArchiveDir } from '../src/services/ArchiveExportService.js';
     26import { kiesInstance, eisOrigin } from './instance-env.mjs';
    1827
    1928const args = process.argv.slice(2);
     
    2231
    2332if (!slug) {
    24   console.error('gebruik: node scripts/export-archive.mjs <slug> [--dry-run | --out <zip> | --dir <map>]');
     33  console.error('gebruik: node scripts/export-archive.mjs <slug> [--data-root <map>] [--env <pad>] [--dry-run | --out <zip> | --dir <map>]');
    2534  process.exit(1);
    2635}
    2736
    28 const uit = buildArchive(slug);
     37// EERST de instance kiezen, DAN pas de service laden: src/config/database.js
     38// leest DATABASE_PATH bij import en opent de database meteen.
     39let gekozen;
     40try {
     41  gekozen = kiesInstance(slug, {
     42    dataRoot: typeof vlag('--data-root') === 'string' ? vlag('--data-root') : null,
     43    envPad: typeof vlag('--env') === 'string' ? vlag('--env') : null,
     44  });
     45} catch (e) { console.error(e.message); process.exit(1); }
     46eisOrigin(gekozen.bron);
     47
     48const { buildArchive, zipArchive, writeArchiveDir } = await import('../src/services/ArchiveExportService.js');
     49
     50let uit;
     51try { uit = buildArchive(slug); }
     52catch (e) {
     53  console.error(`${e.message}`);
     54  if (gekozen.bron) console.error(`(gelezen uit ${gekozen.bron})`);
     55  else console.error('(geen instance-.env gevonden -- op een split install: --data-root /var/lib/klonkt)');
     56  process.exit(1);
     57}
    2958const t = uit.counts;
    30 console.log(`site      : ${uit.manifest.site.slug} (${uit.manifest.origin || 'geen PUBLIC_BASE_URL'})`);
     59console.log(`site      : ${uit.manifest.site.slug} (${uit.manifest.origin})`);
     60console.log(`instellingen uit: ${gekozen.bron || 'de omgeving'}`);
     61console.log(`database  : ${process.env.DATABASE_PATH || '(standaard in de code-map)'}`);
    3162console.log(`posts     : ${t.posts}`);
    3263console.log(`antwoorden: ${t.replies}   (alleen-lezen archief)`);
  • scripts/import-archive.mjs

    r7d7b36d r6248497  
    1717// en hoort een aparte, bewuste actie te zijn.
    1818
    19 import { readArchive, importArchive } from '../src/services/ArchiveImportService.js';
     19import { kiesInstance, eisOrigin } from './instance-env.mjs';
    2020
    2121const args = process.argv.slice(2);
     
    2424
    2525if (!slug || !bron) {
    26   console.error('gebruik: node scripts/import-archive.mjs <slug> <archief.zip|map> [--dry-run] [--overwrite]');
     26  console.error('gebruik: node scripts/import-archive.mjs <slug> <archief.zip|map> [--data-root <map>] [--env <pad>] [--dry-run] [--overwrite]');
    2727  process.exit(1);
    2828}
     29
     30// EERST de instance kiezen, DAN pas de service laden -- database.js opent de
     31// database bij import. Bij een IMPORT weegt dit zwaarder dan bij een export: in
     32// de verkeerde database schrijven is niet terug te draaien.
     33const vlag = (naam) => { const i = args.indexOf(naam); return i >= 0 ? (args[i + 1] || null) : null; };
     34let gekozen;
     35try {
     36  gekozen = kiesInstance(slug, { dataRoot: vlag('--data-root'), envPad: vlag('--env') });
     37} catch (e) { console.error(e.message); process.exit(1); }
     38eisOrigin(gekozen.bron);
     39console.log(`instellingen uit: ${gekozen.bron || 'de omgeving'}`);
     40console.log(`database        : ${process.env.DATABASE_PATH || '(standaard in de code-map)'}`);
     41
     42const { readArchive, importArchive } = await import('../src/services/ArchiveImportService.js');
    2943
    3044let files;
  • src/services/ArchiveExportService.js

    r7d7b36d r6248497  
    3838
    3939const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
    40 const toISO = (d) => { const t = Date.parse(d); return isNaN(t) ? null : new Date(t).toISOString(); };
     40/**
     41 * Naar ISO 8601 in UTC.
     42 *
     43 * SQLite schrijft CURRENT_TIMESTAMP als "2026-07-01 12:56:10" -- in UTC, maar
     44 * ZONDER zone erbij. Date.parse leest die vorm als LOKALE tijd, en dan schuift
     45 * elk tijdstempel in het archief mee met de tijdzone van de machine die de export
     46 * draait. Op een server in Amsterdam is dat twee uur, en dat merk je pas als je
     47 * ergens anders importeert.
     48 *
     49 * Gevonden doordat Bart vroeg of dit wel naar UTC normaliseert. De testmachine
     50 * draait op UTC, dus geen enkele test kon het zien.
     51 */
     52const toISO = (d) => {
     53  if (!d) return null;
     54  const s = String(d).trim();
     55  const zonderZone = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(s);
     56  const t = Date.parse(zonderZone ? `${s.replace(' ', 'T')}Z` : s);
     57  return isNaN(t) ? null : new Date(t).toISOString();
     58};
    4159
    4260const MIME_BY_EXT = {
  • test/archive-export.test.js

    r7d7b36d r6248497  
    175175});
    176176
     177test('een SQL-tijdstempel wordt als UTC gelezen, niet als lokale tijd', () => {
     178  // SQLite schrijft CURRENT_TIMESTAMP in UTC zonder zone erbij. Date.parse leest
     179  // die vorm als LOKALE tijd, en dan schuift elk tijdstempel in het archief mee
     180  // met de machine die de export draait -- in Amsterdam twee uur.
     181  //
     182  // Deze test is alleen zinvol onder een NIET-UTC tijdzone; draai hem daarom ook
     183  // eens als:  TZ=Europe/Amsterdam node --test test/archive-export.test.js
     184  post('tijdstip', { published_at: '2026-07-01 12:56:10', created_at: '2026-07-01 12:56:10' });
     185  const o = lees(AX.buildArchive('me'), 'posts/tijdstip.json');
     186  assert.equal(o.published, '2026-07-01T12:56:10.000Z',
     187    'het moment uit de database is UTC en hoort dat te blijven');
     188});
     189
    177190test('een concept gaat gewoon mee', () => {
    178191  post('concept', { status: 'draft', published_at: null });
Note: See TracChangeset for help on using the changeset viewer.