Files
Bridge-Multi/src-electron/ipc/download/FileTransfer.ts
2023-11-28 19:50:45 -06:00

122 lines
3.7 KiB
TypeScript

import { Dirent, readdir as _readdir } from 'fs'
import mv from 'mv'
import { join } from 'path'
import { rimraf } from 'rimraf'
import { promisify } from 'util'
import { settings } from '../SettingsHandler.ipc'
import { DownloadError } from './ChartDownload'
const readdir = promisify(_readdir)
interface EventCallback {
'start': (destinationFolder: string) => void
'error': (err: DownloadError, retry: () => void | Promise<void>) => void
'complete': () => void
}
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
const transferErrors = {
libraryError: () => ({ header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' }),
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, body: `${err.name}: ${err.message}` }
}
export class FileTransfer {
private callbacks = {} as Callbacks
private wasCanceled = false
private destinationFolder: string | null
private nestedSourceFolder: string // The top-level folder that is copied to the library folder
constructor(private sourceFolder: string, destinationFolderName: string) {
this.destinationFolder = settings.libraryPath ? join(settings.libraryPath, destinationFolderName) : null
this.nestedSourceFolder = sourceFolder
}
/**
* 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
}
async beginTransfer() {
if (!this.destinationFolder) {
this.callbacks.error(transferErrors.libraryError(), () => this.beginTransfer())
} else {
this.callbacks.start(this.destinationFolder)
await this.cleanFolder()
if (this.wasCanceled) { return }
this.moveFolder()
}
}
/**
* Fixes common problems with the download chart folder.
*/
private async cleanFolder() {
let files: Dirent[]
try {
files = await readdir(this.nestedSourceFolder, { withFileTypes: true })
} catch (err) {
this.callbacks.error(transferErrors.readError(err), () => this.cleanFolder())
return
}
// Remove nested folders
if (files.length === 1 && !files[0].isFile()) {
this.nestedSourceFolder = join(this.nestedSourceFolder, files[0].name)
await this.cleanFolder()
return
}
// Delete '__MACOSX' folder
for (const file of files) {
if (!file.isFile() && file.name === '__MACOSX') {
try {
await rimraf(join(this.nestedSourceFolder, file.name))
} catch (err) {
this.callbacks.error(transferErrors.rimrafError(err), () => this.cleanFolder())
return
}
} else {
// TODO: handle other common problems, like chart/audio files not named correctly
}
}
}
/**
* Moves the downloaded chart to the library path.
*/
private moveFolder() {
if (!this.destinationFolder) {
this.callbacks.error(transferErrors.libraryError(), () => this.moveFolder())
} else {
mv(this.nestedSourceFolder, this.destinationFolder, { mkdirp: true }, err => {
if (err) {
this.callbacks.error(transferErrors.mvError(err), () => this.moveFolder())
} else {
rimraf(this.sourceFolder) // Delete temp folder
this.callbacks.complete()
}
})
}
}
/**
* Stop the process of transfering the file. (no more events will be fired after this is called)
*/
cancelTransfer() {
this.wasCanceled = true
}
}