(url, timeoutMs = 8000, _depth = 0)
| 60 | } |
| 61 | |
| 62 | export function fetchImageUrl(url, timeoutMs = 8000, _depth = 0) { |
| 63 | if (_depth > MAX_REDIRECTS) return Promise.reject(new Error('Too many image redirects')); |
| 64 | validateImageUrl(url); |
| 65 | |
| 66 | return new Promise((resolve, reject) => { |
| 67 | let settled = false; |
| 68 | const done = (fn, val) => { if (!settled) { settled = true; fn(val); } }; |
| 69 | |
| 70 | const mod = url.startsWith('https') ? https : http; |
| 71 | const req = mod.get(url, { timeout: timeoutMs, headers: { 'Accept': 'image/*' }, lookup: safeLookup }, (res) => { |
| 72 | if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { |
| 73 | res.resume(); |
| 74 | return fetchImageUrl(res.headers.location, timeoutMs, _depth + 1).then( |
| 75 | v => done(resolve, v), e => done(reject, e) |
| 76 | ); |
| 77 | } |
| 78 | if (res.statusCode !== 200) { |
| 79 | res.resume(); |
| 80 | return done(reject, new Error(`Image fetch HTTP ${res.statusCode}`)); |
| 81 | } |
| 82 | const mime = (res.headers['content-type'] || '').split(';')[0].trim().toLowerCase(); |
| 83 | if (!MIME_OK.has(mime)) { |
| 84 | res.resume(); |
| 85 | return done(reject, new Error(`Unsupported image type: ${mime}`)); |
| 86 | } |
| 87 | const chunks = []; |
| 88 | let size = 0; |
| 89 | res.on('data', (d) => { |
| 90 | if (settled) return; |
| 91 | size += d.length; |
| 92 | if (size > MAX_SIZE) { res.destroy(); done(reject, new Error(`Image exceeds ${MAX_SIZE} bytes`)); } |
| 93 | else chunks.push(d); |
| 94 | }); |
| 95 | res.on('end', () => done(resolve, { base64_data: Buffer.concat(chunks).toString('base64'), mime_type: mime })); |
| 96 | res.on('error', (e) => done(reject, e)); |
| 97 | }); |
| 98 | req.on('error', (e) => done(reject, e)); |
| 99 | req.on('timeout', () => { req.destroy(); done(reject, new Error('Image fetch timeout')); }); |
| 100 | }); |
| 101 | } |
| 102 | |
| 103 | export async function extractImages(contentBlocks) { |
| 104 | if (!Array.isArray(contentBlocks)) return { text: String(contentBlocks ?? ''), images: [] }; |
no test coverage detected