ledown
Version:
A library for nodejs to download http large file in pauseable and resumable way
146 lines (124 loc) • 3.86 kB
JavaScript
/** @flow */
import { readFile, writeFile, open, close } from 'fs'
import path from 'path'
import EventEmitter from 'events'
import { promisify } from 'util'
import uuid from 'uuid'
import fetch from './fetch'
import TaskMeta from './TaskMeta'
import TaskThread from './TaskThread'
const writeFileAsync = promisify(writeFile)
const readFileAsync = promisify(readFile)
const openAsync = promisify(open)
const closeAsync = promisify(close)
type TaskOptions = {
url: string,
referUrl?: string,
downloadDir?: string,
downloadPath?: string
}
export default class Task extends EventEmitter {
static ERROR = {
ERR_NO_METADATA: 'ERR_NO_METADATA',
ERR_DISK_IO_WRITE: 'ERR_DISK_IO_WRITE',
ERR_NETWORK: 'ERR_NETWORK'
}
metadata: TaskMeta = null
threads: Array<TaskThread> = []
isRunning: boolean = false
fd: number
static async create ({url, referUrl, downloadDir, downloadPath}: TaskOptions): Promise<Task> {
const task = new Task()
const response = await fetch(url, { method: 'HEAD' })
if (response.statusCode !== 200) {
throw new Error(Task.ERROR.NETWORK_ERROR)
}
const acceptRanges = response.headers['accept-ranges']
const mimeType = response.headers['content-type']
const totalBytes = response.headers['content-length']
if (!downloadPath) {
downloadPath = path.join(downloadDir, `${uuid()}.${mimeType.replace(/^.*\//, '')}`)
}
task.metadata = new TaskMeta({
url,
referUrl,
mimeType,
totalBytes: Number(totalBytes),
resumable: !!acceptRanges,
downloadPath: downloadPath
})
return task
}
static async restoreFromFile (path: string): Task {
const content = await readFileAsync(path, 'utf-8')
const task = new Task()
task.metadata = new TaskMeta(JSON.parse(content))
return task
}
async storeToFile (path: string): void {
const content = JSON.stringify(this.metadata.toJSONObject())
await writeFileAsync(path, content, 'utf-8')
}
async start (numOfThread: number = 1, windowSize: number): void {
if (!this.metadata) {
return this.emitError(new Error(Task.ERROR.ERR_NO_METADATA))
}
if (this.isRunning) return
this.isRunning = true
if (!windowSize) {
windowSize = Math.ceil(this.metadata.remainingByteCount / numOfThread)
}
if (!this.metadata.resumable) {
// non-resumable resource, use 1 thread to download all the bytes
numOfThread = 1
windowSize = this.metadata.totalBytes
}
await this.openFd()
if (this.fd) {
let endHandler = this.getThreadEndHandler(numOfThread)
const options = {windowSize, fd: this.fd}
for (; numOfThread > 0; numOfThread--) {
const taskThread = new TaskThread(this.metadata, options)
taskThread.on('progress', () => this.emit('progress'))
taskThread.on('end', endHandler)
taskThread.on('error', this.emitError)
taskThread.start()
this.threads.push(taskThread)
}
}
}
getThreadEndHandler (num: number): Function {
return async () => {
if (--num === 0) {
this.isRunning = false
this.threads = []
await this.closeFd()
this.emit('end')
}
}
}
async stop (): void {
if (!this.isRunning) return
this.isRunning = false
await Promise.all(this.threads.map(thread => thread.stop()))
this.threads = []
this.metadata.resetDownloadingRange()
await this.closeFd()
}
async openFd (dir: string): number {
this.fd = await openAsync(this.metadata.downloadPath, 'a')
}
async closeFd () {
await closeAsync(this.fd)
}
emitError = async (err) => {
await this.stop()
this.emit('error', err)
}
off (...arg) {
return this.removeListener(...arg)
}
get progress (): number {
return this.metadata.progress
}
}