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 { 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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