Files
2026-07-13 12:46:20 +08:00

264 lines
7.8 KiB
JavaScript

/**
* Utility functions for npm installer
* Replaces download and phin packages with native Node.js http/https and tar
* Supports Node.js 16+
* Supports GITHUB_PROXY environment variable for proxying GitHub URLs
*/
const https = require('https')
const http = require('http')
const fs = require('fs')
const path = require('path')
const tar = require('tar')
// GitHub proxy support
const GITHUB_PROXY = process.env.GITHUB_PROXY || ''
/**
* Apply GitHub proxy to URLs if configured
* @param {string} url - Original URL
* @returns {string} - Proxy URL or original URL
*/
function applyProxy (url) {
if (!GITHUB_PROXY) return url
if (!url.includes('github.com')) return url
// Remove trailing slash from proxy if present
const proxy = GITHUB_PROXY.replace(/\/+$/, '')
// Ensure url has protocol
const urlWithProto = url.startsWith('http') ? url : `https://${url}`
return `${proxy}/${urlWithProto}`
}
/**
* Format bytes to human readable
*/
function formatBytes (bytes) {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
/**
* Make an HTTP GET request and download to a file with progress
* @param {string} url - URL to fetch
* @param {string} filepath - Destination file path
* @param {number} timeout - Request timeout in milliseconds (default: 300000 = 5min)
* @param {function} onProgress - Progress callback (received, total, percent)
* @returns {Promise<string>} Path to downloaded file
*/
function httpDownload (url, filepath, timeout = 300000, onProgress) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http
const req = client.get(url, { timeout }, (res) => {
// Handle redirects
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
if (res.headers.location) {
// Handle relative URLs
let redirectUrl = res.headers.location
if (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://')) {
const parsedUrl = new URL(url)
redirectUrl = `${parsedUrl.protocol}//${parsedUrl.host}${redirectUrl}`
}
resolve(httpDownload(redirectUrl, filepath, timeout, onProgress))
return
}
}
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Unknown error'}`))
return
}
const total = parseInt(res.headers['content-length'] || '0', 10)
let received = 0
let lastPercent = -1
const fileStream = fs.createWriteStream(filepath)
res.on('data', (chunk) => {
received += chunk.length
if (onProgress && total > 0) {
const percent = Math.round((received / total) * 100)
if (percent !== lastPercent) {
lastPercent = percent
onProgress(received, total, percent)
}
}
})
res.pipe(fileStream)
fileStream.on('finish', () => {
fileStream.close()
resolve(filepath)
})
fileStream.on('error', (err) => {
fs.unlink(filepath, () => {}) // Clean up partial download
reject(err)
})
})
req.on('error', reject)
req.on('timeout', () => {
req.destroy()
reject(new Error(`Request timeout after ${timeout}ms`))
})
})
}
/**
* Make an HTTP GET request and return response body as string
* @param {string} url - URL to fetch
* @param {number} timeout - Request timeout in milliseconds (default: 15000)
* @returns {Promise<string>} Response body as string
*/
function httpGet (url, timeout = 15000) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http
const req = client.get(url, { timeout }, (res) => {
// Handle redirects
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
if (res.headers.location) {
// Handle relative URLs
let redirectUrl = res.headers.location
if (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://')) {
const parsedUrl = new URL(url)
redirectUrl = `${parsedUrl.protocol}//${parsedUrl.host}${redirectUrl}`
}
resolve(httpGet(redirectUrl, timeout))
return
}
}
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Unknown error'}`))
return
}
const chunks = []
res.on('data', (chunk) => chunks.push(chunk))
res.on('end', () => {
const buffer = Buffer.concat(chunks)
resolve(buffer.toString())
})
res.on('error', reject)
})
req.on('error', reject)
req.on('timeout', () => {
req.destroy()
reject(new Error(`Request timeout after ${timeout}ms`))
})
})
}
/**
* Extract a tar.gz file to destination directory
* @param {string} filepath - Path to tar.gz file
* @param {string} dest - Destination directory
* @param {number} strip - Number of leading path components to strip (default: 0)
* @returns {Promise<void>}
*/
function extractTarGz (filepath, dest, strip = 0) {
return tar.extract({
file: filepath,
cwd: dest,
strip
})
}
/**
* Download and optionally extract a file with progress
* @param {string} url - URL to download from
* @param {string} dest - Destination directory
* @param {object} options - Options
* @param {boolean} options.extract - Whether to extract the file (default: true)
* @param {string} options.displayName - Display name for progress output
* @returns {Promise<{filepath: string, extracted: boolean}>}
*/
async function download (url, dest, { extract: doExtract = true, displayName } = {}) {
// Ensure dest directory exists
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true })
}
// Extract filename from URL
const urlParts = url.split('/')
const filename = urlParts[urlParts.length - 1].split('?')[0] || 'download'
const filepath = path.join(dest, filename)
// Apply proxy if configured
const downloadUrl = applyProxy(url)
const label = displayName || filename
const proxyInfo = GITHUB_PROXY ? ' [via proxy]' : ''
console.log('')
console.log(` Downloading: ${label}${proxyInfo}`)
let lastPercent = -1
await httpDownload(downloadUrl, filepath, 300000, (received, total, percent) => {
if (percent !== lastPercent && percent % 10 === 0) {
lastPercent = percent
const receivedStr = formatBytes(received)
const totalStr = formatBytes(total)
process.stdout.write(` Progress: ${percent}% (${receivedStr} / ${totalStr})\n`)
}
})
console.log(` Progress: 100% (${formatBytes(fs.statSync(filepath).size)})`)
console.log(' Download complete!')
let extracted = false
if (doExtract && (filepath.endsWith('.tar.gz') || filepath.endsWith('.tgz'))) {
console.log(' Extracting archive...')
await extractTarGz(filepath, dest)
// Clean up the downloaded archive
try {
fs.unlinkSync(filepath)
} catch (err) {
// Ignore cleanup errors
}
extracted = true
console.log(' Extraction complete!')
}
return { filepath, extracted }
}
/**
* Phin replacement - simple promisified HTTP client
* @param {object} options - Request options
* @param {string} options.url - URL to fetch
* @param {number} options.timeout - Request timeout (default: 15000)
* @returns {Promise<{body: Buffer, statusCode: number, headers: object}>}
*/
async function phin (options) {
const { url, timeout = 15000 } = options
const body = await httpGet(url, timeout)
return {
body: Buffer.from(body),
statusCode: 200,
headers: {}
}
}
phin.promisified = phin
module.exports = {
httpGet,
httpDownload,
extractTarGz,
download,
phin,
applyProxy,
formatBytes,
GITHUB_PROXY
}