More download bugfixes

This commit is contained in:
Geomitron
2020-05-10 15:46:11 -04:00
parent a81fddea3b
commit 8a4620d771
10 changed files with 74 additions and 42 deletions

View File

@@ -10,9 +10,11 @@ import { mkdir as _mkdir } from 'fs'
import { ProgressType, NewDownload } from 'src/electron/shared/interfaces/download.interface'
import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface'
import { FileTransfer } from './FileTransfer'
import * as _rimraf from 'rimraf'
const randomBytes = promisify(_randomBytes)
const mkdir = promisify(_mkdir)
const rimraf = promisify(_rimraf)
type EventCallback = {
/** Note: this will not be the last event if `retry()` is called. */
@@ -31,6 +33,8 @@ export class ChartDownload {
private callbacks = {} as Callbacks
private files: DriveFile[]
private percent = 0 // Needs to be stored here because errors won't know the exact percent
private chartPath: string
private dropFastUpdate = false
private readonly individualFileProgressPortion: number
private readonly destinationFolderName: string
@@ -68,6 +72,13 @@ export class ChartDownload {
}
}
/**
* Updates the GUI to indicate that a retry will be attempted.
*/
displayRetrying() {
this.updateGUI('', 'Waiting for other downloads to finish to retry...', 'good')
}
/**
* Cancels the download if it is running.
*/
@@ -76,6 +87,7 @@ export class ChartDownload {
const cancelFn = this.cancelFn
this.cancelFn = undefined
cancelFn()
rimraf(this.chartPath) // Delete temp folder
}
}
@@ -83,6 +95,15 @@ export class ChartDownload {
* Updates the GUI with new information about this chart download.
*/
private updateGUI(header: string, description: string, type: ProgressType) {
if (type == 'fastUpdate') {
if (this.dropFastUpdate) {
return
} else {
this.dropFastUpdate = true
setTimeout(() => this.dropFastUpdate = false, 30)
}
}
emitIPCEvent('download-updated', {
versionID: this.versionID,
title: `${this.data.avTagName} - ${this.data.artist}`,
@@ -108,9 +129,8 @@ export class ChartDownload {
*/
async beginDownload() {
// CREATE DOWNLOAD DIRECTORY
let chartPath: string
try {
chartPath = await this.createDownloadFolder()
this.chartPath = await this.createDownloadFolder()
} catch (err) {
this.retryFn = () => this.beginDownload()
this.updateGUI('Access Error', err.message, 'error')
@@ -119,7 +139,7 @@ export class ChartDownload {
// DOWNLOAD FILES
for (let i = 0; i < this.files.length; i++) {
const downloader = new FileDownloader(this.files[i].webContentLink, join(chartPath, this.files[i].name))
const downloader = new FileDownloader(this.files[i].webContentLink, join(this.chartPath, this.files[i].name))
this.cancelFn = () => downloader.cancelDownload()
const downloadComplete = this.addDownloadEventListeners(downloader, i)
@@ -129,7 +149,7 @@ export class ChartDownload {
// EXTRACT FILES
if (this.isArchive) {
const extractor = new FileExtractor(chartPath)
const extractor = new FileExtractor(this.chartPath)
this.cancelFn = () => extractor.cancelExtract()
const extractComplete = this.addExtractorEventListeners(extractor)
@@ -138,7 +158,7 @@ export class ChartDownload {
}
// TRANSFER FILES
const transfer = new FileTransfer(chartPath, this.destinationFolderName)
const transfer = new FileTransfer(this.chartPath, this.destinationFolderName)
this.cancelFn = () => transfer.cancelTransfer()
const transferComplete = this.addTransferEventListeners(transfer)

View File

@@ -29,10 +29,11 @@ class DownloadHandler implements IPCEmitHandler<'download'> {
}
}
private retryDownload(data: Download) { // TODO: cause this to send a GUI update that says waiting for download to finish...
private retryDownload(data: Download) {
const index = this.retryWaiting.findIndex(download => download.versionID == data.versionID)
if (index != -1) {
const retryDownload = this.retryWaiting.splice(index, 1)[0]
retryDownload.displayRetrying()
if (this.currentDownload == undefined) {
this.currentDownload = retryDownload
retryDownload.retry()

View File

@@ -1,5 +1,6 @@
import Comparators from 'comparators'
import { ChartDownload } from './ChartDownload'
import { emitIPCEvent } from '../../main'
export class DownloadQueue {
@@ -15,7 +16,7 @@ export class DownloadQueue {
}
pop() {
return this.downloadQueue.pop()
return this.downloadQueue.shift()
}
get(versionID: number) {
@@ -27,6 +28,7 @@ export class DownloadQueue {
if (index != -1) {
this.downloadQueue[index].cancel()
this.downloadQueue.splice(index, 1)
emitIPCEvent('queue-updated', this.downloadQueue.map(download => download.versionID))
}
}
@@ -39,5 +41,6 @@ export class DownloadQueue {
}
this.downloadQueue.sort(comparator)
emitIPCEvent('queue-updated', this.downloadQueue.map(download => download.versionID))
}
}

View File

@@ -2,6 +2,7 @@ import { AnyFunction } from '../../shared/UtilFunctions'
import { createWriteStream } from 'fs'
import * as needle from 'needle'
// TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
// TODO: add download throttle library and setting
import { getSettings } from '../SettingsHandler.ipc'
import { googleTimer } from './GoogleTimer'
import { DownloadError } from './ChartDownload'
@@ -37,6 +38,7 @@ export class FileDownloader {
private callbacks = {} as Callbacks
private retryCount: number
private wasCanceled = false
private req: NodeJS.ReadableStream
/**
* @param url The download link.
@@ -55,7 +57,6 @@ export class FileDownloader {
* Download the file after waiting for the google rate limit.
*/
beginDownload() {
console.log('Begin download...')
if (getSettings().libraryPath == undefined) {
this.failDownload(downloadErrors.libraryFolder())
} else {
@@ -75,7 +76,7 @@ export class FileDownloader {
*/
private requestDownload(cookieHeader?: string) {
this.callbacks.requestSent()
const req = needle.get(this.url, {
this.req = needle.get(this.url, {
'follow_max': 10,
'open_timeout': 5000,
'headers': Object.assign({
@@ -86,7 +87,7 @@ export class FileDownloader {
)
})
req.on('timeout', this.cancelable((type: string) => {
this.req.on('timeout', this.cancelable((type: string) => {
this.retryCount++
if (this.retryCount <= this.RETRY_MAX) {
console.log(`TIMEOUT: Retry attempt ${this.retryCount}...`)
@@ -96,20 +97,20 @@ export class FileDownloader {
}
}))
req.on('err', this.cancelable((err: Error) => {
this.req.on('err', this.cancelable((err: Error) => {
this.failDownload(downloadErrors.connectionError(err))
}))
req.on('header', this.cancelable((statusCode, headers: Headers) => {
this.req.on('header', this.cancelable((statusCode, headers: Headers) => {
if (statusCode != 200) {
this.failDownload(downloadErrors.responseError(statusCode))
return
}
if (headers['content-type'].startsWith('text/html')) {
this.handleHTMLResponse(req, headers['set-cookie'])
this.handleHTMLResponse(headers['set-cookie'])
} else {
this.handleDownloadResponse(req)
this.handleDownloadResponse()
}
}))
}
@@ -117,14 +118,12 @@ export class FileDownloader {
/**
* A Google Drive HTML response to a download request usually means this is the "file too large to scan for viruses" warning.
* This function sends the request that results from clicking "download anyway", or generates an error if it can't be found.
* @param req The download request.
* @param cookieHeader The "cookie=" header of this request.
*/
private handleHTMLResponse(req: NodeJS.ReadableStream, cookieHeader: string) {
console.log('HTML Response...')
private handleHTMLResponse(cookieHeader: string) {
let virusScanHTML = ''
req.on('data', this.cancelable(data => virusScanHTML += data))
req.on('done', this.cancelable((err: Error) => {
this.req.on('data', this.cancelable(data => virusScanHTML += data))
this.req.on('done', this.cancelable((err: Error) => {
if (err) {
this.failDownload(downloadErrors.connectionError(err))
} else {
@@ -149,21 +148,20 @@ export class FileDownloader {
* Pipes the data from a download response to `this.fullPath`.
* @param req The download request.
*/
private handleDownloadResponse(req: NodeJS.ReadableStream) {
console.log('Download response...')
private handleDownloadResponse() {
this.callbacks.downloadProgress(0)
let downloadedSize = 0
req.pipe(createWriteStream(this.fullPath))
req.on('data', this.cancelable((data) => {
this.req.pipe(createWriteStream(this.fullPath))
this.req.on('data', this.cancelable((data) => {
downloadedSize += data.length
this.callbacks.downloadProgress(downloadedSize)
}))
req.on('err', this.cancelable((err: Error) => {
this.req.on('err', this.cancelable((err: Error) => {
this.failDownload(downloadErrors.connectionError(err))
}))
req.on('end', this.cancelable(() => {
this.req.on('end', this.cancelable(() => {
this.callbacks.complete()
}))
}
@@ -181,6 +179,9 @@ export class FileDownloader {
cancelDownload() {
this.wasCanceled = true
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
if (this.req) {
// TODO: destroy request
}
}
/**

View File

@@ -17,14 +17,14 @@ type EventCallback = {
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
const transferErrors = {
readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file'),
deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file'),
rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder'),
mvError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to move folder to library')
readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file.'),
deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file.'),
rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder.'),
mvError: (err: NodeJS.ErrnoException) => fsError(err, `Failed to move folder to library.${err.code == 'EPERM' ? ' (does the chart already exist?)' : ''}`)
}
function fsError(err: NodeJS.ErrnoException, description: string) {
return { header: `${description} (${err.code})`, body: `${err.name}: ${err.message}` }
return { header: description, body: `${err.name}: ${err.message}` }
}
export class FileTransfer {