更换下载库
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import request from 'request'
|
||||
import { EventEmitter } from 'events'
|
||||
import { performance } from 'perf_hooks'
|
||||
import { STATUS } from './util'
|
||||
|
||||
|
||||
const defaultChunkInfo = {
|
||||
path: null,
|
||||
startByte: 0,
|
||||
endByte: '',
|
||||
}
|
||||
|
||||
const defaultRequestOptions = {
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
}
|
||||
const defaultOptions = {
|
||||
|
||||
}
|
||||
|
||||
class Task extends EventEmitter {
|
||||
/**
|
||||
*
|
||||
* @param {String} url download url
|
||||
* @param {Object} chunkInfo
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(url, savePath, filename, options = {}) {
|
||||
super()
|
||||
|
||||
this.resumeLastChunk = null
|
||||
this.downloadUrl = url
|
||||
this.chunkInfo = Object.assign({}, defaultChunkInfo, {
|
||||
path: path.join(savePath, filename),
|
||||
startByte: 0,
|
||||
})
|
||||
if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''
|
||||
|
||||
this.options = Object.assign({}, defaultOptions, options)
|
||||
|
||||
this.requestOptions = Object.assign({}, defaultRequestOptions, this.options.requestOptions || {})
|
||||
if (!this.requestOptions.headers) this.requestOptions.headers = {}
|
||||
|
||||
this.progress = {
|
||||
total: 0,
|
||||
downloaded: 0,
|
||||
speed: 0,
|
||||
}
|
||||
this.statsEstimate = {
|
||||
time: 0,
|
||||
bytes: 0,
|
||||
prevBytes: 0,
|
||||
}
|
||||
this.status = STATUS.idle
|
||||
}
|
||||
|
||||
__init() {
|
||||
this.status = STATUS.init
|
||||
const { path, startByte, endByte } = this.chunkInfo
|
||||
if (startByte) this.requestOptions.headers.range = `bytes=${startByte}-${endByte}`
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!path) return resolve()
|
||||
fs.stat(path, (errStat, stats) => {
|
||||
if (errStat) {
|
||||
if (errStat.code !== 'ENOENT') {
|
||||
this.__handleError(errStat)
|
||||
reject(errStat)
|
||||
return
|
||||
}
|
||||
} else if (stats.size >= 10) {
|
||||
fs.open(path, 'r', (errOpen, fd) => {
|
||||
if (errOpen) {
|
||||
this.__handleError(errOpen)
|
||||
reject(errOpen)
|
||||
return
|
||||
}
|
||||
fs.read(fd, Buffer.alloc(10), 0, 10, stats.size - 10, (errRead, bytesRead, buffer) => {
|
||||
if (errRead) {
|
||||
this.__handleError(errRead)
|
||||
reject(errRead)
|
||||
return
|
||||
}
|
||||
fs.close(fd, errClose => {
|
||||
if (errClose) {
|
||||
this.__handleError(errClose)
|
||||
reject(errClose)
|
||||
return
|
||||
}
|
||||
|
||||
// resume download
|
||||
// console.log(buffer)
|
||||
this.resumeLastChunk = buffer
|
||||
this.progress.downloaded = stats.size
|
||||
this.requestOptions.headers.range = `bytes=${stats.size - 10}-${endByte || ''}`
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
__httpFetch(url, options) {
|
||||
// console.log(options)
|
||||
this.request = request(url, options)
|
||||
.on('response', response => {
|
||||
if (response.statusCode !== 200 && response.statusCode !== 206) {
|
||||
this.status = STATUS.failed
|
||||
this.emit('fail', response)
|
||||
this.__closeRequest()
|
||||
this.__closeWriteStream()
|
||||
return
|
||||
}
|
||||
this.emit('response', response)
|
||||
try {
|
||||
this.__initDownload(response)
|
||||
} catch (error) {
|
||||
return this.__handleError(error)
|
||||
}
|
||||
this.status = STATUS.running
|
||||
response
|
||||
.on('data', this.__handleWriteData.bind(this))
|
||||
.on('error', err => this.__handleError(err))
|
||||
.on('end', () => this.__handleComplete())
|
||||
})
|
||||
.on('error', err => this.__handleError(err))
|
||||
.on('close', () => this.__closeWriteStream())
|
||||
}
|
||||
|
||||
__initDownload(response) {
|
||||
this.progress.total = parseInt(response.headers['content-length'] || 0)
|
||||
let options = {}
|
||||
let isResumable = this.options.forceResume || response.headers['accept-ranges'] !== 'none'
|
||||
if (isResumable) {
|
||||
options.flags = 'a'
|
||||
if (this.progress.downloaded) this.progress.total -= 10
|
||||
} else {
|
||||
if (this.chunkInfo.startByte > 0) return this.__handleError(new Error('The resource cannot be resumed download.'))
|
||||
}
|
||||
this.progress.total += this.progress.downloaded
|
||||
this.statsEstimate.prevBytes = this.progress.downloaded
|
||||
if (!this.chunkInfo.path) return this.__handleError(new Error('Chunk save Path is not set.'))
|
||||
this.ws = fs.createWriteStream(this.chunkInfo.path, options)
|
||||
|
||||
this.ws.on('finish', () => this.__closeWriteStream())
|
||||
this.ws.on('error', async err => {
|
||||
await this.__handleError(err)
|
||||
fs.unlink(this.chunkInfo.path, () => this.__handleError(err))
|
||||
})
|
||||
}
|
||||
|
||||
__handleComplete() {
|
||||
if (this.status == STATUS.error) return
|
||||
this.__closeWriteStream().then(() => {
|
||||
if (this.progress.downloaded == this.progress.total) {
|
||||
this.status = STATUS.completed
|
||||
this.emit('completed')
|
||||
} else {
|
||||
this.status = STATUS.stopped
|
||||
this.emit('stop')
|
||||
}
|
||||
})
|
||||
console.log('end')
|
||||
}
|
||||
|
||||
__handleError(error) {
|
||||
if (this.status == STATUS.error) return
|
||||
this.status = STATUS.error
|
||||
this.__closeRequest()
|
||||
this.__closeWriteStream()
|
||||
this.emit('error', error)
|
||||
}
|
||||
|
||||
__closeWriteStream() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.ws) return resolve()
|
||||
console.log('close write stream')
|
||||
this.ws.close(err => {
|
||||
if (err) {
|
||||
this.status = STATUS.error
|
||||
this.emit('error', err)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
this.ws = null
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
__closeRequest() {
|
||||
if (!this.request) return
|
||||
console.log('close request')
|
||||
this.request.abort()
|
||||
this.request = null
|
||||
}
|
||||
|
||||
__handleWriteData(chunk) {
|
||||
if (this.resumeLastChunk) {
|
||||
chunk = this.__handleDiffChunk(chunk)
|
||||
if (!chunk) {
|
||||
this.__handleError(new Error('Resume failed, response chunk does not match.'))
|
||||
this.stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
// console.log('data', chunk)
|
||||
if (this.status == STATUS.stopped || this.ws == null) return console.log('cancel write')
|
||||
this.__calculateProgress(chunk.length)
|
||||
this.ws.write(chunk, err => {
|
||||
if (!err) return
|
||||
console.log(err)
|
||||
this.__handleError(err)
|
||||
this.stop()
|
||||
})
|
||||
}
|
||||
|
||||
__handleDiffChunk(chunk) {
|
||||
// console.log('diff', chunk)
|
||||
let resumeLastChunkLen = this.resumeLastChunk.length
|
||||
let chunkLen = chunk.length
|
||||
let isOk
|
||||
if (chunkLen >= resumeLastChunkLen) {
|
||||
isOk = chunk.slice(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk.toString('hex')
|
||||
if (!isOk) return null
|
||||
|
||||
this.resumeLastChunk = null
|
||||
return chunk.slice(resumeLastChunkLen)
|
||||
} else {
|
||||
isOk = chunk.slice(0, chunkLen).toString('hex') === this.resumeLastChunk.slice(0, chunkLen).toString('hex')
|
||||
if (!isOk) return null
|
||||
this.resumeLastChunk = this.resumeLastChunk.slice(chunkLen)
|
||||
return chunk.slice(chunkLen)
|
||||
}
|
||||
}
|
||||
|
||||
__handleStop() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.request) {
|
||||
this.request.abort()
|
||||
this.request = null
|
||||
}
|
||||
if (this.ws) {
|
||||
this.ws.close(err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
this.emit('error', err)
|
||||
return
|
||||
}
|
||||
this.ws = null
|
||||
resolve()
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
__calculateProgress(receivedBytes) {
|
||||
const currentTime = performance.now()
|
||||
const elaspsedTime = currentTime - this.statsEstimate.time
|
||||
|
||||
const progress = this.progress
|
||||
progress.downloaded += receivedBytes
|
||||
progress.progress = progress.total ? (progress.downloaded / progress.total) * 100 : -1
|
||||
|
||||
|
||||
// emit the progress every second or if finished
|
||||
if (progress.downloaded === progress.total || elaspsedTime > 1000) {
|
||||
this.statsEstimate.time = currentTime
|
||||
this.statsEstimate.bytes = progress.downloaded - this.statsEstimate.prevBytes
|
||||
this.statsEstimate.prevBytes = progress.downloaded
|
||||
this.emit('progress', {
|
||||
total: progress.total,
|
||||
downloaded: progress.downloaded,
|
||||
progress: progress.progress,
|
||||
speed: this.statsEstimate.bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.status = STATUS.running
|
||||
await this.__init()
|
||||
this.__httpFetch(this.downloadUrl, this.requestOptions)
|
||||
this.emit('start')
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.status === STATUS.stopped) return
|
||||
this.status = STATUS.stopped
|
||||
await this.__handleStop()
|
||||
this.emit('stop')
|
||||
}
|
||||
|
||||
refreshUrl(url) {
|
||||
this.downloadUrl = url
|
||||
}
|
||||
}
|
||||
|
||||
export default Task
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DownloaderHelper } from 'node-downloader-helper'
|
||||
import Downloader from './Downloader'
|
||||
// import { pauseResumeTimer } from './util'
|
||||
import { sizeFormate } from '../index'
|
||||
import { sizeFormate, getProxyInfo } from '../index'
|
||||
import { debugDownload } from '../env'
|
||||
|
||||
// these are the default options
|
||||
@@ -21,38 +21,34 @@ export default ({
|
||||
fileName,
|
||||
method = 'get',
|
||||
headers,
|
||||
override,
|
||||
forceResume,
|
||||
// resumeTime = 5000,
|
||||
onEnd = () => {},
|
||||
onCompleted = () => {},
|
||||
onError = () => {},
|
||||
onStateChanged = () => {},
|
||||
onDownload = () => {},
|
||||
onPause = () => {},
|
||||
onResume = () => {},
|
||||
onFail = () => {},
|
||||
onStart = () => {},
|
||||
onStop = () => {},
|
||||
onProgress = () => {},
|
||||
resumeInfo,
|
||||
} = {}) => {
|
||||
const dl = new DownloaderHelper(url, path, {
|
||||
fileName,
|
||||
method,
|
||||
headers,
|
||||
override,
|
||||
const dl = new Downloader(url, path, fileName, {
|
||||
requestOptions: {
|
||||
method,
|
||||
headers,
|
||||
proxy: getProxyInfo(),
|
||||
},
|
||||
|
||||
forceResume,
|
||||
})
|
||||
|
||||
dl.on('end', () => {
|
||||
onEnd()
|
||||
dl.on('completed', () => {
|
||||
onCompleted()
|
||||
debugDownload && console.log('Download Completed')
|
||||
}).on('error', err => {
|
||||
if (err.message === 'socket hang up') return
|
||||
onError(err)
|
||||
debugDownload && console.error('Something happend', err)
|
||||
}).on('stateChanged', state => {
|
||||
onStateChanged(state)
|
||||
debugDownload && console.log('State: ', state)
|
||||
}).on('download', () => {
|
||||
onDownload()
|
||||
}).on('start', () => {
|
||||
onStart()
|
||||
// pauseResumeTimer(dl, resumeTime)
|
||||
}).on('progress', stats => {
|
||||
const progress = stats.progress.toFixed(2)
|
||||
@@ -68,24 +64,17 @@ export default ({
|
||||
const total = sizeFormate(stats.total)
|
||||
console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`)
|
||||
}
|
||||
}).on('pause', () => {
|
||||
onPause()
|
||||
}).on('stop', () => {
|
||||
onStop()
|
||||
debugDownload && console.log('paused')
|
||||
}).on('resume', () => {
|
||||
onResume()
|
||||
debugDownload && console.log('resume')
|
||||
}).on('fail', resp => {
|
||||
onFail(resp)
|
||||
debugDownload && console.log('fail')
|
||||
})
|
||||
|
||||
debugDownload && console.log('Downloading: ', url)
|
||||
|
||||
if (resumeInfo) {
|
||||
dl.__total = resumeInfo.totalFileSize // <--- Workaround
|
||||
// dl.__filePath = resumeInfo.filePath // <--- Workaround
|
||||
dl.__isResumable = true // <--- Workaround
|
||||
dl.resume()
|
||||
} else {
|
||||
dl.start()
|
||||
}
|
||||
dl.start()
|
||||
|
||||
return dl
|
||||
}
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
import { DH_STATES } from 'node-downloader-helper'
|
||||
|
||||
|
||||
export const pauseResumeTimer = (_dl, wait) => {
|
||||
setTimeout(() => {
|
||||
if (_dl.state === DH_STATES.FINISHED || _dl.state === DH_STATES.FAILED) {
|
||||
return
|
||||
}
|
||||
|
||||
_dl
|
||||
.pause()
|
||||
.then(() => console.log(`Paused for ${wait / 1000} seconds`))
|
||||
.then(() =>
|
||||
setTimeout(() => {
|
||||
if (!_dl.isResumable()) {
|
||||
console.warn(
|
||||
"This URL doesn't support resume, it will start from the beginning",
|
||||
)
|
||||
}
|
||||
return _dl.resume()
|
||||
}, wait),
|
||||
)
|
||||
}, wait)
|
||||
exports.STATUS = {
|
||||
idle: 'IDLE',
|
||||
init: 'INIT',
|
||||
running: 'RUNNING',
|
||||
paused: 'PAUSED',
|
||||
stopped: 'STOPPED',
|
||||
completed: 'COMPLETED',
|
||||
error: 'ERROR',
|
||||
failed: 'FAILED',
|
||||
}
|
||||
|
||||
@@ -374,3 +374,8 @@ export const clearCache = () => rendererInvoke('clearCache')
|
||||
* @param {*} height
|
||||
*/
|
||||
export const setWindowSize = (width, height) => rendererSend('setWindowSize', { width, height })
|
||||
|
||||
|
||||
export const getProxyInfo = () => window.globalObj.proxy.enable
|
||||
? `http://${window.globalObj.proxy.username}:${window.globalObj.proxy.password}@${window.globalObj.proxy.host}:${window.globalObj.proxy.port};`
|
||||
: undefined
|
||||
|
||||
@@ -4,6 +4,7 @@ import { debugRequest } from './env'
|
||||
import { requestMsg } from './message'
|
||||
import { bHh } from './music/options'
|
||||
import { deflateRawSync } from 'zlib'
|
||||
import { getProxyInfo } from './index'
|
||||
// import fs from 'fs'
|
||||
|
||||
const defaultHeaders = {
|
||||
@@ -219,10 +220,6 @@ export const http_jsonp = (url, options, callback) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getProxyInfo = () => window.globalObj.proxy.enable
|
||||
? `http://${window.globalObj.proxy.username}:${window.globalObj.proxy.password}@${window.globalObj.proxy.host}:${window.globalObj.proxy.port};`
|
||||
: undefined
|
||||
|
||||
const regx = /(?:\d\w)+/g
|
||||
|
||||
const fetchData = (url, method, {
|
||||
|
||||
Reference in New Issue
Block a user