the home site for me: also iteration 3 or 4 of my site

feat: add cdn mover

dunkirk.sh 8dfc8bda 76752ba3

verified
Changed files
+395 -7
scripts
+63
bun.lock
··· 4 4 "workspaces": { 5 5 "": { 6 6 "dependencies": { 7 + "@types/cli-progress": "^3.11.6", 8 + "cli-progress": "^3.12.0", 7 9 "dotenv": "^16.4.7", 8 10 "glob": "^13.0.0", 11 + "sharp": "^0.34.5", 9 12 }, 10 13 "devDependencies": { 11 14 "@types/bun": "latest", ··· 18 21 19 22 "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], 20 23 24 + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], 25 + 26 + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], 27 + 28 + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], 29 + 30 + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], 31 + 32 + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], 33 + 34 + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], 35 + 36 + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], 37 + 38 + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], 39 + 40 + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], 41 + 42 + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], 43 + 44 + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], 45 + 46 + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], 47 + 48 + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], 49 + 50 + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], 51 + 52 + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], 53 + 54 + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], 55 + 56 + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], 57 + 58 + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], 59 + 60 + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], 61 + 62 + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], 63 + 64 + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], 65 + 66 + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], 67 + 68 + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], 69 + 70 + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], 71 + 72 + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], 73 + 74 + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], 75 + 21 76 "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], 22 77 23 78 "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], ··· 27 82 "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], 28 83 29 84 "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 85 + 86 + "@types/cli-progress": ["@types/cli-progress@3.11.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA=="], 30 87 31 88 "@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="], 32 89 ··· 70 127 71 128 "chromium-bidi": ["chromium-bidi@0.11.0", "", { "dependencies": { "mitt": "3.0.1", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA=="], 72 129 130 + "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], 131 + 73 132 "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], 74 133 75 134 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], ··· 83 142 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 84 143 85 144 "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], 145 + 146 + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 86 147 87 148 "devtools-protocol": ["devtools-protocol@0.0.1367902", "", {}, "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg=="], 88 149 ··· 189 250 "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 190 251 191 252 "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 253 + 254 + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], 192 255 193 256 "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], 194 257
+4 -1
package.json
··· 9 9 "gen-og": "bun run scripts/genOG.ts" 10 10 }, 11 11 "dependencies": { 12 + "@types/cli-progress": "^3.11.6", 13 + "cli-progress": "^3.12.0", 12 14 "dotenv": "^16.4.7", 13 - "glob": "^13.0.0" 15 + "glob": "^13.0.0", 16 + "sharp": "^0.34.5" 14 17 }, 15 18 "devDependencies": { 16 19 "@types/bun": "latest",
+8 -6
scripts/preprocess.ts
··· 73 73 params.push(`class="${classes.join(' ')}"`); 74 74 } 75 75 76 - const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=["']?([^"'\s}]+)["']?/g); 77 - for (const [, key, value] of keyValueMatches) { 76 + const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g); 77 + for (const [, key, doubleQuoted, singleQuoted, unquoted] of keyValueMatches) { 78 78 if (key !== 'class') { 79 - params.push(`${key}="${value.replace(/["']/g, '')}"`); 79 + const value = doubleQuoted || singleQuoted || unquoted; 80 + params.push(`${key}="${value}"`); 80 81 } 81 82 } 82 83 } ··· 101 102 params.push(`class="${classes.join(' ')}"`); 102 103 } 103 104 104 - const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=["']?([^"'\s}]+)["']?/g); 105 - for (const [, key, value] of keyValueMatches) { 105 + const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g); 106 + for (const [, key, doubleQuoted, singleQuoted, unquoted] of keyValueMatches) { 106 107 if (key !== 'class') { 107 - params.push(`${key}="${value.replace(/["']/g, '')}"`); 108 + const value = doubleQuoted || singleQuoted || unquoted; 109 + params.push(`${key}="${value}"`); 108 110 } 109 111 } 110 112 }
+320
scripts/rehost.ts
··· 1 + #!/usr/bin/env bun 2 + 3 + import fs from "fs"; 4 + import path from "path"; 5 + import { glob } from "glob"; 6 + import cliProgress from "cli-progress"; 7 + import sharp from "sharp"; 8 + 9 + const UPLOAD_URL = "https://l4.dunkirk.sh/upload"; 10 + const AUTH_TOKEN = "crumpets"; 11 + const contentDir = process.argv[2] || "content"; 12 + const CONCURRENCY = 15; // Number of parallel uploads 13 + const MAX_DIMENSION = 1920; // Max dimension for either width or height 14 + 15 + interface ImageMatch { 16 + filePath: string; 17 + originalUrl: string; 18 + line: number; 19 + } 20 + 21 + interface UploadResult { 22 + url: string; 23 + newUrl: string | null; 24 + error?: string; 25 + } 26 + 27 + async function uploadImage( 28 + url: string, 29 + progressBar?: cliProgress.SingleBar, 30 + ): Promise<{ newUrl: string | null; error?: string }> { 31 + try { 32 + const response = await fetch(url, { 33 + signal: AbortSignal.timeout(30000), // 30 second timeout 34 + }); 35 + 36 + if (!response.ok) { 37 + progressBar?.increment(); 38 + return { 39 + newUrl: null, 40 + error: `Download failed: ${response.status} ${response.statusText}`, 41 + }; 42 + } 43 + 44 + const blob = await response.blob(); 45 + let buffer = Buffer.from(await blob.arrayBuffer()); 46 + 47 + // Get file extension from URL or content type 48 + const urlExt = url.split(".").pop()?.split("?")[0]; 49 + const contentType = response.headers.get("content-type") || ""; 50 + let ext = urlExt || "jpg"; 51 + 52 + if (contentType.includes("png")) ext = "png"; 53 + else if (contentType.includes("jpeg") || contentType.includes("jpg")) 54 + ext = "jpg"; 55 + else if (contentType.includes("gif")) ext = "gif"; 56 + else if (contentType.includes("webp")) ext = "webp"; 57 + 58 + // Resize image if it's too large 59 + try { 60 + const image = sharp(buffer); 61 + const metadata = await image.metadata(); 62 + 63 + if (metadata.width && metadata.height) { 64 + const maxDimension = Math.max(metadata.width, metadata.height); 65 + 66 + if (maxDimension > MAX_DIMENSION) { 67 + // Resize so the longest side is MAX_DIMENSION 68 + const isLandscape = metadata.width > metadata.height; 69 + buffer = await image 70 + .resize( 71 + isLandscape ? MAX_DIMENSION : undefined, 72 + isLandscape ? undefined : MAX_DIMENSION, 73 + { 74 + fit: 'inside', 75 + withoutEnlargement: true, 76 + } 77 + ) 78 + .toBuffer(); 79 + } 80 + } 81 + } catch (resizeError) { 82 + // If resize fails, continue with original buffer 83 + console.error(`\nWarning: Failed to resize ${url}:`, resizeError); 84 + } 85 + 86 + const filename = `image_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`; 87 + 88 + // Create form data 89 + const formData = new FormData(); 90 + const file = new File([buffer], filename, { type: contentType }); 91 + formData.append("file", file); 92 + 93 + const uploadResponse = await fetch(UPLOAD_URL, { 94 + method: "POST", 95 + headers: { 96 + Authorization: `Bearer ${AUTH_TOKEN}`, 97 + }, 98 + body: formData, 99 + signal: AbortSignal.timeout(30000), 100 + }); 101 + 102 + if (!uploadResponse.ok) { 103 + const errorText = await uploadResponse.text(); 104 + progressBar?.increment(); 105 + return { 106 + newUrl: null, 107 + error: `Upload failed: ${uploadResponse.status} ${uploadResponse.statusText} - ${errorText}`, 108 + }; 109 + } 110 + 111 + const result = await uploadResponse.json(); 112 + progressBar?.increment(); 113 + return { newUrl: result.url }; 114 + } catch (error) { 115 + progressBar?.increment(); 116 + return { 117 + newUrl: null, 118 + error: `Exception: ${error instanceof Error ? error.message : String(error)}`, 119 + }; 120 + } 121 + } 122 + 123 + async function processInBatches<T, R>( 124 + items: T[], 125 + batchSize: number, 126 + processor: (item: T) => Promise<R>, 127 + ): Promise<R[]> { 128 + const results: R[] = []; 129 + 130 + for (let i = 0; i < items.length; i += batchSize) { 131 + const batch = items.slice(i, i + batchSize); 132 + const batchResults = await Promise.all(batch.map(processor)); 133 + results.push(...batchResults); 134 + } 135 + 136 + return results; 137 + } 138 + 139 + function findImages(filePath: string): ImageMatch[] { 140 + const content = fs.readFileSync(filePath, "utf8"); 141 + const lines = content.split("\n"); 142 + const images: ImageMatch[] = []; 143 + 144 + for (let i = 0; i < lines.length; i++) { 145 + const line = lines[i]; 146 + 147 + // Find all image URLs in standard markdown: ![alt](url) or ![alt](url){attrs} 148 + const singleImageRegex = /!\[([^\]]*)\]\(([^)]+)\)(?:\{[^}]+\})?/g; 149 + let match; 150 + 151 + while ((match = singleImageRegex.exec(line)) !== null) { 152 + const url = match[2]; 153 + // Only process hel1 cdn URLs, skip gifs 154 + if ( 155 + url.includes("hc-cdn.hel1.your-objectstorage.com") && 156 + !url.toLowerCase().endsWith(".gif") 157 + ) { 158 + images.push({ 159 + filePath, 160 + originalUrl: url, 161 + line: i + 1, 162 + }); 163 + } 164 + } 165 + 166 + // Find all image URLs in multi-image format: !![alt1](url1)[alt2](url2){attrs} 167 + const multiImageRegex = /!!(\[([^\]]*)\]\(([^)]+)\))+(?:\{[^}]+\})?/g; 168 + while ((match = multiImageRegex.exec(line)) !== null) { 169 + const urlMatches = [...match[0].matchAll(/\[([^\]]*)\]\(([^)]+)\)/g)]; 170 + for (const urlMatch of urlMatches) { 171 + const url = urlMatch[2]; 172 + // Only process hel1 cdn URLs, skip gifs 173 + if ( 174 + url.includes("hc-cdn.hel1.your-objectstorage.com") && 175 + !url.toLowerCase().endsWith(".gif") 176 + ) { 177 + images.push({ 178 + filePath, 179 + originalUrl: url, 180 + line: i + 1, 181 + }); 182 + } 183 + } 184 + } 185 + } 186 + 187 + return images; 188 + } 189 + 190 + function replaceImageUrl( 191 + filePath: string, 192 + oldUrl: string, 193 + newUrl: string, 194 + ): void { 195 + let content = fs.readFileSync(filePath, "utf8"); 196 + 197 + // Replace all occurrences of the old URL with the new one 198 + // This handles both single and multi-image formats 199 + content = content.replaceAll(oldUrl, newUrl); 200 + 201 + fs.writeFileSync(filePath, content); 202 + } 203 + 204 + async function main() { 205 + const files = glob.sync(`${contentDir}/**/*.md`); 206 + const allImages: ImageMatch[] = []; 207 + 208 + // Find all images across all files 209 + for (const file of files) { 210 + const images = findImages(file); 211 + allImages.push(...images); 212 + } 213 + 214 + if (allImages.length === 0) { 215 + console.log("No external images found to rehost."); 216 + return; 217 + } 218 + 219 + const uniqueUrls = [...new Set(allImages.map((img) => img.originalUrl))]; 220 + console.log( 221 + `Found ${uniqueUrls.length} unique images to rehost (${allImages.length} total references)\n`, 222 + ); 223 + 224 + // Create progress bar 225 + const progressBar = new cliProgress.SingleBar({ 226 + format: 227 + "Uploading |{bar}| {percentage}% | {value}/{total} images | ETA: {eta}s", 228 + barCompleteChar: "\u2588", 229 + barIncompleteChar: "\u2591", 230 + hideCursor: true, 231 + }); 232 + 233 + progressBar.start(uniqueUrls.length, 0); 234 + 235 + // Process URLs in parallel with concurrency limit 236 + const urlMap = new Map<string, string>(); 237 + const failedUploads: { url: string; error: string }[] = []; 238 + 239 + const results = await processInBatches( 240 + uniqueUrls, 241 + CONCURRENCY, 242 + async (url) => { 243 + const result = await uploadImage(url, progressBar); 244 + return { url, ...result }; 245 + }, 246 + ); 247 + 248 + progressBar.stop(); 249 + 250 + // Build URL map and collect errors 251 + for (const { url, newUrl, error } of results) { 252 + if (newUrl) { 253 + urlMap.set(url, newUrl); 254 + } else if (error) { 255 + failedUploads.push({ url, error }); 256 + } 257 + } 258 + 259 + const successCount = urlMap.size; 260 + const failCount = uniqueUrls.length - successCount; 261 + 262 + if (failCount > 0) { 263 + console.log( 264 + `\n⚠️ Failed to upload ${failCount} image${failCount === 1 ? "" : "s"}`, 265 + ); 266 + 267 + // Group errors by type 268 + const errorGroups = new Map<string, string[]>(); 269 + for (const { url, error } of failedUploads) { 270 + const errorType = error.split(":")[0]; 271 + if (!errorGroups.has(errorType)) { 272 + errorGroups.set(errorType, []); 273 + } 274 + errorGroups.get(errorType)!.push(url); 275 + } 276 + 277 + console.log("\nError summary:"); 278 + for (const [errorType, urls] of errorGroups.entries()) { 279 + console.log( 280 + ` ${errorType}: ${urls.length} image${urls.length === 1 ? "" : "s"}`, 281 + ); 282 + if (urls.length <= 3) { 283 + urls.forEach((url) => console.log(` - ${url}`)); 284 + } else { 285 + urls.slice(0, 2).forEach((url) => console.log(` - ${url}`)); 286 + console.log(` ... and ${urls.length - 2} more`); 287 + } 288 + } 289 + } 290 + 291 + if (urlMap.size === 0) { 292 + console.log("\n❌ No images were successfully uploaded."); 293 + return; 294 + } 295 + 296 + // Replace URLs in files 297 + console.log( 298 + `\n✓ Successfully uploaded ${successCount} image${successCount === 1 ? "" : "s"}`, 299 + ); 300 + console.log("\nUpdating markdown files..."); 301 + 302 + let filesUpdated = 0; 303 + const updatedFiles = new Set<string>(); 304 + 305 + for (const [oldUrl, newUrl] of urlMap.entries()) { 306 + const affectedImages = allImages.filter( 307 + (img) => img.originalUrl === oldUrl, 308 + ); 309 + for (const img of affectedImages) { 310 + replaceImageUrl(img.filePath, oldUrl, newUrl); 311 + updatedFiles.add(img.filePath); 312 + } 313 + } 314 + 315 + console.log( 316 + `✓ Updated ${updatedFiles.size} file${updatedFiles.size === 1 ? "" : "s"}\n`, 317 + ); 318 + } 319 + 320 + main();