| 38 | } |
| 39 | |
| 40 | function downloadFile(url, dest, visited = new Set()) { |
| 41 | return new Promise((resolve, reject) => { |
| 42 | try { |
| 43 | // Avoid infinite redirect loops |
| 44 | if (visited.has(url) || visited.size > 5) { |
| 45 | return reject(new Error('Too many redirects')); |
| 46 | } |
| 47 | visited.add(url); |
| 48 | |
| 49 | const useHttps = url.startsWith('https://'); |
| 50 | const client = useHttps ? require('https') : require('http'); |
| 51 | const req = client.get(url, { |
| 52 | headers: { |
| 53 | 'User-Agent': 'KnightBot-Updater/1.0', |
| 54 | 'Accept': '*/*' |
| 55 | } |
| 56 | }, res => { |
| 57 | // Handle redirects |
| 58 | if ([301, 302, 303, 307, 308].includes(res.statusCode)) { |
| 59 | const location = res.headers.location; |
| 60 | if (!location) return reject(new Error(`HTTP ${res.statusCode} without Location`)); |
| 61 | const nextUrl = new URL(location, url).toString(); |
| 62 | res.resume(); |
| 63 | return downloadFile(nextUrl, dest, visited).then(resolve).catch(reject); |
| 64 | } |
| 65 | |
| 66 | if (res.statusCode !== 200) { |
| 67 | return reject(new Error(`HTTP ${res.statusCode}`)); |
| 68 | } |
| 69 | |
| 70 | const file = fs.createWriteStream(dest); |
| 71 | res.pipe(file); |
| 72 | file.on('finish', () => file.close(resolve)); |
| 73 | file.on('error', err => { |
| 74 | try { file.close(() => {}); } catch {} |
| 75 | fs.unlink(dest, () => reject(err)); |
| 76 | }); |
| 77 | }); |
| 78 | req.on('error', err => { |
| 79 | fs.unlink(dest, () => reject(err)); |
| 80 | }); |
| 81 | } catch (e) { |
| 82 | reject(e); |
| 83 | } |
| 84 | }); |
| 85 | } |
| 86 | |
| 87 | async function extractZip(zipPath, outDir) { |
| 88 | // Try to use platform tools; no extra npm modules required |