download-npm-backup.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. #!/usr/bin/env node
  2. /**
  3. * 从 registry 拉取完整 npm 包并解压到 nodejs/backup/npm(供离线恢复用)
  4. */
  5. const fs = require('fs');
  6. const path = require('path');
  7. const https = require('https');
  8. const zlib = require('zlib');
  9. const NPM_REGISTRY = 'https://registry.npmmirror.com';
  10. const repoNodejs = __dirname;
  11. const backupRoot = path.join(repoNodejs, 'backup');
  12. const npmBackupDir = path.join(backupRoot, 'npm');
  13. const extractDir = path.join(repoNodejs, 'npm-backup-extract');
  14. function isNpmBundleComplete(dir) {
  15. if (!fs.existsSync(path.join(dir, 'bin', 'npm-cli.js'))) return false;
  16. return fs.existsSync(path.join(dir, 'node_modules', 'graceful-fs', 'package.json'));
  17. }
  18. function get(url) {
  19. return new Promise((resolve, reject) => {
  20. const req = https.get(url, { headers: { 'User-Agent': 'Node/download-npm-backup' } }, (res) => {
  21. if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
  22. const loc = res.headers.location;
  23. return get(loc.startsWith('http') ? loc : new URL(loc, url).href).then(resolve).catch(reject);
  24. }
  25. const chunks = [];
  26. res.on('data', (c) => chunks.push(c));
  27. res.on('end', () => resolve(Buffer.concat(chunks)));
  28. res.on('error', reject);
  29. });
  30. req.on('error', reject);
  31. });
  32. }
  33. function extractTar(buffer, outDir) {
  34. let offset = 0;
  35. while (offset + 512 <= buffer.length) {
  36. const header = buffer.slice(offset, offset + 512);
  37. if (header.every((b) => b === 0)) break;
  38. const name = header.slice(0, 100).toString('utf8').replace(/\0/g, '');
  39. const size = parseInt(header.slice(124, 136).toString('utf8').trim(), 8) || 0;
  40. const typeflag = header[156] && header[156] !== 0 ? String.fromCharCode(header[156]) : '0';
  41. offset += 512;
  42. const content = buffer.slice(offset, offset + size);
  43. offset += Math.ceil(size / 512) * 512;
  44. if (!name || name.includes('..')) continue;
  45. const dest = path.join(outDir, name);
  46. if (typeflag === '5' || name.endsWith('/')) {
  47. if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
  48. } else if (size >= 0) {
  49. const dir = path.dirname(dest);
  50. if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  51. fs.writeFileSync(dest, content);
  52. }
  53. }
  54. }
  55. async function main() {
  56. fs.mkdirSync(backupRoot, { recursive: true });
  57. console.log('Fetching npm metadata from registry...');
  58. const meta = JSON.parse((await get(`${NPM_REGISTRY}/npm/latest`)).toString());
  59. const version = meta.version;
  60. const tarball = meta.dist?.tarball || `${NPM_REGISTRY}/npm/-/npm-${version}.tgz`;
  61. console.log('Downloading npm@' + version + '...');
  62. const tgz = await get(tarball);
  63. if (tgz.length < 1000 && tgz.toString().includes('<!')) {
  64. throw new Error('Registry returned HTML instead of tarball.');
  65. }
  66. if (tgz[0] !== 0x1f || tgz[1] !== 0x8b) {
  67. throw new Error('Downloaded file is not gzip.');
  68. }
  69. if (fs.existsSync(extractDir)) {
  70. try {
  71. fs.rmSync(extractDir, { recursive: true, force: true });
  72. } catch (_) {}
  73. }
  74. fs.mkdirSync(extractDir, { recursive: true });
  75. const tar = zlib.gunzipSync(tgz);
  76. extractTar(tar, extractDir);
  77. const pkgDir = path.join(extractDir, 'package');
  78. const unpacked = fs.existsSync(pkgDir) ? pkgDir : path.join(extractDir, fs.readdirSync(extractDir)[0]);
  79. if (fs.existsSync(npmBackupDir)) {
  80. console.log('Removing old nodejs/backup/npm...');
  81. fs.rmSync(npmBackupDir, { recursive: true, force: true });
  82. }
  83. fs.renameSync(unpacked, npmBackupDir);
  84. try {
  85. fs.rmSync(extractDir, { recursive: true, force: true });
  86. } catch (_) {}
  87. if (!isNpmBundleComplete(npmBackupDir)) {
  88. console.error('[X] Extracted npm backup failed integrity check (missing npm-cli or graceful-fs)');
  89. process.exit(1);
  90. }
  91. console.log('[OK] Full npm@' + version + ' saved to nodejs/backup/npm');
  92. }
  93. main().catch((err) => {
  94. console.error(err);
  95. process.exit(1);
  96. });