+63
bun.lock
+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
+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
+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
+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:  or {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: ![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();