UNPKG

ledown

Version:

A library for nodejs to download http large file in pauseable and resumable way

146 lines (124 loc) 3.86 kB
/** @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 } }