ledown
Version:
A library for nodejs to download http large file in pauseable and resumable way
132 lines (115 loc) • 3.13 kB
JavaScript
/** @flow */
import EventEmitter from 'events'
import { promisify } from 'util'
import { write } from 'fs'
import TaskMeta from './TaskMeta'
import Task from './Task'
import fetch from './fetch'
import { type IncomingMessage } from 'http'
const writeAsync = promisify(write)
export type DownloadOptions = {
windowSize: number,
fd: number
}
export default class TaskThread extends EventEmitter {
taskMeta: TaskMeta
options: DownloadOptions
currentPosition: number
currentRequest: IncomingMessage
isRunning: boolean = false
pendingWrite: number = 0
constructor (taskMeta: TaskMeta, options: DownloadOptions) {
super()
this.taskMeta = taskMeta
this.options = options
this.handleData = this.handleData.bind(this)
this.handleSingleRequestEnd = this.handleSingleRequestEnd.bind(this)
}
executeRequest = async () => {
if (!this.isRunning) return
const range = this.taskMeta.allocRange(this.options.windowSize)
if (!range) {
return this.emitEnd()
}
try {
this.currentPosition = range[0]
this.currentRequest = await fetch(this.taskMeta.url, {
method: 'GET',
headers: {
Range: `bytes=${range[0]}-${range[1] - 1}`
}
})
if (this.currentRequest.statusCode >= 400) {
throw new Error(Task.ERROR.ERR_NETWORK)
}
this.currentRequest.on('end', this.handleSingleRequestEnd)
this.currentRequest.on('data', this.handleData)
} catch (e) {
this.emitError(e)
}
}
start (): void {
if (this.isRunning) return
this.isRunning = true
this.executeRequest()
}
async stop (): void {
if (!this.isRunning) return
this.isRunning = false
if (this.currentRequest) {
this.currentRequest.destroy()
this.currentRequest = null
}
if (this.pendingWrite > 0) {
await this.awaitPendingWrite()
}
}
async awaitPendingWrite () {
return new Promise(resolve => {
let awaitProgress = () => {
if (this.pendingWrite === 0) {
this.off('progress', awaitProgress)
resolve()
}
}
if (this.pendingWrite === 0) {
resolve()
} else {
this.on('progress', awaitProgress)
}
})
}
async handleData (data: Buffer) {
if (!this.isRunning) return
const start = this.currentPosition
const end = start + data.byteLength
this.currentPosition = end
this.pendingWrite++
try {
await writeAsync(this.options.fd, data, 0, data.byteLength, start)
} catch (e) {
this.pendingWrite--
return this.emitError(new Error(Task.ERROR.ERR_DISK_IO_WRITE))
}
this.pendingWrite--
this.taskMeta.completeRange([start, end])
this.emit('progress')
}
async emitError (e) {
await this.stop()
this.emit('error', e)
}
async emitEnd () {
await this.stop()
this.emit('end')
}
async handleSingleRequestEnd () {
if (!this.isRunning) return
this.currentRequest = null
await this.awaitPendingWrite()
this.executeRequest()
}
off (...arg) {
return this.removeListener(...arg)
}
}