@lpb_name/down
Version:
A Node.js download manager with multi-threading support
237 lines (198 loc) • 6.62 kB
JavaScript
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import cliProgress from 'cli-progress';
class Timer {
constructor() {
this.startTime = 0;
this.endTime = 0;
}
start() {
this.startTime = Date.now();
}
stop() {
this.endTime = Date.now();
}
getDuration() {
return ((this.endTime - this.startTime) / 1000).toFixed(2);
}
}
export function createDownloadManager(config) {
const {
url,
output = path.basename(new URL(url).pathname),
threads = 3
} = config;
let totalSize = 0;
let downloadedBytes = 0;
const multibar = new cliProgress.MultiBar({
clearOnComplete: false,
hideCursor: true,
format: '{threadId} |{bar}| {percentage}% | {value_mb}/{total_mb}MB | {speed}',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
align: 'left',
autopadding: true,
forceRedraw: false,
barsize: 30,
stopOnComplete: true
});
let threadBars = [];
let mergeBar = null;
let isCompleted = false;
const totalTimer = new Timer();
const downloadTimer = new Timer();
const mergeTimer = new Timer();
async function getFileSize() {
const response = await axios.head(url);
// 只有多线程下载时才检查Range支持
if (threads > 1) {
const acceptRanges = response.headers['accept-ranges'];
if (!acceptRanges || acceptRanges.toLowerCase() !== 'bytes') {
throw new Error('服务器不支持分片下载 (Range 请求),请使用单线程下载');
}
}
const contentLength = response.headers['content-length'];
if (!contentLength) {
throw new Error('无法获取文件大小,服务器未提供 Content-Length 头');
}
return parseInt(contentLength);
}
async function downloadChunk(start, end, index, retries = 3) {
const chunkSize = end - start + 1;
let threadDownloaded = 0;
const threadBar = multibar.create(chunkSize, 0, {
threadId: `Thread ${String(index + 1).padStart(2, ' ')}`,
speed: 0,
value_mb: (0).toFixed(2),
total_mb: (chunkSize / (1024 * 1024)).toFixed(2)
});
threadBars[index] = threadBar;
const startTime = Date.now();
// 创建临时文件
const tempFile = `${output}.part${index}`;
for (let attempt = 0; attempt < retries; attempt++) {
try {
const writeStream = fs.createWriteStream(tempFile);
const requestConfig = {
url,
method: 'GET',
responseType: 'stream',
timeout: 30000 // 30秒超时
};
// 只有多线程下载时才使用Range请求
if (threads > 1) {
requestConfig.headers = {
Range: `bytes=${start}-${end}`
};
}
const response = await axios(requestConfig);
return new Promise((resolve, reject) => {
response.data.on('data', (chunk) => {
if (isCompleted) return;
downloadedBytes += chunk.length;
threadDownloaded += chunk.length;
const threadSpeed = (threadDownloaded / (Date.now() - startTime) * 1000 / 1024 / 1024).toFixed(2);
threadBar.update(threadDownloaded, {
speed: `速度: ${threadSpeed}MB/s`,
value_mb: (threadDownloaded / (1024 * 1024)).toFixed(2)
});
});
response.data.pipe(writeStream);
response.data.once('end', () => {
writeStream.end();
});
writeStream.once('finish', () => {
if (!isCompleted) {
// 确保进度条显示100%
threadBar.update(chunkSize, {
speed: '已完成',
value_mb: (chunkSize / (1024 * 1024)).toFixed(2)
});
}
resolve(tempFile);
});
response.data.once('error', (error) => {
reject(error);
});
writeStream.once('error', (error) => {
reject(error);
});
});
} catch (error) {
if (attempt === retries - 1) {
throw new Error(`分片 ${index + 1} 下载失败,已重试 ${retries} 次: ${error.message}`);
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
}
}
}
async function start() {
try {
totalTimer.start();
// 获取文件大小
totalSize = await getFileSize();
const chunkSize = Math.ceil(totalSize / threads);
downloadTimer.start();
// 创建下载任务
const tasks = [];
for (let i = 0; i < threads; i++) {
const start = i * chunkSize;
const end = i === threads - 1 ? totalSize - 1 : start + chunkSize - 1;
tasks.push(downloadChunk(start, end, i));
}
// 等待所有下载完成
const tempFiles = await Promise.all(tasks);
downloadTimer.stop();
// 标记为完成,停止所有进度条更新
isCompleted = true;
// 开始合并文件
mergeTimer.start();
mergeBar = multibar.create(threads, 0, {
threadId: 'Thread 合并 ',
value_mb: '0',
total_mb: threads.toString(),
speed: ''
}, {
format: '{threadId} |{bar}| {percentage}% | 已合并: {value_mb}/{total_mb}个分片'
});
// 合并临时文件
const finalWriteStream = fs.createWriteStream(output);
for (let i = 0; i < tempFiles.length; i++) {
const tempFile = tempFiles[i];
const readStream = fs.createReadStream(tempFile);
await new Promise((resolve, reject) => {
readStream.pipe(finalWriteStream, { end: false });
readStream.once('end', () => {
mergeBar.update(i + 1, {
value_mb: (i + 1).toString()
});
// 删除临时文件
fs.unlinkSync(tempFile);
resolve();
});
readStream.once('error', reject);
});
}
// 结束最终文件写入
finalWriteStream.end();
await new Promise(resolve => finalWriteStream.once('close', resolve));
mergeTimer.stop();
totalTimer.stop();
// 停止进度条显示
multibar.stop();
// 输出完成信息
console.log(`\n下载完成,耗时: ${downloadTimer.getDuration()}秒`);
console.log(`合并完成,耗时: ${mergeTimer.getDuration()}秒`);
console.log(`总耗时: ${totalTimer.getDuration()}秒`);
console.log(`\n下载完成!`);
} catch (error) {
multibar.stop();
throw error;
}
}
return {
start
};
}