方案一:通过Electron的DownloadItem
- 支持点击链接下载
- 支持输入文件名称和选择下载路径后,手动下载
typescript
复制代码
import { IpcEvents } from '@interfaces/ipc-events'
import { DownLoadFile } from '@interfaces/types'
import { DownloadFileStore, getUniqueFilename } from '@utils/download'
import { app, dialog, ipcMain, session } from 'electron'
import { existsSync, unlinkSync } from 'node:fs'
import { resolve } from 'node:path'
import { v4 as uuidv4 } from 'uuid'
import { WindowsManager } from './windows'
/**
* 设置基于 Electron DownloadItem 的文件下载管理。
* 监听会话的 'will-download' 事件来捕获下载项,
* 并为每个下载项设置事件监听器以跟踪其状态和计算速度。
* 同时注册 IPC 处理程序来控制下载(开始、取消、暂停、恢复)。
*/
export const setupDownloadFileByDownloadItem = () => {
// 存储下载记录的实例
const recordStore = new DownloadFileStore()
// 存储下载项对象的映射,键为下载记录的唯一标识符
const downloadItemMap = new Map<string, Electron.DownloadItem>()
const windowsManager = WindowsManager.getInstance()
const pendingDownloadOptions = new Map<string, { savePath?: string; filename?: string }>()
// 定义一个接口来存储速度计算所需的历史数据
interface SpeedCalculationData {
lastTimestamp: number // 上次采样时间戳 (毫秒)
lastBytesReceived: number // 上次采样时已接收的字节数
// 可以扩展以存储更多历史点用于平滑计算
}
// 使用 Map 来存储每个下载项的速度计算数据
const speedDataMap = new Map<string, SpeedCalculationData>()
/**
* 监听会话的 'will-download' 事件。
* 当一个新的下载请求被发起时,此事件会被触发。
* @param _event - Electron 事件对象
* @param item - Electron DownloadItem 对象,代表当前的下载任务
*/
session.defaultSession.on('will-download', async (_event, item): Promise<boolean | void> => {
const url = item.getURL()
const options = pendingDownloadOptions.get(url)
let savePathToUse = app.getPath('downloads') // 默认保存路径为下载文件夹
let filenameToUse = item.getFilename() // 默认使用服务器提供的文件名
if (options) {
savePathToUse = options.savePath || app.getPath('downloads')
filenameToUse = options.filename || item.getFilename()
}
filenameToUse = getUniqueFilename(savePathToUse, filenameToUse)
const filePath = resolve(savePathToUse, filenameToUse)
item.setSavePath(filePath) // 设置下载文件的保存路径
// 创建一个新的下载记录对象
const record: DownLoadFile.DownloadRecord = {
key: uuidv4(), // 使用 uuid 生成唯一标识符
url, // 获取下载链接
filename: filenameToUse, // 获取文件名
status: 'no-start', // 初始状态为未开始
savePath: savePathToUse, // 获取保存路径 (可能为空,由对话框决定)
fileSize: item.getTotalBytes(), // 获取文件总大小
downloadedBytes: 0, // 已下载字节数初始化为 0
speedBytesPerSecond: 0 // 初始速度为 0
}
// 将新记录添加到存储中
recordStore.addNewDownloadRecord(record)
// 将 DownloadItem 对象存入 Map,方便后续通过 key 查找
downloadItemMap.set(record.key, item)
// 为这个特定的下载项设置事件监听器
setupDownloadEvents(record.key)
sendDownloadRecords()
})
const sendDownloadRecords = () => {
windowsManager.receiveDownloadInfoWindows.forEach((win) => {
win.webContents.send(IpcEvents.SEND_DOWNLOAD_RECORDS, recordStore.downloadRecords)
})
}
ipcMain.handle(IpcEvents.OPEN_FILE_DIALOG, async (_, oldPath?: string) => {
oldPath = oldPath ? oldPath : recordStore.downloadFileDir
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
filters: [
{
name: 'All Files',
extensions: ['*']
}
],
title: '选择保存位置',
defaultPath: oldPath
})
if (canceled || !filePaths || filePaths.length === 0) {
return oldPath
} else {
const newPath = filePaths[0]
recordStore.downloadFileDir = newPath
return newPath
}
})
ipcMain.handle(IpcEvents.GET_DOWNLOAD_RECORDS, () => {
return recordStore.downloadRecords
})
ipcMain.handle(IpcEvents.DELETE_DOWNLOAD, (_event, record: DownLoadFile.DownloadRecord) => {
recordStore.removeDownloadRecord(record.key)
const filePath = record.savePath
if (existsSync(filePath)) {
unlinkSync(filePath)
}
sendDownloadRecords()
return true
})
ipcMain.handle(IpcEvents.DOWNLOAD_FILE, (_event, options: DownLoadFile.DownloadOptions) => {
const { savePath, url, filename } = options
pendingDownloadOptions.set(url, { savePath, filename })
session.defaultSession.downloadURL(url, {})
})
/**
* 为指定 key 的 DownloadItem 设置事件监听器 ('updated' 和 'done')。
* @param key - 下载记录的唯一标识符
*/
const setupDownloadEvents = (key: string) => {
const item = downloadItemMap.get(key)
if (item) {
// 初始化该下载项的速度计算数据
speedDataMap.set(key, {
lastTimestamp: Date.now(),
lastBytesReceived: 0 // 初始为 0
})
/**
* 监听 DownloadItem 的 'updated' 事件。
* 当下载进度更新或状态改变时触发。
* @param _ - Electron 事件对象
* @param state - 下载状态 ('progressing', 'interrupted')
*/
item.on('updated', (_event, state) => {
const record = recordStore.findDownloadRecord(key)
if (!record) return // 安全检查
if (state === 'interrupted') {
// 如果下载被中断,更新记录状态为暂停或取消
const status: DownLoadFile.DownloadRecord['status'] = item.isPaused()
? 'paused'
: 'cancelled'
recordStore.updateDownloadRecord(key, {
status: status,
speedBytesPerSecond: 0, // 中断时速度归零
savePath: item.savePath
})
// 可以选择在此清理 speedDataMap,但如果用户可能恢复,则保留
// speedDataMap.delete(key);
} else if (state === 'progressing') {
// 如果下载正在进行,更新记录的状态、已下载字节数等信息
const currentBytes = item.getReceivedBytes()
const totalBytes = item.getTotalBytes()
const currentTimestamp = Date.now()
let speedBytesPerSecond = 0
const speedData = speedDataMap.get(key)
if (speedData) {
const timeDiffSeconds = (currentTimestamp - speedData.lastTimestamp) / 1000 // 转换为秒
// 避免除以零或负数时间差
if (timeDiffSeconds > 0) {
const bytesDiff = currentBytes - speedData.lastBytesReceived
// 计算瞬时速度 (字节/秒)
speedBytesPerSecond = bytesDiff / timeDiffSeconds
}
// 更新速度计算数据为当前值,供下次计算使用
speedData.lastTimestamp = currentTimestamp
speedData.lastBytesReceived = currentBytes
} else {
// 如果没有速度数据(理论上不应该发生,但做安全检查)
// 重新初始化
speedDataMap.set(key, {
lastTimestamp: currentTimestamp,
lastBytesReceived: currentBytes
})
}
// 更新记录,包括速度
recordStore.updateDownloadRecord(key, {
status: 'progressing',
downloadedBytes: currentBytes,
fileSize: totalBytes, // 文件大小可能在下载过程中更新
speedBytesPerSecond: Math.max(0, Math.round(speedBytesPerSecond)), // 确保非负,并四舍五入为整数 B/s
savePath: item.savePath
})
sendDownloadRecords()
}
})
/**
* 监听 DownloadItem 的 'done' 事件。
* 当下载完成、取消或失败时触发。
*/
item.on('done', (_event, state) => {
// 注意:Electron 30+ 'done' 事件回调签名已更改,包含 state 参数
// state 可能是 'completed', 'cancelled', 'interrupted'
// 为了兼容性和准确性,使用 state 参数
const record = recordStore.findDownloadRecord(key)
console.log(item.getSavePath(), item.getFilename())
if (record) {
let finalStatus: DownLoadFile.DownloadRecord['status'] = 'interrupted' // 默认
if (state === 'completed') {
finalStatus = 'complete'
} else if (state === 'cancelled') {
finalStatus = 'cancelled'
} else {
// 'interrupted' or unknown
finalStatus = 'interrupted'
}
// 根据 DownloadItem 的最终状态更新记录
recordStore.updateDownloadRecord(key, {
status: finalStatus,
downloadedBytes: item.getReceivedBytes(),
fileSize: item.getTotalBytes(),
speedBytesPerSecond: 0 // 完成后速度为 0
})
}
// 下载结束后,从 Maps 中移除对应的引用
downloadItemMap.delete(key)
speedDataMap.delete(key) // 清除速度计算数据
sendDownloadRecords()
recordStore.persistRecords()
})
}
}
/**
* 监听 IPC 事件 IpcEvents.DOWNLOAD_FILE。
* 当渲染进程请求开始下载一个文件时触发。
* @param _event - IPC 事件对象
* @param options - 下载选项,包含 URL
*/
ipcMain.on(IpcEvents.DOWNLOAD_FILE, (_event, options: DownLoadFile.DownloadOptions) => {
// 调用 session 的 downloadURL 方法开始下载
session.defaultSession.downloadURL(options.url)
})
/**
* 处理 IPC 事件 IpcEvents.CANCEL_DOWNLOAD。
* 当渲染进程请求取消一个下载时触发。
* @param _event - IPC 事件对象
* @param key - 要取消的下载记录的唯一标识符
* @returns Promise<boolean> - 操作结果 (总是返回 true)
*/
ipcMain.handle(IpcEvents.CANCEL_DOWNLOAD, async (_event, key: string) => {
const item = downloadItemMap.get(key)
if (item) {
// 调用 DownloadItem 的 cancel 方法取消下载
item.cancel()
}
// 返回操作结果
return true
})
/**
* 处理 IPC 事件 IpcEvents.PAUSE_DOWNLOAD。
* 当渲染进程请求暂停一个下载时触发。
* @param _event - IPC 事件对象
* @param key - 要暂停的下载记录的唯一标识符
*/
ipcMain.handle(IpcEvents.PAUSE_DOWNLOAD, async (_event, key: string) => {
const item = downloadItemMap.get(key)
if (item) {
// 调用 DownloadItem 的 pause 方法暂停下载
item.pause()
}
// 无返回值
})
/**
* 处理 IPC 事件 IpcEvents.RESUME_DOWNLOAD。
* 当渲染进程请求恢复一个下载时触发。
* @param _event - IPC 事件对象
* @param key - 要恢复的下载记录的唯一标识符
*/
ipcMain.handle(IpcEvents.RESUME_DOWNLOAD, async (_event, key: string) => {
const item = downloadItemMap.get(key)
if (item) {
// 如果 DownloadItem 存在,直接调用 resume 方法恢复下载
item.resume()
// 恢复时重置速度计算的基准点,以避免速度突增
const speedData = speedDataMap.get(key)
if (speedData) {
const now = Date.now()
speedData.lastTimestamp = now
speedData.lastBytesReceived = item.getReceivedBytes()
}
return
}
// 如果 DownloadItem 不存在(可能因为应用重启),但记录存在且状态为暂停
const record = recordStore.findDownloadRecord(key)
if (record && !item && record.status === 'paused') {
// 检查本地是否已存在部分下载的文件
const filePath = resolve(record.savePath)
if (existsSync(filePath)) {
// 如果存在,先删除它,然后重新开始下载
unlinkSync(filePath)
}
// 重新调用 session 的 downloadURL 方法开始下载
session.defaultSession.downloadURL(record.url)
}
// 无返回值
})
/**
* 监听 app 的 'before-quit' 事件。
* 在应用退出前,取消所有正在进行的下载任务。
*/
app.on('before-quit', () => {
// 遍历所有 DownloadItem 并调用 cancel 方法
downloadItemMap.forEach((item) => {
item.cancel()
})
// 清理速度数据 Map
speedDataMap.clear()
recordStore.persistRecords()
})
}
方案二:通过worker_thread方案,基于nodejs相关网络库的流式下载
- 拦截默认下载
- 支持手动下载
- 支持应用退出后续传
- 扩展性更强
主进程
typescript
复制代码
// setupDownloadFileV2.ts
import { app, ipcMain, session } from 'electron'
import { IpcEvents } from '@interfaces/ipc-events'
import { DownloadFileStore, DownloadWorkerManager } from '@utils/download'
import type { DownLoadFile } from '@interfaces/types'
import { v4 as uuidV4 } from 'uuid'
import { WindowsManager } from './windows'
import { debounce } from 'lodash'
export async function setupDownloadFileV2() {
await app.whenReady()
const recordStore = new DownloadFileStore()
const downloadWorkerManager = new DownloadWorkerManager()
session.defaultSession.on('will-download', (event, item) => {
// electron的默认下载行为,这里拦截并阻止,使用自定义下载行为
const filename = item.getFilename()
const url = item.getURL()
const savePath = item.savePath || app.getPath('downloads')
handleDownload({ url, savePath, filename: filename })
event.preventDefault()
return false
})
ipcMain.handle(IpcEvents.GET_DOWNLOAD_RECORDS, () => {
return recordStore.downloadRecords
})
// 开启一个下载任务
ipcMain.handle(IpcEvents.DOWNLOAD_FILE, async (_event, options: DownLoadFile.DownloadOptions) => {
handleDownload(options)
})
// 继续下载 todo : 断点续传
ipcMain.handle(IpcEvents.RESUME_DOWNLOAD, async (_event, record: DownLoadFile.DownloadRecord) => {
handleResume(record)
})
// 取消下载
ipcMain.handle(IpcEvents.CANCEL_DOWNLOAD, async (event, record: DownLoadFile.DownloadRecord) => {
const worker = downloadWorkerManager.getWorker(record.key)
if (worker) {
worker.postMessage({ type: 'cancel', data: { key: record.key } })
}
event.sender.send(IpcEvents.SEND_DOWNLOAD_RECORDS, recordStore.downloadRecords)
})
// 暂停下载
ipcMain.handle(IpcEvents.PAUSE_DOWNLOAD, async (_event, record: DownLoadFile.DownloadRecord) => {
const worker = downloadWorkerManager.getWorker(record.key)
if (worker) {
worker.postMessage({ type: 'pause', data: { key: record.key } })
}
})
ipcMain.handle(IpcEvents.DELETE_DOWNLOAD, (_event, record: DownLoadFile.DownloadRecord) => {
const worker = downloadWorkerManager.getWorker(record.key)
if (worker) {
worker.postMessage({ type: 'cancel', data: { key: record.key } })
}
recordStore.removeDownloadRecord(record.key)
WindowsManager.getInstance().receiveDownloadInfoWindows.forEach((window) => {
window.webContents.send(IpcEvents.SEND_DOWNLOAD_RECORDS, recordStore.downloadRecords)
})
})
const messageHandler = debounce((message: DownLoadFile.DownloadFileResult) => {
const { key, ...rest } = message.data
recordStore.updateDownloadRecord(key, rest)
if (message.type === 'progress') {
WindowsManager.getInstance().receiveDownloadInfoWindows.forEach((window) => {
window.webContents.send(IpcEvents.DOWNLOAD_FILE_PROGRESS, message.data)
})
}
if (['error', 'cancelled', 'paused', 'complete'].includes(message.type)) {
downloadWorkerManager.removeWorker(message.data.key)
}
if (['error', 'cancelled', 'paused', 'complete', 'begin'].includes(message.type)) {
WindowsManager.getInstance().receiveDownloadInfoWindows.forEach((window) => {
window.webContents.send(IpcEvents.SEND_DOWNLOAD_RECORDS, recordStore.downloadRecords)
})
}
})
const handleResume = (record: DownLoadFile.DownloadRecord) => {
downloadWorkerManager.createWorker({
action: 'resume',
record
})
downloadWorkerManager.on('message', (message: DownLoadFile.DownloadFileResult) => {
messageHandler(message)
})
}
const handleDownload = (options: DownLoadFile.DownloadOptions) => {
const record: DownLoadFile.DownloadRecord = {
url: options.url,
savePath: options.savePath ?? app.getPath('downloads'),
key: uuidV4(),
filename: options.filename || '',
// 初次创建时,文件大小为0,下载状态为未开始,后续通过网络获取
fileSize: 0,
status: 'no-start',
downloadedBytes: 0
}
recordStore.addNewDownloadRecord(record)
downloadWorkerManager.createWorker({
action: 'download',
record
})
downloadWorkerManager.on('message', (message) => {
messageHandler(message)
})
}
}
worker
typescript
复制代码
import { DownLoadFile } from '@interfaces/types'
import { workerData, parentPort } from 'node:worker_threads'
import axios, { AxiosResponse } from 'axios'
import { basename, dirname, join, parse } from 'node:path'
import { createWriteStream, existsSync, mkdirSync, statSync, unlinkSync } from 'node:fs'
import { IncomingMessage } from 'node:http'
import { EventEmitter } from 'node:events'
class DownloadWorker extends EventEmitter {
private supportsRange = true
private abortCtrl = new AbortController()
private currentState: 'running' | 'paused' | 'cancelled' = 'running'
static DOWN_CHUNK_SIZE = 1024 * 1024
constructor(private record: DownLoadFile.DownloadRecord) {
super()
}
get key() {
return this.record.key
}
get filename() {
return this.record.filename
}
set filename(value: string) {
this.record.filename = value
}
get savePath() {
return this.record.savePath
}
get url() {
return this.record.url
}
get fileSize() {
return this.record.fileSize
}
set fileSize(value: number) {
this.record.fileSize = value
}
get downloadedBytes() {
return this.record.downloadedBytes
}
set downloadedBytes(value: number) {
this.record.downloadedBytes = value
}
async beginDownload() {
try {
this.currentState = 'running'
await this.ensureFileBaseInfo()
await this.download()
} catch (err: any) {
if (axios.isCancel(err)) {
console.log(`Download cancelled for ${this.key}`)
} else {
console.error(`Download failed for ${this.key}:`, err.message)
this.emit('error', err)
}
}
}
cancel() {
if (this.currentState === 'cancelled') {
console.warn(`Download ${this.key} is already cancelled`)
return
}
this.currentState = 'cancelled'
this.abortCtrl.abort()
this.deleteFile()
}
pause() {
if (this.currentState !== 'running') {
console.warn(`Cannot pause download ${this.key}, current state is ${this.currentState}`)
return
}
this.currentState = 'paused'
this.abortCtrl.abort()
}
/**
*
* @description 恢复下载,备用api,为了后续暂停时不退出worker时使用
*/
async resume() {
this.currentState = 'running'
this.abortCtrl = new AbortController()
if (existsSync(this.filePath)) {
this.downloadedBytes = statSync(this.filePath).size
this._download()
} else {
// 如果文件不存在,重新开始下载
await this.beginDownload()
}
}
get filePath() {
return join(this.savePath, this.filename)
}
/**
* @description 删除文件,在取消下载是会调用
*/
deleteFile() {
const filepath = this.filePath
if (existsSync(filepath)) {
unlinkSync(filepath)
}
}
async download() {
const dir = dirname(this.savePath)
const filepath = this.filePath
if (existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}
if (existsSync(filepath)) {
const stats = statSync(filepath)
this.downloadedBytes = stats.size
if (this.downloadedBytes === this.fileSize) {
this.sendComplete()
return
}
// 如果本地文件大于远程文件,可能有问题,从头开始
if (this.downloadedBytes > this.fileSize) {
unlinkSync(filepath)
this.downloadedBytes = 0
}
}
console.log(`Starting/Resuming download for ${this.key} from byte ${this.downloadedBytes}`)
console.log(filepath, this.filename)
this._download()
}
private async _download() {
const writeStream = createWriteStream(this.filePath, {
flags: this.downloadedBytes === 0 ? 'w' : 'a'
})
try {
const response: AxiosResponse<IncomingMessage> = await axios.get(this.url, {
responseType: 'stream',
headers: {
Range: this.supportsRange ? `bytes=${this.downloadedBytes}-` : undefined
},
signal: this.abortCtrl.signal
})
const downloadStream = response.data
downloadStream.on('data', (chunk) => {
if (this.currentState !== 'running') return
this.downloadedBytes += chunk.length
this.sendProgress()
})
downloadStream.on('error', (err) => {
// 尝试关闭写入流
writeStream.destroy(err)
if (this.currentState === 'paused') {
this.sendPaused()
} else if (this.currentState === 'cancelled') {
this.sendCancelled()
} else {
console.error(`Download stream failed for key ${this.key}:`, err.message)
this.sendError(err.message)
}
})
writeStream.on('error', (err) => {
console.error(`Write stream error for key ${this.key}:`, err.message)
if (downloadStream) {
downloadStream.destroy()
}
if (this.currentState === 'paused') {
this.sendPaused()
} else if (this.currentState === 'cancelled') {
this.sendCancelled()
} else {
this.sendError((err as Error).message)
}
})
writeStream.on('finish', () => {
if (this.currentState === 'running') {
console.log(`Download completed for key ${this.key}: ${this.filePath}`)
this.sendComplete()
}
})
downloadStream.pipe(writeStream)
} catch (err: any) {
console.log(`Download failed for key ${this.key}: ${err.message}`)
if (this.currentState === 'paused') {
this.sendPaused()
} else if (this.currentState === 'cancelled') {
this.sendCancelled()
} else {
this.sendError(err.message)
}
}
}
private getFileNameFromContentDispositionOrUrl(
contentDisposition: string | undefined,
url: string
) {
if (contentDisposition) {
let filename = this.extractFilenameFromContentDisposition(contentDisposition)
if (filename) {
return filename
}
}
const filename = this.getFileNameFromUrl(url)
if (filename) {
return filename
}
// 返回默认名称
return Date.now().toString()
}
getFileNameFromUrl(fileUrl: string) {
try {
const parseUrl = new URL(fileUrl)
const pathname = parseUrl.pathname
let filename = basename(pathname)
// 去除可能的末尾 '/'
filename = filename.replace(/\/+$/, '')
// 如果路径是像 /download/123 这样的 ID,basename 可能无扩展名
// 但至少返回它
if (filename) {
return filename
}
return filename
} catch (e) {
console.log('get filename from url error')
}
return null
}
extractFilenameFromContentDisposition(contentDisposition: string): string | null {
// 匹配 filename="..."
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/i)
if (filenameMatch) {
let filename = filenameMatch[1]
// 移除包裹的引号
if (filename.startsWith('"') && filename.endsWith('"')) {
filename = filename.slice(1, -1)
}
return decodeURIComponent(filename.trim())
}
// 匹配 filename*=UTF-8''...
const encodedFilenameMatch = contentDisposition.match(
/filename[^;=\n]*\*=(?:UTF-8|utf-8)''(.+)/i
)
if (encodedFilenameMatch) {
try {
// URL 解码(处理百分号编码)
return decodeURIComponent(encodedFilenameMatch[1])
} catch (e) {
console.warn('Failed to decode URI component:', encodedFilenameMatch[1])
return null
}
}
return null
}
getUniqueFilename(
directory: string,
filename: string,
suffixTemplate: string = ' (%d)' // 可自定义模板
): string {
const { name, ext } = parse(filename)
let candidate = filename
let counter = 1
while (existsSync(join(directory, candidate))) {
candidate = `${name}${suffixTemplate.replace('%d', counter.toString())}${ext}`
counter++
}
return candidate
}
initFilename(response: AxiosResponse) {
this.filename = this.filename
? this.filename
: this.getFileNameFromContentDispositionOrUrl(
response.headers['content-disposition'],
this.url
)
this.filename = this.getUniqueFilename(this.savePath, this.filename)
}
async ensureFileBaseInfo() {
try {
const response = await axios.head(this.url)
if (response.status >= 200 && response.status < 300) {
this.initFilename(response)
const contentLength = response.headers['content-length']
if (contentLength) {
this.fileSize = Number(contentLength)
this.supportsRange = response.headers['accept-ranges'] === 'bytes'
console.log(
`File size for ${this.key}: ${this.fileSize}, Supports Range: ${this.supportsRange}`
)
this.sendBegin()
} else {
throw new Error('Content-Length header not found in HEAD response')
}
} else if (response.status === 404) {
throw new Error('File not found (404)')
} else {
throw new Error(`Failed to get file info, status: ${response.status}`)
}
} catch (err: any) {
console.warn(`HEAD request failed for ${this.key}, trying GET for headers:`, err.message)
try {
const getResponse = await axios.get(this.url, {
responseType: 'stream'
})
this.initFilename(getResponse)
const contentLength = getResponse.headers['content-length']
if (contentLength) {
this.fileSize = Number(contentLength)
this.supportsRange = getResponse.headers['accept-ranges'] === 'bytes'
console.log(
`(GET fallback) File size for ${this.key}: ${this.fileSize}, Supports Range: ${this.supportsRange}`
)
getResponse.data.destroy()
this.sendBegin()
} else {
getResponse.data.destroy()
throw new Error('Content-Length header not found in GET response (fallback)')
}
} catch (getErr: any) {
console.error(`GET request (fallback) also failed for ${this.key}:`, getErr.message)
throw new Error(
`Could not determine file size: HEAD failed (${err.message}), GET fallback failed (${getErr.message})`
)
}
}
}
sendPaused() {
this.record = { ...this.record, status: 'paused' }
const event: DownLoadFile.DownloadFileResult = {
type: 'paused',
data: this.record
}
this.emit('paused', event)
}
sendCancelled() {
this.record = { ...this.record, status: 'cancelled' }
const event: DownLoadFile.DownloadFileResult = {
type: 'cancelled',
data: this.record
}
this.emit('cancelled', event)
}
sendBegin() {
this.record = { ...this.record, status: 'progressing' }
const event: DownLoadFile.DownloadFileResult = {
type: 'begin',
data: this.record
}
this.emit('begin', event)
}
sendError(err: string) {
this.record = { ...this.record, status: 'error' }
const event: DownLoadFile.DownloadFileResult = {
type: 'error',
data: this.record,
message: err
}
this.emit('error', event)
}
sendComplete() {
this.record = { ...this.record, status: 'complete' }
const event: DownLoadFile.DownloadFileResult = {
type: 'complete',
data: this.record
}
this.emit('complete', event)
}
sendProgress() {
this.record = { ...this.record, status: 'progressing' }
const event: DownLoadFile.DownloadFileResult = {
type: 'progress',
data: this.record
}
this.emit('progress', event)
}
}
;(async (data: DownLoadFile.DownloadFileWorkerData) => {
const { action, record } = data
const downloadWorker = new DownloadWorker(record)
try {
downloadWorker.on('progress', (event: DownLoadFile.DownloadFileResult) => {
parentPort?.postMessage(event)
})
downloadWorker.on('complete', (event: DownLoadFile.DownloadFileResult) => {
parentPort?.postMessage(event)
})
downloadWorker.on('error', (event: DownLoadFile.DownloadFileResult) => {
parentPort?.postMessage(event)
})
downloadWorker.on('paused', (event: DownLoadFile.DownloadFileResult) => {
parentPort?.postMessage(event)
})
downloadWorker.on('cancelled', (event: DownLoadFile.DownloadFileResult) => {
parentPort?.postMessage(event)
})
if (action === 'download') {
downloadWorker.beginDownload()
} else {
downloadWorker.resume()
}
parentPort?.on('message', (event: DownLoadFile.ToDownloadWorkerEvent) => {
if (event.type === 'cancel') {
downloadWorker.cancel()
} else if (event.type === 'pause') {
downloadWorker.pause()
} else if (event.type === 'resume') {
// 由于目前是发生异常则退出该worker所以resume暂时不实现
}
})
} catch (err: any) {
downloadWorker.sendError(err.message || '下载异常')
}
})(workerData as DownLoadFile.DownloadFileWorkerData)