Changes in src/services/ArchiveExportService.js [e9ec5e4:ba76bf5] in Klonkt
- File:
-
- 1 edited
-
src/services/ArchiveExportService.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ArchiveExportService.js
re9ec5e4 rba76bf5 38 38 39 39 const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex'); 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 */ 40 // SQLite's CURRENT_TIMESTAMP schrijft 'YYYY-MM-DD HH:MM:SS' in UTC, zonder marker. 41 // Kale Date.parse leest dat als LOKALE tijd, dus op een server op UTC+2 ging er twee 42 // uur van elke stempel af voordat hij het archief in ging. Die verschuiving wordt bij 43 // het exporteren ingebakken en valt niet weg bij het importeren: exporteer je in 44 // Amsterdam, dan is die post overal permanent twee uur te vroeg, en in zomer- en 45 // wintertijd verschillend. Het raakte de stempels die de database zelf zet (concepten, 46 // ingeplande posts, gearchiveerde antwoorden), niet die uit de editor, dus de schade 47 // was stil en gedeeltelijk. EXPORT-FORMAT.md schreef altijd al "ISO 8601, UTC" voor. 48 // Zelfde regel als isoStamp() in ActivityPubService en stampMs() in guardianship/offers. 49 const SQL_STAMP = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(\.\d+)?$/; 52 50 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); 51 const s = String(d == null ? '' : d); 52 const t = Date.parse(SQL_STAMP.test(s) ? `${s.replace(' ', 'T')}Z` : s); 57 53 return isNaN(t) ? null : new Date(t).toISOString(); 58 54 }; … … 238 234 } 239 235 240 // ── Wie je volgt, als CSV ─────────────────────────────────────────241 //242 // Kolomvorm van Mastodon, zodat deze lijst ook DAAR te importeren is en die van243 // daar hier. Dat is het hele punt van een verhuisformaat: het moet ook werken244 // als je naar iets anders vertrekt dan waar je vandaan kwam.245 //246 // Account address,Show boosts,Notify on new posts,Languages,Featured247 //248 // `Featured` is onze kolom en draagt `auto_boost`: het vinkje dat in de UI249 // "Uitgelicht" heet (tl.autoboost) en hun posts in jouw Cirkel laat meelopen.250 //251 // `Show boosts` blijft LEEG. Dat is bij Mastodon "toon de reblogs van deze252 // persoon in mijn tijdlijn", en dat kent Klonkt niet. De verleiding is groot om253 // er auto_boost in te schrijven omdat in beide het woord boost zit, maar het is254 // een ander ding: dat van ons gaat over hun eigen posts in JOUW Cirkel, niet255 // over andermans posts die zij doorgeven. `Notify` en `Languages` kent Klonkt256 // evenmin. Die drie staan er omdat Mastodon de POSITIES telt.257 const CSV_KOP = 'Account address,Show boosts,Notify on new posts,Languages,Featured';258 259 /** Een veld dat een komma, aanhalingsteken of nieuwe regel bevat moet geciteerd. */260 function csvVeld(v) {261 const s = String(v == null ? '' : v);262 return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;263 }264 265 /**266 * De volglijst van een site als CSV, of null als er niets te melden valt.267 *268 * Alleen `accepted`: een openstaand verzoek is geen relatie, en het opnieuw269 * versturen ervan op de nieuwe plek zou een tweede verzoek zijn bij iemand die270 * de eerste misschien bewust liet liggen.271 *272 * Het adres is de handle zonder de leidende @, want zo schrijft Mastodon hem.273 * Ontbreekt de handle, dan valt hij terug op de actor-URI: die is altijd te274 * herleiden, ook als de webfinger-naam ooit verloren ging.275 */276 export function followingCsv(slug) {277 let rijen = [];278 try {279 rijen = db.prepare(`SELECT actor_uri, handle, auto_boost FROM ap_following280 WHERE slug = ? AND status = 'accepted'281 ORDER BY handle IS NULL, handle, actor_uri`).all(slug);282 } catch { return null; } // oude database zonder de kolom283 if (!rijen.length) return null;284 const regels = rijen.map((r) => [285 csvVeld((r.handle || r.actor_uri || '').replace(/^@/, '')),286 '', // Show boosts: niet van ons287 '', // Notify on new posts: idem288 '', // Languages: idem289 r.auto_boost ? 'true' : 'false', // Featured: het vinkje "Uitgelicht"290 ].join(','));291 return `${CSV_KOP}\n${regels.join('\n')}\n`;292 }293 294 /**295 * Lees zo'n CSV terug. Puur, zodat de vorm te toetsen is zonder database.296 *297 * Vergeeflijk met opzet: een bestand uit Mastodon heeft vier kolommen en geen298 * `Featured`, een handgemaakt bestand heeft misschien alleen adressen. Beide299 * moeten werken, want anders is het geen uitwisselformaat maar een eigen300 * bestandje dat toevallig op een CSV lijkt.301 */302 export function parseFollowingCsv(text) {303 const uit = [];304 const regels = String(text || '').split(/\r?\n/).filter((r) => r.trim());305 if (!regels.length) return uit;306 // Een kopregel herkennen we aan het eerste veld; anders is regel 1 al data.307 const start = /^\s*"?account address"?\s*(,|$)/i.test(regels[0]) ? 1 : 0;308 for (const regel of regels.slice(start)) {309 const velden = splitsCsvRegel(regel);310 const adres = (velden[0] || '').trim().replace(/^@/, '');311 if (!adres) continue;312 uit.push({313 address: adres,314 // Alleen kolom 5. Een bestand uit Mastodon heeft die niet en levert dus315 // `false`, en dat is juist: hun "Show boosts" in kolom 2 gaat over iets316 // anders en mag hier niet als uitgelicht binnenkomen.317 featured: /^(true|1|yes)$/i.test((velden[4] || '').trim()),318 });319 }320 return uit;321 }322 323 /** Eén CSV-regel, met respect voor geciteerde velden en verdubbelde aanhalingstekens. */324 function splitsCsvRegel(regel) {325 const velden = [];326 let veld = '';327 let inCitaat = false;328 for (let i = 0; i < regel.length; i++) {329 const c = regel[i];330 if (inCitaat) {331 if (c === '"') {332 if (regel[i + 1] === '"') { veld += '"'; i++; } else inCitaat = false;333 } else veld += c;334 } else if (c === '"') inCitaat = true;335 else if (c === ',') { velden.push(veld); veld = ''; }336 else veld += c;337 }338 velden.push(veld);339 return velden;340 }341 342 236 /** 343 237 * Bouw het archief als een lijst bestanden: pad -> inhoud (Buffer). … … 349 243 const origin = (opts.origin || process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 350 244 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug); 351 if (!site) { 352 // De naam van de INSTANCE (de map, de unit) en de slug van de SITE in zijn 353 // database zijn twee dingen. Ze vallen vaak samen en soms niet, en dan zat je 354 // met een foutmelding die je liet raden. Zeg dus wat er wel in staat. 355 let bestaand = []; 356 try { bestaand = db.prepare('SELECT slug FROM sites ORDER BY rowid').all().map((r) => r.slug); } catch { /* geen sites-tabel */ } 357 const wat = slug ? `onbekende site: ${slug}` : 'geen site opgegeven'; 358 throw new Error(bestaand.length 359 ? `${wat}. In deze database staat: ${bestaand.join(', ')}` 360 : `${wat}. In deze database staat geen enkele site -- wijst DATABASE_PATH naar de juiste?`); 361 } 245 if (!site) throw new Error(`onbekende site: ${slug}`); 362 246 363 247 const bestanden = new Map(); // pad -> Buffer … … 438 322 } 439 323 440 // Wie je volgt. Dit ontbrak, en daarmee was een "verhuizing" halfslachtig: de441 // Move vertelt je VOLGERS waar je heen ging, maar niets vertelde JOU wie jij442 // volgde. Die lijst stond alleen in de oude database, en die laat je achter.443 const volgCsv = followingCsv(slug);444 if (volgCsv) { bestanden.set('following.csv', Buffer.from(volgCsv, 'utf8')); tellingen.following = volgCsv.trim().split('\n').length - 1; }445 446 324 const files = {}; 447 325 for (const pad of [...bestanden.keys()].sort()) files[pad] = sha256(bestanden.get(pad));
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)