| 28 | // ── Helpers ─────────────────────────────────────────────────────────────────── |
| 29 | |
| 30 | function download(url, dest) { |
| 31 | return new Promise((resolve, reject) => { |
| 32 | console.log(`Downloading ${url} → ${dest}`) |
| 33 | const file = fs.createWriteStream(dest) |
| 34 | const request = (u) => { |
| 35 | https.get(u, { headers: { 'User-Agent': 'modly-build' } }, (res) => { |
| 36 | if (res.statusCode === 301 || res.statusCode === 302) { |
| 37 | request(res.headers.location) |
| 38 | return |
| 39 | } |
| 40 | if (res.statusCode !== 200) { |
| 41 | reject(new Error(`HTTP ${res.statusCode} for ${u}`)) |
| 42 | return |
| 43 | } |
| 44 | const total = parseInt(res.headers['content-length'] || '0', 10) |
| 45 | let received = 0 |
| 46 | res.on('data', (chunk) => { |
| 47 | received += chunk.length |
| 48 | if (total > 0) { |
| 49 | const pct = Math.round((received / total) * 100) |
| 50 | process.stdout.write(`\r ${pct}% (${Math.round(received / 1024 / 1024)} MB)`) |
| 51 | } |
| 52 | }) |
| 53 | res.pipe(file) |
| 54 | res.on('end', () => { |
| 55 | process.stdout.write('\n') |
| 56 | file.close(() => resolve()) |
| 57 | }) |
| 58 | }).on('error', reject) |
| 59 | } |
| 60 | request(url) |
| 61 | file.on('error', reject) |
| 62 | }) |
| 63 | } |
| 64 | |
| 65 | function extractTar(tarPath, destDir) { |
| 66 | console.log(`Extracting ${tarPath} → ${destDir}`) |