Changeset f894be5 in Klonkt for src/services/MigrationService.js
- Timestamp:
- 08/14/2026 01:20:51 AM (4 weeks ago)
- Branches:
- main
- Children:
- 9f9b45f
- Parents:
- 5a49eba
- File:
-
- 1 edited
-
src/services/MigrationService.js (modified) (11 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/MigrationService.js
r5a49eba rf894be5 240 240 const AFBEELDING = /^image\//i; 241 241 242 /** 243 * Hoort deze URL bij de bron, en wijst hij onder /media/? 244 * 245 * Dan behouden we het PAD. Drie redenen tegelijk: 246 * - de gebakken content verwijst relatief of absoluut naar dat pad, en met 247 * hetzelfde pad hier klopt elke verwijzing zonder herschrijf-acrobatiek; 248 * - de media-bibliotheek (Beheer, Media) scant de MAP post-images, niet de 249 * databasetabel. Een bestand onder migrated/<uuid> bestaat wel en is 250 * onzichtbaar: Robins lege images-tab; 251 * - de zip-import bewaart originele paden al, dus zo convergeren beide 252 * routes op dezelfde bestanden. 253 * 254 * De ../-bewaking is geen formaliteit: het pad komt van een andere server. 255 */ 256 function bronMediaPad(url, bronOrigin, { mediaRoot, path }) { 257 try { 258 const u = new URL(String(url)); 259 if (`${u.protocol}//${u.host}` !== bronOrigin) return null; 260 if (!u.pathname.startsWith('/media/')) return null; 261 const rel = decodeURIComponent(u.pathname.slice('/media/'.length)); 262 const abs = path.resolve(mediaRoot, rel); 263 const root = path.resolve(mediaRoot); 264 if (abs === root || !abs.startsWith(`${root}${path.sep}`)) return null; 265 return { rel: `/media/${rel}`, abs }; 266 } catch { return null; } 267 } 268 242 269 /** AS2 geeft de duur als ISO-8601 ("PT212S"), de database wil seconden. */ 243 270 function duurSeconden(v) { … … 279 306 * moet stranden, ook als die bron "van onszelf" is. 280 307 */ 281 async function haalBijlage(url, { safeFetch, mediaRoot, fs, path, maxBytes, submap = 'migrated', headers = null }) {308 async function haalBijlage(url, { safeFetch, mediaRoot, fs, path, maxBytes, submap = 'migrated', headers = null, doel = null }) { 282 309 // Ondertekend als het moet. Gehoste audio zit achter dezelfde poort als de 283 310 // rest van de bron, en een kale fetch krijgt daar een 403: de bron kan dan … … 295 322 })(); 296 323 const naam = `${crypto.randomUUID()}.${ext}`; 324 // `doel` wint: dan behouden we het pad van de bron (zie bronMediaPad). 297 325 // Zonder submap komt het bestand in de root zelf: dat is wat gehoste audio 298 326 // nodig heeft, want de speler zoekt AUDIO_ROOT + bestandsnaam en kijkt niet 299 327 // in mappen eronder. 300 const rel = submap ? `${submap}/${naam}` : naam;301 const abs = submap ? path.join(mediaRoot, submap, naam) : path.join(mediaRoot, naam);328 const rel = doel ? doel.rel : (submap ? `${submap}/${naam}` : naam); 329 const abs = doel ? doel.abs : (submap ? path.join(mediaRoot, submap, naam) : path.join(mediaRoot, naam)); 302 330 fs.mkdirSync(path.dirname(abs), { recursive: true }); 303 331 fs.writeFileSync(abs, buf); 304 return { url: `/media/${rel}`, mediaType: type, size: buf.length, filename: naam, storage_path: abs }; 332 // doel.rel is al een volledig /media/-pad; de submap-variant is dat nog niet. 333 return { url: doel ? doel.rel : `/media/${rel}`, mediaType: type, size: buf.length, filename: naam, storage_path: abs }; 334 } 335 336 /** 337 * Alle bron-media in een lap HTML binnenhalen en de verwijzingen relatief maken. 338 * 339 * Werkt op ALLE https://bron/media/...-voorkomens, niet alleen op <img src>: 340 * de gebakken content zet dezelfde URL ook in een href om het plaatje groot te 341 * openen, en een half herschreven paar (lokaal plaatje, hotlink eromheen) is 342 * verwarrender dan geen herschrijving. 343 * 344 * Idempotent: wat al gedownload is wordt niet opnieuw gehaald, en een tweede 345 * ronde over dezelfde tekst vindt gewoon niets meer te doen. 346 */ 347 async function inhoudMediaBinnen(html, bronOrigin, site, rapport, { safeFetch, mediaRoot, fs, path, maxBytes }) { 348 let inhoud = String(html || ''); 349 if (!inhoud || !bronOrigin) return { inhoud, n: 0 }; 350 // LET OP de dubbele backslash: dit is een STRING die een RegExp wordt. Met een 351 // enkele \s eet de template literal de backslash op en sluit de klasse de 352 // LETTER s uit; "post-images" knapte dan af op de s en elke URL met een s 353 // erin werd half herschreven. Gevonden doordat de waarschuwing ".../media/po" 354 // meldde, afgekapt precies voor de s. 355 const patroon = new RegExp(`${bronOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(/media/[^"'\\s)<>]+)`, 'g'); 356 const gezien = new Set(); 357 let n = 0; 358 for (const m of [...inhoud.matchAll(patroon)]) { 359 const vol = m[0]; 360 if (gezien.has(vol)) continue; 361 gezien.add(vol); 362 const doel = bronMediaPad(vol, bronOrigin, { mediaRoot, path }); 363 if (!doel) { rapport.waarschuwingen.push(`onbruikbaar mediapad in tekst: ${vol}`); continue; } 364 let ok = false; 365 try { fs.statSync(doel.abs); ok = true; } catch { /* nog niet binnen */ } 366 if (!ok) { 367 const g = await haalBijlage(vol, { safeFetch, mediaRoot, fs, path, maxBytes, doel }).catch(() => null); 368 if (!g) { rapport.mediaMislukt++; rapport.waarschuwingen.push(`plaatje in tekst niet opgehaald: ${vol}`); continue; } 369 rapport.media++; 370 try { 371 db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?, ?, ?, ?, ?, ?)') 372 .run(crypto.randomUUID(), site.id, path.basename(doel.abs), g.mediaType, g.size, doel.abs); 373 } catch { /* administratie */ } 374 } 375 inhoud = inhoud.split(vol).join(doel.rel); 376 n++; 377 } 378 return { inhoud, n }; 305 379 } 306 380 … … 349 423 const bronActor = await getJson(site.slug, bron); 350 424 if (!bronActor || !bronActor.id) return { error: 'unreachable' }; 425 // De origin van de bron: alles op deze host onder /media/ is van hem en mag 426 // naar hetzelfde pad hier. Uit de actor-id, niet uit de invoer. 427 const bronOrigin = (() => { try { const u = new URL(bronActor.id); return `${u.protocol}//${u.host}`; } catch { return null; } })(); 351 428 if (bronActor.movedTo !== me) return { error: 'not_moved_here', movedTo: bronActor.movedTo || null }; 352 429 … … 366 443 367 444 const rapport = { 368 bron: bronActor.id, posts: 0, overgeslagen: 0, opnieuw: 0, media: 0, mediaMislukt: 0,445 bron: bronActor.id, posts: 0, overgeslagen: 0, opnieuw: 0, postsBijgewerkt: 0, media: 0, mediaMislukt: 0, 369 446 blocks: 0, tracksBinnen: 0, tracksMislukt: 0, tracksBijgewerkt: 0, overgeslagenTracks: 0, 370 447 playlistsBinnen: 0, playlistsMislukt: 0, waarschuwingen: [], … … 393 470 if (!bronActor.outbox) return { ...rapport, error: 'no_outbox' }; 394 471 let pagina = await getJson(site.slug, typeof bronActor.outbox === 'string' ? bronActor.outbox : bronActor.outbox.id); 395 if (pagina && pagina.first && !(pagina.orderedItems || pagina.items)) { 472 const verwacht = pagina && Number(pagina.totalItems) || null; 473 // Is er een `first`, dan ALTIJD de paginaketen volgen, ook als de kale 474 // collectie zelf items draagt. Klonkt zet daar een kopie van pagina 1 in 475 // (Pleroma eiste een first, en sindsdien staan ze er allebei), maar alleen 476 // echte pagina's dragen een `next`. Wie op de kale collectie blijft hangen 477 // verwerkt pagina 1 en denkt dan klaar te zijn: precies 18 van Robins 35 478 // berichten, zonder één waarschuwing. 479 if (pagina && pagina.first) { 396 480 pagina = await getJson(site.slug, typeof pagina.first === 'string' ? pagina.first : pagina.first.id); 397 481 } … … 422 506 // opnieuw op, dan komt het gewoon terug: er staat immers niets meer. 423 507 const id = ruwId(o.id) || crypto.randomUUID(); 424 if (db.prepare('SELECT 1 FROM posts WHERE id = ? AND site_id = ?').get(id, site.id)) { 508 const bestaand = db.prepare('SELECT id, content, cover_image_url FROM posts WHERE id = ? AND site_id = ?').get(id, site.id); 509 if (bestaand) { 510 // Niet alleen overslaan: REPAREREN wat een eerdere ronde liet liggen. 511 // Robins 18 posts stonden er al, met hotlinks naar de bron in de 512 // tekst en zonder cover. Een tweede ronde die dat ziet en passeert 513 // laat je met een site vol verwijzingen naar een domein dat 514 // opgezegd wordt. 515 if (safeFetch && fs && path && mediaRoot && bronOrigin && String(bestaand.content || '').includes(bronOrigin)) { 516 const r2 = await inhoudMediaBinnen(bestaand.content, bronOrigin, site, rapport, { safeFetch, mediaRoot, fs, path, maxBytes }); 517 if (r2.n) { 518 db.prepare('UPDATE posts SET content = ? WHERE id = ?').run(r2.inhoud, bestaand.id); 519 rapport.postsBijgewerkt++; 520 } 521 } 522 if (!bestaand.cover_image_url && safeFetch && fs && path && mediaRoot) { 523 // De cover alsnog: hij zit als bijlage op de Note. 524 for (const a of (Array.isArray(o.attachment) ? o.attachment : []).slice(0, 20)) { 525 const u = a && (typeof a === 'string' ? a : (a.url && (typeof a.url === 'string' ? a.url : a.url.href))); 526 if (!u || !AFBEELDING.test(String((a && a.mediaType) || ''))) continue; 527 const doel = bronMediaPad(u, bronOrigin, { mediaRoot, path }); 528 const g = await haalBijlage(String(u), { safeFetch, mediaRoot, fs, path, maxBytes, doel }).catch(() => null); 529 if (g) { 530 db.prepare('UPDATE posts SET cover_image_url = ? WHERE id = ?').run(g.url, bestaand.id); 531 rapport.media++; 532 rapport.postsBijgewerkt++; 533 } 534 break; 535 } 536 } 425 537 rapport.overgeslagen++; 426 538 continue; … … 433 545 const bijlagen = Array.isArray(o.attachment) ? o.attachment : []; 434 546 const binnen = []; 547 let inhoud = o.content || ''; 435 548 if (safeFetch && fs && path && mediaRoot) { 436 549 for (const a of bijlagen.slice(0, 20)) { 437 550 const u = a && (typeof a === 'string' ? a : (a.url && (typeof a.url === 'string' ? a.url : a.url.href))); 438 551 if (!u || !/^https?:\/\//i.test(String(u))) continue; 439 const g = await haalBijlage(String(u), { safeFetch, mediaRoot, fs, path, maxBytes }).catch(() => null); 552 const doel = bronMediaPad(u, bronOrigin, { mediaRoot, path }); 553 const g = await haalBijlage(String(u), { safeFetch, mediaRoot, fs, path, maxBytes, doel }).catch(() => null); 440 554 if (!g) { rapport.mediaMislukt++; rapport.waarschuwingen.push(`bijlage niet opgehaald: ${u}`); continue; } 441 555 binnen.push({ ...g, naam: (a && a.name) || null, type: (a && a.mediaType) || g.mediaType }); … … 446 560 } catch { /* media-rij is administratie, het bestand staat er */ } 447 561 } 562 // De PLAATJES IN DE TEKST. De gebakken content draagt absolute 563 // verwijzingen naar de bron (https://oud/media/...), en die bleven 564 // gewoon staan: elke afbeelding hotlinkte naar een domein dat je gaat 565 // opzeggen, en je eigen mediamap bleef leeg. Downloaden naar 566 // HETZELFDE pad en de verwijzing relatief maken; wat niet lukt blijft 567 // absoluut staan en wordt gemeld, want een lokale 404 is erger dan 568 // een hotlink. 569 const r2 = await inhoudMediaBinnen(inhoud, bronOrigin, site, rapport, { safeFetch, mediaRoot, fs, path, maxBytes }); 570 inhoud = r2.inhoud; 448 571 } 449 572 … … 451 574 const rest = binnen.filter((b) => b !== cover); 452 575 // De titel zit in de content, niet in een veld (zie titelUitContent). 453 const { titel, rest: body } = o.name ? { titel: o.name, rest: o.content || '' } : titelUitContent(o.content || '');576 const { titel, rest: body } = o.name ? { titel: o.name, rest: inhoud } : titelUitContent(inhoud); 454 577 // De slug uit de MENSELIJKE url, niet uit de AP-id. Zo houdt het bericht 455 578 // hetzelfde webadres als op de oude instantie, en blijft een link die … … 488 611 } 489 612 if (gezien >= max) rapport.waarschuwingen.push(`gestopt bij ${max} berichten, draai het nog eens voor de rest`); 613 // Silently minder ophalen dan de bron zegt te hebben is precies hoe 18 van 614 // de 35 wekenlang op "klaar" had kunnen staan. Tel na en zeg het. 615 if (verwacht && gezien < verwacht && gezien < max) { 616 rapport.waarschuwingen.push(`de bron meldt ${verwacht} items en er zijn er ${gezien} verwerkt; een pagina is mogelijk niet opgehaald, probeer het nog eens`); 617 } 490 618 491 619 // ── De muziekbibliotheek ──────────────────────────────────────
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)