Changeset 6248497 in Klonkt
- Timestamp:
- 08/07/2026 04:37:37 PM (5 weeks ago)
- 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)
- Files:
-
- 1 added
- 5 edited
-
docs/EXPORT-FORMAT.md (modified) (1 diff)
-
scripts/export-archive.mjs (modified) (3 diffs)
-
scripts/import-archive.mjs (modified) (2 diffs)
-
scripts/instance-env.mjs (added)
-
src/services/ArchiveExportService.js (modified) (1 diff)
-
test/archive-export.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
docs/EXPORT-FORMAT.md
r7d7b36d r6248497 99 99 | `summary` | `posts.content_warning` | AS2 summary is the content warning | 100 100 | `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. | 102 102 | `updated` | `posts.updated_at` | omitted when equal to `published` | 103 103 | `url` | `<origin>/<slug>` | the human permalink | -
scripts/export-archive.mjs
r7d7b36d r6248497 6 6 // node scripts/export-archive.mjs <slug> --out boiert.zip 7 7 // 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 8 17 // 9 18 // Het formaat staat in docs/EXPORT-FORMAT.md. … … 15 24 import fs from 'fs'; 16 25 import path from 'path'; 17 import { buildArchive, zipArchive, writeArchiveDir } from '../src/services/ArchiveExportService.js';26 import { kiesInstance, eisOrigin } from './instance-env.mjs'; 18 27 19 28 const args = process.argv.slice(2); … … 22 31 23 32 if (!slug) { 24 console.error('gebruik: node scripts/export-archive.mjs <slug> [--d ry-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>]'); 25 34 process.exit(1); 26 35 } 27 36 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. 39 let gekozen; 40 try { 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); } 46 eisOrigin(gekozen.bron); 47 48 const { buildArchive, zipArchive, writeArchiveDir } = await import('../src/services/ArchiveExportService.js'); 49 50 let uit; 51 try { uit = buildArchive(slug); } 52 catch (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 } 29 58 const t = uit.counts; 30 console.log(`site : ${uit.manifest.site.slug} (${uit.manifest.origin || 'geen PUBLIC_BASE_URL'})`); 59 console.log(`site : ${uit.manifest.site.slug} (${uit.manifest.origin})`); 60 console.log(`instellingen uit: ${gekozen.bron || 'de omgeving'}`); 61 console.log(`database : ${process.env.DATABASE_PATH || '(standaard in de code-map)'}`); 31 62 console.log(`posts : ${t.posts}`); 32 63 console.log(`antwoorden: ${t.replies} (alleen-lezen archief)`); -
scripts/import-archive.mjs
r7d7b36d r6248497 17 17 // en hoort een aparte, bewuste actie te zijn. 18 18 19 import { readArchive, importArchive } from '../src/services/ArchiveImportService.js';19 import { kiesInstance, eisOrigin } from './instance-env.mjs'; 20 20 21 21 const args = process.argv.slice(2); … … 24 24 25 25 if (!slug || !bron) { 26 console.error('gebruik: node scripts/import-archive.mjs <slug> <archief.zip|map> [--d ry-run] [--overwrite]');26 console.error('gebruik: node scripts/import-archive.mjs <slug> <archief.zip|map> [--data-root <map>] [--env <pad>] [--dry-run] [--overwrite]'); 27 27 process.exit(1); 28 28 } 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. 33 const vlag = (naam) => { const i = args.indexOf(naam); return i >= 0 ? (args[i + 1] || null) : null; }; 34 let gekozen; 35 try { 36 gekozen = kiesInstance(slug, { dataRoot: vlag('--data-root'), envPad: vlag('--env') }); 37 } catch (e) { console.error(e.message); process.exit(1); } 38 eisOrigin(gekozen.bron); 39 console.log(`instellingen uit: ${gekozen.bron || 'de omgeving'}`); 40 console.log(`database : ${process.env.DATABASE_PATH || '(standaard in de code-map)'}`); 41 42 const { readArchive, importArchive } = await import('../src/services/ArchiveImportService.js'); 29 43 30 44 let files; -
src/services/ArchiveExportService.js
r7d7b36d r6248497 38 38 39 39 const 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 */ 52 const 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 }; 41 59 42 60 const MIME_BY_EXT = { -
test/archive-export.test.js
r7d7b36d r6248497 175 175 }); 176 176 177 test('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 177 190 test('een concept gaat gewoon mee', () => { 178 191 post('concept', { status: 'draft', published_at: null });
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)