Moved filesystem checks before everything else

This commit is contained in:
Geomitron
2020-05-17 23:30:25 -04:00
parent b9e1d61529
commit 9cdc5e8847
5 changed files with 155 additions and 49 deletions

View File

@@ -1,19 +1,15 @@
import { FileDownloader } from './FileDownloader' import { FileDownloader } from './FileDownloader'
import { tempPath } from '../../shared/Paths'
import { join } from 'path' import { join } from 'path'
import { FileExtractor } from './FileExtractor' import { FileExtractor } from './FileExtractor'
import { sanitizeFilename, interpolate } from '../../shared/UtilFunctions' import { sanitizeFilename, interpolate } from '../../shared/UtilFunctions'
import { emitIPCEvent } from '../../main' import { emitIPCEvent } from '../../main'
import { promisify } from 'util' import { promisify } from 'util'
import { randomBytes as _randomBytes } from 'crypto'
import { mkdir as _mkdir } from 'fs'
import { ProgressType, NewDownload } from 'src/electron/shared/interfaces/download.interface' import { ProgressType, NewDownload } from 'src/electron/shared/interfaces/download.interface'
import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface' import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface'
import { FileTransfer } from './FileTransfer' import { FileTransfer } from './FileTransfer'
import * as _rimraf from 'rimraf' import * as _rimraf from 'rimraf'
import { FilesystemChecker } from './FilesystemChecker'
const randomBytes = promisify(_randomBytes)
const mkdir = promisify(_mkdir)
const rimraf = promisify(_rimraf) const rimraf = promisify(_rimraf)
type EventCallback = { type EventCallback = {
@@ -33,7 +29,7 @@ export class ChartDownload {
private callbacks = {} as Callbacks private callbacks = {} as Callbacks
private files: DriveFile[] private files: DriveFile[]
private percent = 0 // Needs to be stored here because errors won't know the exact percent private percent = 0 // Needs to be stored here because errors won't know the exact percent
private chartPath: string private tempPath: string
private dropFastUpdate = false private dropFastUpdate = false
private readonly individualFileProgressPortion: number private readonly individualFileProgressPortion: number
@@ -88,7 +84,7 @@ export class ChartDownload {
this.cancelFn = undefined this.cancelFn = undefined
cancelFn() cancelFn()
try { try {
rimraf(this.chartPath) // Delete temp folder rimraf(this.tempPath) // Delete temp folder
} catch (e) { /** Do nothing */ } } catch (e) { /** Do nothing */ }
} }
} }
@@ -130,18 +126,18 @@ export class ChartDownload {
* Starts the download process. * Starts the download process.
*/ */
async beginDownload() { async beginDownload() {
// CREATE DOWNLOAD DIRECTORY // CHECK FILESYSTEM ACCESS
try { const checker = new FilesystemChecker(this.destinationFolderName)
this.chartPath = await this.createDownloadFolder() this.cancelFn = () => checker.cancelCheck()
} catch (err) {
this.retryFn = () => this.beginDownload() const checkerComplete = this.addFilesystemCheckerEventListeners(checker)
this.updateGUI('Access Error', err.message, 'error') checker.beginCheck()
return await checkerComplete
}
// DOWNLOAD FILES // DOWNLOAD FILES
for (let i = 0; i < this.files.length; i++) { for (let i = 0; i < this.files.length; i++) {
const downloader = new FileDownloader(this.files[i].webContentLink, join(this.chartPath, this.files[i].name)) if (this.files[i].name == 'ch.dat') { continue }
const downloader = new FileDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name))
this.cancelFn = () => downloader.cancelDownload() this.cancelFn = () => downloader.cancelDownload()
const downloadComplete = this.addDownloadEventListeners(downloader, i) const downloadComplete = this.addDownloadEventListeners(downloader, i)
@@ -151,7 +147,7 @@ export class ChartDownload {
// EXTRACT FILES // EXTRACT FILES
if (this.isArchive) { if (this.isArchive) {
const extractor = new FileExtractor(this.chartPath) const extractor = new FileExtractor(this.tempPath)
this.cancelFn = () => extractor.cancelExtract() this.cancelFn = () => extractor.cancelExtract()
const extractComplete = this.addExtractorEventListeners(extractor) const extractComplete = this.addExtractorEventListeners(extractor)
@@ -160,7 +156,7 @@ export class ChartDownload {
} }
// TRANSFER FILES // TRANSFER FILES
const transfer = new FileTransfer(this.chartPath, this.destinationFolderName) const transfer = new FileTransfer(this.tempPath, this.destinationFolderName)
this.cancelFn = () => transfer.cancelTransfer() this.cancelFn = () => transfer.cancelTransfer()
const transferComplete = this.addTransferEventListeners(transfer) const transferComplete = this.addTransferEventListeners(transfer)
@@ -171,26 +167,22 @@ export class ChartDownload {
} }
/** /**
* Attempts to create a unique folder in Bridge's data paths. * Defines what happens in reponse to `FilesystemChecker` events.
* @returns the new folder's path. * @returns a `Promise` that resolves when the filesystem has been checked.
* @throws an error if this fails.
*/ */
private async createDownloadFolder() { private addFilesystemCheckerEventListeners(checker: FilesystemChecker) {
let retryCount = 0 checker.on('start', () => {
let chartPath = '' this.updateGUI('Checking filesystem...', '', 'good')
})
while (retryCount < 5) { checker.on('error', this.handleError.bind(this))
chartPath = join(tempPath, `chart_${(await randomBytes(5)).toString('hex')}`)
try {
await mkdir(chartPath)
return chartPath
} catch (e) {
console.log(`Error creating folder [${chartPath}], retrying with a different folder...`)
retryCount++
}
}
throw new Error(`Bridge was unable to create a directory at [${chartPath}]`) return new Promise<void>(resolve => {
checker.on('complete', (tempPath) => {
this.tempPath = tempPath
resolve()
})
})
} }
/** /**

View File

@@ -3,7 +3,6 @@ import { createWriteStream } from 'fs'
import * as needle from 'needle' import * as needle from 'needle'
// TODO: replace needle with got (for cancel() method) (if before-headers event is possible?) // TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
// TODO: add download throttle library and setting // TODO: add download throttle library and setting
import { getSettings } from '../SettingsHandler.ipc'
import { googleTimer } from './GoogleTimer' import { googleTimer } from './GoogleTimer'
import { DownloadError } from './ChartDownload' import { DownloadError } from './ChartDownload'
@@ -19,7 +18,6 @@ type EventCallback = {
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
const downloadErrors = { const downloadErrors = {
libraryFolder: () => { return { header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' } },
timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } }, timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } },
connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } }, connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } },
responseError: (statusCode: number) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } }, responseError: (statusCode: number) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } },
@@ -57,9 +55,6 @@ export class FileDownloader {
* Download the file after waiting for the google rate limit. * Download the file after waiting for the google rate limit.
*/ */
beginDownload() { beginDownload() {
if (getSettings().libraryPath == undefined) {
this.failDownload(downloadErrors.libraryFolder())
} else {
googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => { googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => {
this.callbacks.waitProgress(remainingSeconds, totalSeconds) this.callbacks.waitProgress(remainingSeconds, totalSeconds)
})) }))
@@ -68,7 +63,6 @@ export class FileDownloader {
this.requestDownload() this.requestDownload()
})) }))
} }
}
/** /**
* Sends a request to download the file at `this.url`. * Sends a request to download the file at `this.url`.

View File

@@ -36,7 +36,7 @@ export class FileExtractor {
constructor(private sourceFolder: string) { } constructor(private sourceFolder: string) { }
/** /**
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) * Calls `callback` when `event` fires. (no events will be fired after `this.cancelExtract()` is called)
*/ */
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) { on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
this.callbacks[event] = callback this.callbacks[event] = callback

View File

@@ -39,7 +39,7 @@ export class FileTransfer {
} }
/** /**
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) * Calls `callback` when `event` fires. (no events will be fired after `this.cancelTransfer()` is called)
*/ */
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) { on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
this.callbacks[event] = callback this.callbacks[event] = callback

View File

@@ -0,0 +1,120 @@
import { DownloadError } from './ChartDownload'
import { tempPath } from '../../shared/Paths'
import { AnyFunction } from 'src/electron/shared/UtilFunctions'
import { randomBytes as _randomBytes } from 'crypto'
import { mkdir, access, constants } from 'fs'
import { join } from 'path'
import { promisify } from 'util'
import { getSettings } from '../SettingsHandler.ipc'
const randomBytes = promisify(_randomBytes)
type EventCallback = {
'start': () => void
'error': (err: DownloadError, retry: () => void | Promise<void>) => void
'complete': (tempPath: string) => void
}
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
const filesystemErrors = {
libraryFolder: () => { return { header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' } },
libraryAccess: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to access library folder.'),
destinationFolderExists: (destinationPath: string) => {
return { header: 'This chart already exists in your library folder.', body: destinationPath }
},
mkdirError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to create temporary folder.')
}
function fsError(err: NodeJS.ErrnoException, description: string) {
return { header: description, body: `${err.name}: ${err.message}` }
}
export class FilesystemChecker {
private callbacks = {} as Callbacks
private wasCanceled = false
constructor(private destinationFolderName: string) { }
/**
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
*/
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
this.callbacks[event] = callback
}
/**
* Check that the filesystem is set up for the download.
*/
beginCheck() {
this.callbacks.start()
this.checkLibraryFolder()
}
/**
* Verifies that the user has specified a library folder.
*/
private checkLibraryFolder() {
if (getSettings().libraryPath == undefined) {
this.callbacks.error(filesystemErrors.libraryFolder(), () => this.beginCheck())
} else {
access(getSettings().libraryPath, constants.W_OK, this.cancelable((err) => {
if (err) {
this.callbacks.error(filesystemErrors.libraryAccess(err), () => this.beginCheck())
} else {
this.checkDestinationFolder()
}
}))
}
}
/**
* Checks that the destination folder doesn't already exist.
*/
private checkDestinationFolder() {
const destinationPath = join(getSettings().libraryPath, this.destinationFolderName)
access(destinationPath, constants.F_OK, this.cancelable((err) => {
if (err) { // File does not exist
this.createDownloadFolder()
} else {
this.callbacks.error(filesystemErrors.destinationFolderExists(destinationPath), () => this.beginCheck())
}
}))
}
/**
* Attempts to create a unique folder in Bridge's data paths.
*/
private async createDownloadFolder(retryCount = 0) {
const tempChartPath = join(tempPath, `chart_${(await randomBytes(5)).toString('hex')}`)
mkdir(tempChartPath, this.cancelable((err) => {
if (err) {
if (retryCount < 5) {
console.log(`Error creating folder [${tempChartPath}], retrying with a different folder...`)
this.createDownloadFolder(retryCount + 1)
} else {
this.callbacks.error(filesystemErrors.mkdirError(err), () => this.createDownloadFolder())
}
} else {
this.callbacks.complete(tempChartPath)
}
}))
}
/**
* Stop the process of checking the filesystem permissions. (no more events will be fired after this is called)
*/
cancelCheck() {
this.wasCanceled = true
}
/**
* Wraps a function that is able to be prevented if `this.cancelCheck()` was called.
*/
private cancelable<F extends AnyFunction>(fn: F) {
return (...args: Parameters<F>): ReturnType<F> => {
if (this.wasCanceled) { return }
return fn(...Array.from(args))
}
}
}