mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 14:19:38 +00:00
Moved filesystem checks before everything else
This commit is contained in:
@@ -1,19 +1,15 @@
|
||||
import { FileDownloader } from './FileDownloader'
|
||||
import { tempPath } from '../../shared/Paths'
|
||||
import { join } from 'path'
|
||||
import { FileExtractor } from './FileExtractor'
|
||||
import { sanitizeFilename, interpolate } from '../../shared/UtilFunctions'
|
||||
import { emitIPCEvent } from '../../main'
|
||||
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 { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface'
|
||||
import { FileTransfer } from './FileTransfer'
|
||||
import * as _rimraf from 'rimraf'
|
||||
import { FilesystemChecker } from './FilesystemChecker'
|
||||
|
||||
const randomBytes = promisify(_randomBytes)
|
||||
const mkdir = promisify(_mkdir)
|
||||
const rimraf = promisify(_rimraf)
|
||||
|
||||
type EventCallback = {
|
||||
@@ -33,7 +29,7 @@ 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 tempPath: string
|
||||
private dropFastUpdate = false
|
||||
|
||||
private readonly individualFileProgressPortion: number
|
||||
@@ -88,7 +84,7 @@ export class ChartDownload {
|
||||
this.cancelFn = undefined
|
||||
cancelFn()
|
||||
try {
|
||||
rimraf(this.chartPath) // Delete temp folder
|
||||
rimraf(this.tempPath) // Delete temp folder
|
||||
} catch (e) { /** Do nothing */ }
|
||||
}
|
||||
}
|
||||
@@ -130,18 +126,18 @@ export class ChartDownload {
|
||||
* Starts the download process.
|
||||
*/
|
||||
async beginDownload() {
|
||||
// CREATE DOWNLOAD DIRECTORY
|
||||
try {
|
||||
this.chartPath = await this.createDownloadFolder()
|
||||
} catch (err) {
|
||||
this.retryFn = () => this.beginDownload()
|
||||
this.updateGUI('Access Error', err.message, 'error')
|
||||
return
|
||||
}
|
||||
// CHECK FILESYSTEM ACCESS
|
||||
const checker = new FilesystemChecker(this.destinationFolderName)
|
||||
this.cancelFn = () => checker.cancelCheck()
|
||||
|
||||
const checkerComplete = this.addFilesystemCheckerEventListeners(checker)
|
||||
checker.beginCheck()
|
||||
await checkerComplete
|
||||
|
||||
// DOWNLOAD FILES
|
||||
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()
|
||||
|
||||
const downloadComplete = this.addDownloadEventListeners(downloader, i)
|
||||
@@ -151,7 +147,7 @@ export class ChartDownload {
|
||||
|
||||
// EXTRACT FILES
|
||||
if (this.isArchive) {
|
||||
const extractor = new FileExtractor(this.chartPath)
|
||||
const extractor = new FileExtractor(this.tempPath)
|
||||
this.cancelFn = () => extractor.cancelExtract()
|
||||
|
||||
const extractComplete = this.addExtractorEventListeners(extractor)
|
||||
@@ -160,7 +156,7 @@ export class ChartDownload {
|
||||
}
|
||||
|
||||
// TRANSFER FILES
|
||||
const transfer = new FileTransfer(this.chartPath, this.destinationFolderName)
|
||||
const transfer = new FileTransfer(this.tempPath, this.destinationFolderName)
|
||||
this.cancelFn = () => transfer.cancelTransfer()
|
||||
|
||||
const transferComplete = this.addTransferEventListeners(transfer)
|
||||
@@ -171,26 +167,22 @@ export class ChartDownload {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to create a unique folder in Bridge's data paths.
|
||||
* @returns the new folder's path.
|
||||
* @throws an error if this fails.
|
||||
* Defines what happens in reponse to `FilesystemChecker` events.
|
||||
* @returns a `Promise` that resolves when the filesystem has been checked.
|
||||
*/
|
||||
private async createDownloadFolder() {
|
||||
let retryCount = 0
|
||||
let chartPath = ''
|
||||
private addFilesystemCheckerEventListeners(checker: FilesystemChecker) {
|
||||
checker.on('start', () => {
|
||||
this.updateGUI('Checking filesystem...', '', 'good')
|
||||
})
|
||||
|
||||
while (retryCount < 5) {
|
||||
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++
|
||||
}
|
||||
}
|
||||
checker.on('error', this.handleError.bind(this))
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ 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'
|
||||
|
||||
@@ -19,7 +18,6 @@ type EventCallback = {
|
||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||
|
||||
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})` } },
|
||||
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}` } },
|
||||
@@ -57,9 +55,6 @@ export class FileDownloader {
|
||||
* Download the file after waiting for the google rate limit.
|
||||
*/
|
||||
beginDownload() {
|
||||
if (getSettings().libraryPath == undefined) {
|
||||
this.failDownload(downloadErrors.libraryFolder())
|
||||
} else {
|
||||
googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => {
|
||||
this.callbacks.waitProgress(remainingSeconds, totalSeconds)
|
||||
}))
|
||||
@@ -68,7 +63,6 @@ export class FileDownloader {
|
||||
this.requestDownload()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to download the file at `this.url`.
|
||||
|
||||
@@ -36,7 +36,7 @@ export class FileExtractor {
|
||||
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]) {
|
||||
this.callbacks[event] = callback
|
||||
|
||||
@@ -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]) {
|
||||
this.callbacks[event] = callback
|
||||
|
||||
120
src/electron/ipc/download/FilesystemChecker.ts
Normal file
120
src/electron/ipc/download/FilesystemChecker.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user