/** * Repairs the rows 0011 got wrong. 0011 took any path containing "favicon", * or ending in `.svg`, for an icon, so artwork such as * `/wp-content/uploads/2022/04/mir-logo-favicon.png` lost its `thumbnailUrl`, * and its dump showed as a pull-quote instead of an image in the journal * mosaic. * * This rule judges the file name and its folder instead. It spares * `mir-logo-favicon.png` and catches `icon_SEARCH.png`, which 0011 missed. * * @module */ import type { DatabaseSync } from "node:sqlite"; /** 0011's rule, copied as it shipped, to tell which rows it moved. */ function wasIconUrl0011(raw: string): boolean { let pathname: string; try { pathname = new URL(raw).pathname; } catch { return false; } return /favicon|apple-touch-icon|\/icons?\//i.test(pathname) || /\.(ico|svg)$/i.test(pathname); } /** * Checks whether a URL is a site icon. It is one when the file ends in `.ico`, * sits in a `favicon(s)` or `icon(s)` folder, or is named `favicon`, * `apple-touch-icon` or `icon`, alone or followed by a digit, `-`, `_` or `.`. */ function isIconUrl(raw: string): boolean { let pathname: string; try { pathname = new URL(raw).pathname; } catch { return false; } const segments = pathname.split("/").filter(Boolean); const filename = segments.pop() ?? ""; if (/\.ico$/i.test(filename)) return true; if (segments.some((s) => /^(favicons?|icons?)$/i.test(s))) return true; const basename = filename.replace(/\.[^.]+$/, ""); return /^(favicon|apple-touch-icon|icon)(?=$|[-_.\d])/i.test(basename); } /** * Run once by `runMigrations`. It moves a `faviconUrl` back to `thumbnailUrl` * only when 0011's rule takes it for an icon and this one doesn't. That also * catches the ones a later fetch stored that this rule doesn't recognise, such * as a `logo.svg`. It then moves the icons 0011 missed. */ export function up(db: DatabaseSync): void { const rows = db.prepare( `SELECT id, rich_content FROM dumps WHERE kind = 'url' AND rich_content IS NOT NULL;`, ).all() as { id: string; rich_content: string }[]; const update = db.prepare( `UPDATE dumps SET rich_content = ? WHERE id = ?;`, ); let restored = 0; let reclassified = 0; for (const row of rows) { let rich: { thumbnailUrl?: string; faviconUrl?: string }; try { rich = JSON.parse(row.rich_content); } catch { continue; } if ( rich.faviconUrl && !rich.thumbnailUrl && wasIconUrl0011(rich.faviconUrl) && !isIconUrl(rich.faviconUrl) ) { const { faviconUrl: _dropped, ...rest } = rich; update.run( JSON.stringify({ ...rest, thumbnailUrl: rich.faviconUrl }), row.id, ); restored++; continue; } if ( rich.thumbnailUrl && !rich.faviconUrl && isIconUrl(rich.thumbnailUrl) ) { const { thumbnailUrl: _dropped, ...rest } = rich; update.run( JSON.stringify({ ...rest, faviconUrl: rich.thumbnailUrl }), row.id, ); reclassified++; } } if (restored > 0 || reclassified > 0) { console.log( `[migrate] 0012: restored ${restored} thumbnail(s), reclassified ${reclassified} icon(s)`, ); } }