mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-09 05:09:39 +00:00
Fix compiler issues
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
@@ -15,8 +15,8 @@
|
||||
"main": "dist/electron/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "run-p serve:angular serve:electron",
|
||||
"serve:electron": "wait-on http-get://localhost:4200/ && nodemon --exec \"tsc -p tsconfig.electron.json && electron . --dev\" --watch src/electron -e ts",
|
||||
"start": "concurrently \"npm run serve:angular\" \"npm run serve:electron\" -n angular,electron -c red,yellow",
|
||||
"serve:electron": "nodemon --exec \"tsc -p tsconfig.electron.json && electron . --dev\" --watch src/electron -e ts",
|
||||
"lint": "ng lint",
|
||||
"clean": "rimraf dist release",
|
||||
"build:windows": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --windows",
|
||||
@@ -44,6 +44,7 @@
|
||||
"fomantic-ui": "^2.8.3",
|
||||
"jquery": "^3.5.1",
|
||||
"jsonfile": "^6.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mv": "^2.1.1",
|
||||
"randombytes": "^2.1.0",
|
||||
"rimraf": "^5.0.5",
|
||||
@@ -64,6 +65,7 @@
|
||||
"@angular/language-service": "^17.0.4",
|
||||
"@types/cli-color": "^2.0.0",
|
||||
"@types/jsonfile": "^6.0.0",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/mv": "^2.1.0",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/randombytes": "^2.0.0",
|
||||
@@ -87,4 +89,4 @@
|
||||
"tsx": "^4.4.0",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -59,6 +59,9 @@ dependencies:
|
||||
jsonfile:
|
||||
specifier: ^6.0.1
|
||||
version: 6.1.0
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
mv:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@@ -115,6 +118,9 @@ devDependencies:
|
||||
'@types/jsonfile':
|
||||
specifier: ^6.0.0
|
||||
version: 6.1.4
|
||||
'@types/lodash':
|
||||
specifier: ^4.14.202
|
||||
version: 4.14.202
|
||||
'@types/mv':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.4
|
||||
@@ -3294,6 +3300,10 @@ packages:
|
||||
dependencies:
|
||||
'@types/node': 14.18.63
|
||||
|
||||
/@types/lodash@4.14.202:
|
||||
resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
|
||||
dev: true
|
||||
|
||||
/@types/mime@1.3.5:
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
dev: true
|
||||
|
||||
@@ -173,7 +173,7 @@ export class ChartSidebarComponent implements OnInit {
|
||||
* @returns a string describing the difficulty number in the selected version.
|
||||
*/
|
||||
private getDiffNumber(instrument: Instrument) {
|
||||
const diffNumber: number = this.selectedVersion[`diff_${instrument}`]
|
||||
const diffNumber: number = (this.selectedVersion as any)[`diff_${instrument}`]
|
||||
return diffNumber == -1 || diffNumber == undefined ? 'Unknown' : String(diffNumber)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Directive, ElementRef, Input } from '@angular/core'
|
||||
|
||||
import * as _ from 'underscore'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
@Directive({
|
||||
selector: '[appProgressBar]',
|
||||
})
|
||||
export class ProgressBarDirective {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _$progressBar: any
|
||||
|
||||
progress: (percent: number) => void
|
||||
|
||||
@Input() set percent(percent: number) {
|
||||
@@ -23,5 +26,4 @@ export class ProgressBarDirective {
|
||||
}
|
||||
return this._$progressBar
|
||||
}
|
||||
private _$progressBar: any
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import { ElectronService } from './electron.service'
|
||||
})
|
||||
export class AlbumArtService {
|
||||
|
||||
constructor(private electronService: ElectronService) { }
|
||||
|
||||
private imageCache: { [songID: number]: string } = {}
|
||||
|
||||
constructor(private electronService: ElectronService) { }
|
||||
|
||||
async getImage(songID: number): Promise<string | null> {
|
||||
if (this.imageCache[songID] == undefined) {
|
||||
const albumArtResult = await this.electronService.invoke('album-art', songID)
|
||||
|
||||
@@ -5,11 +5,6 @@ import { SearchService } from './search.service'
|
||||
|
||||
// Note: this class prevents event cycles by only emitting events if the checkbox changes
|
||||
|
||||
interface SelectionEvent {
|
||||
songID: number
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { join, parse } from 'path'
|
||||
import { parse } from 'path'
|
||||
import { rimraf } from 'rimraf'
|
||||
|
||||
import { NewDownload, ProgressType } from 'src/electron/shared/interfaces/download.interface'
|
||||
import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface'
|
||||
import { emitIPCEvent } from '../../main'
|
||||
import { hasVideoExtension } from '../../shared/ElectronUtilFunctions'
|
||||
import { interpolate, sanitizeFilename } from '../../shared/UtilFunctions'
|
||||
import { sanitizeFilename } from '../../shared/UtilFunctions'
|
||||
import { getSettings } from '../SettingsHandler.ipc'
|
||||
import { FileDownloader, getDownloader } from './FileDownloader'
|
||||
import { FileExtractor } from './FileExtractor'
|
||||
// import { FileDownloader, getDownloader } from './FileDownloader'
|
||||
import { FilesystemChecker } from './FilesystemChecker'
|
||||
import { FileTransfer } from './FileTransfer'
|
||||
|
||||
@@ -143,23 +142,23 @@ export class ChartDownload {
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
let wasCanceled = false
|
||||
this.cancelFn = () => { wasCanceled = true }
|
||||
const downloader = getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name))
|
||||
// const downloader = getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name))
|
||||
if (wasCanceled) { return }
|
||||
this.cancelFn = () => downloader.cancelDownload()
|
||||
// this.cancelFn = () => downloader.cancelDownload()
|
||||
|
||||
const downloadComplete = this.addDownloadEventListeners(downloader, i)
|
||||
downloader.beginDownload()
|
||||
await downloadComplete
|
||||
// const downloadComplete = this.addDownloadEventListeners(downloader, i)
|
||||
// downloader.beginDownload()
|
||||
// await downloadComplete
|
||||
}
|
||||
|
||||
// EXTRACT FILES
|
||||
if (this.isArchive) {
|
||||
const extractor = new FileExtractor(this.tempPath)
|
||||
this.cancelFn = () => extractor.cancelExtract()
|
||||
// const extractor = new FileExtractor(this.tempPath)
|
||||
// this.cancelFn = () => extractor.cancelExtract()
|
||||
|
||||
const extractComplete = this.addExtractorEventListeners(extractor)
|
||||
extractor.beginExtract()
|
||||
await extractComplete
|
||||
// const extractComplete = this.addExtractorEventListeners(extractor)
|
||||
// extractor.beginExtract()
|
||||
// await extractComplete
|
||||
}
|
||||
|
||||
// TRANSFER FILES
|
||||
@@ -196,68 +195,68 @@ export class ChartDownload {
|
||||
* Defines what happens in response to `FileDownloader` events.
|
||||
* @returns a `Promise` that resolves when the download finishes.
|
||||
*/
|
||||
private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) {
|
||||
let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
||||
let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts
|
||||
let fileProgress = 0
|
||||
// private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) {
|
||||
// let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
||||
// let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts
|
||||
// let fileProgress = 0
|
||||
|
||||
downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => {
|
||||
downloadStartPoint = this.individualFileProgressPortion / 2
|
||||
this.percent =
|
||||
this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2)
|
||||
this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good')
|
||||
})
|
||||
// downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => {
|
||||
// downloadStartPoint = this.individualFileProgressPortion / 2
|
||||
// this.percent =
|
||||
// this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2)
|
||||
// this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good')
|
||||
// })
|
||||
|
||||
downloader.on('requestSent', () => {
|
||||
fileProgress = downloadStartPoint
|
||||
this.percent = this._allFilesProgress + fileProgress
|
||||
this.updateGUI(downloadHeader, 'Sending request...', 'good')
|
||||
})
|
||||
// downloader.on('requestSent', () => {
|
||||
// fileProgress = downloadStartPoint
|
||||
// this.percent = this._allFilesProgress + fileProgress
|
||||
// this.updateGUI(downloadHeader, 'Sending request...', 'good')
|
||||
// })
|
||||
|
||||
downloader.on('downloadProgress', (bytesDownloaded: number) => {
|
||||
downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
||||
const size = Number(this.files[fileIndex].size)
|
||||
fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion)
|
||||
this.percent = this._allFilesProgress + fileProgress
|
||||
this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'good')
|
||||
})
|
||||
// downloader.on('downloadProgress', (bytesDownloaded: number) => {
|
||||
// downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
||||
// const size = Number(this.files[fileIndex].size)
|
||||
// fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion)
|
||||
// this.percent = this._allFilesProgress + fileProgress
|
||||
// this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'good')
|
||||
// })
|
||||
|
||||
downloader.on('error', this.handleError.bind(this))
|
||||
// downloader.on('error', this.handleError.bind(this))
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
downloader.on('complete', () => {
|
||||
this._allFilesProgress += this.individualFileProgressPortion
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
// return new Promise<void>(resolve => {
|
||||
// downloader.on('complete', () => {
|
||||
// this._allFilesProgress += this.individualFileProgressPortion
|
||||
// resolve()
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
/**
|
||||
* Defines what happens in response to `FileExtractor` events.
|
||||
* @returns a `Promise` that resolves when the extraction finishes.
|
||||
*/
|
||||
private addExtractorEventListeners(extractor: FileExtractor) {
|
||||
let archive = ''
|
||||
// private addExtractorEventListeners(extractor: FileExtractor) {
|
||||
// let archive = ''
|
||||
|
||||
extractor.on('start', filename => {
|
||||
archive = filename
|
||||
this.updateGUI(`[${archive}]`, 'Extracting...', 'good')
|
||||
})
|
||||
// extractor.on('start', filename => {
|
||||
// archive = filename
|
||||
// this.updateGUI(`[${archive}]`, 'Extracting...', 'good')
|
||||
// })
|
||||
|
||||
extractor.on('extractProgress', (percent, filecount) => {
|
||||
this.percent = interpolate(percent, 0, 100, 80, 95)
|
||||
this.updateGUI(`[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`, `Extracting... (${percent}%)`, 'good')
|
||||
})
|
||||
// extractor.on('extractProgress', (percent, filecount) => {
|
||||
// this.percent = interpolate(percent, 0, 100, 80, 95)
|
||||
// this.updateGUI(`[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`, `Extracting... (${percent}%)`, 'good')
|
||||
// })
|
||||
|
||||
extractor.on('error', this.handleError.bind(this))
|
||||
// extractor.on('error', this.handleError.bind(this))
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
extractor.on('complete', () => {
|
||||
this.percent = 95
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
// return new Promise<void>(resolve => {
|
||||
// extractor.on('complete', () => {
|
||||
// this.percent = 95
|
||||
// resolve()
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
/**
|
||||
* Defines what happens in response to `FileTransfer` events.
|
||||
|
||||
@@ -1,368 +1,368 @@
|
||||
import Bottleneck from 'bottleneck'
|
||||
import { createWriteStream, writeFile as _writeFile } from 'fs'
|
||||
import { google } from 'googleapis'
|
||||
import * as needle from 'needle'
|
||||
import { join } from 'path'
|
||||
import { Readable } from 'stream'
|
||||
import { inspect, promisify } from 'util'
|
||||
// import Bottleneck from 'bottleneck'
|
||||
// import { createWriteStream, writeFile as _writeFile } from 'fs'
|
||||
// import { google } from 'googleapis'
|
||||
// import * as needle from 'needle'
|
||||
// import { join } from 'path'
|
||||
// import { Readable } from 'stream'
|
||||
// import { inspect, promisify } from 'util'
|
||||
|
||||
import { devLog } from '../../shared/ElectronUtilFunctions'
|
||||
import { tempPath } from '../../shared/Paths'
|
||||
import { AnyFunction } from '../../shared/UtilFunctions'
|
||||
import { DownloadError } from './ChartDownload'
|
||||
// TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
|
||||
import { googleTimer } from './GoogleTimer'
|
||||
// import { devLog } from '../../shared/ElectronUtilFunctions'
|
||||
// import { tempPath } from '../../shared/Paths'
|
||||
// import { AnyFunction } from '../../shared/UtilFunctions'
|
||||
// import { DownloadError } from './ChartDownload'
|
||||
// // TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
|
||||
// import { googleTimer } from './GoogleTimer'
|
||||
|
||||
const drive = google.drive('v3')
|
||||
const limiter = new Bottleneck({
|
||||
minTime: 200, // Wait 200 ms between API requests
|
||||
})
|
||||
// const drive = google.drive('v3')
|
||||
// const limiter = new Bottleneck({
|
||||
// minTime: 200, // Wait 200 ms between API requests
|
||||
// })
|
||||
|
||||
const RETRY_MAX = 2
|
||||
const writeFile = promisify(_writeFile)
|
||||
// const RETRY_MAX = 2
|
||||
// const writeFile = promisify(_writeFile)
|
||||
|
||||
interface EventCallback {
|
||||
'waitProgress': (remainingSeconds: number, totalSeconds: number) => void
|
||||
/** Note: this event can be called multiple times if the connection times out or a large file is downloaded */
|
||||
'requestSent': () => void
|
||||
'downloadProgress': (bytesDownloaded: number) => void
|
||||
/** Note: after calling retry, the event lifecycle restarts */
|
||||
'error': (err: DownloadError, retry: () => void) => void
|
||||
'complete': () => void
|
||||
}
|
||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||
export type FileDownloader = APIFileDownloader | SlowFileDownloader
|
||||
// interface EventCallback {
|
||||
// 'waitProgress': (remainingSeconds: number, totalSeconds: number) => void
|
||||
// /** Note: this event can be called multiple times if the connection times out or a large file is downloaded */
|
||||
// 'requestSent': () => void
|
||||
// 'downloadProgress': (bytesDownloaded: number) => void
|
||||
// /** Note: after calling retry, the event lifecycle restarts */
|
||||
// 'error': (err: DownloadError, retry: () => void) => void
|
||||
// 'complete': () => void
|
||||
// }
|
||||
// type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||
// export type FileDownloader = APIFileDownloader | SlowFileDownloader
|
||||
|
||||
const downloadErrors = {
|
||||
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: string) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } },
|
||||
htmlError: (path: string) => { return { header: 'Download server returned HTML instead of a file.', body: path, isLink: true } },
|
||||
linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } },
|
||||
}
|
||||
// const downloadErrors = {
|
||||
// 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: string) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } },
|
||||
// htmlError: (path: string) => { return { header: 'Download server returned HTML instead of a file.', body: path, isLink: true } },
|
||||
// linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } },
|
||||
// }
|
||||
|
||||
/**
|
||||
* Downloads a file from `url` to `fullPath`.
|
||||
* Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
||||
* On error, provides the ability to retry.
|
||||
* Will only send download requests once every `getSettings().rateLimitDelay` seconds if a Google account has not been authenticated.
|
||||
* @param url The download link.
|
||||
* @param fullPath The full path to where this file should be stored (including the filename).
|
||||
*/
|
||||
export function getDownloader(url: string, fullPath: string): FileDownloader {
|
||||
return new SlowFileDownloader(url, fullPath)
|
||||
}
|
||||
// /**
|
||||
// * Downloads a file from `url` to `fullPath`.
|
||||
// * Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
||||
// * On error, provides the ability to retry.
|
||||
// * Will only send download requests once every `getSettings().rateLimitDelay` seconds.
|
||||
// */
|
||||
// class SlowFileDownloader {
|
||||
|
||||
/**
|
||||
* Downloads a file from `url` to `fullPath`.
|
||||
* On error, provides the ability to retry.
|
||||
*/
|
||||
class APIFileDownloader {
|
||||
private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u
|
||||
// private callbacks = {} as Callbacks
|
||||
// private retryCount: number
|
||||
// private wasCanceled = false
|
||||
// private req: NodeJS.ReadableStream
|
||||
|
||||
private callbacks = {} as Callbacks
|
||||
private retryCount: number
|
||||
private wasCanceled = false
|
||||
private fileID: string
|
||||
private downloadStream: Readable
|
||||
// /**
|
||||
// * @param url The download link.
|
||||
// * @param fullPath The full path to where this file should be stored (including the filename).
|
||||
// */
|
||||
// constructor(private url: string, private fullPath: string) { }
|
||||
|
||||
/**
|
||||
* @param url The download link.
|
||||
* @param fullPath The full path to where this file should be stored (including the filename).
|
||||
*/
|
||||
constructor(private url: string, private fullPath: string) {
|
||||
// url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download"
|
||||
this.fileID = this.URL_REGEX.exec(url)[1]
|
||||
}
|
||||
// /**
|
||||
// * 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
|
||||
// }
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
// /**
|
||||
// * Download the file after waiting for the google rate limit.
|
||||
// */
|
||||
// beginDownload() {
|
||||
// googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => {
|
||||
// this.callbacks.waitProgress(remainingSeconds, totalSeconds)
|
||||
// }))
|
||||
|
||||
/**
|
||||
* Download the file after waiting for the google rate limit.
|
||||
*/
|
||||
beginDownload() {
|
||||
if (this.fileID == undefined) {
|
||||
this.failDownload(downloadErrors.linkError(this.url))
|
||||
}
|
||||
// googleTimer.on('complete', this.cancelable(() => {
|
||||
// this.requestDownload()
|
||||
// }))
|
||||
// }
|
||||
|
||||
this.startDownloadStream()
|
||||
}
|
||||
// /**
|
||||
// * Sends a request to download the file at `this.url`.
|
||||
// * @param cookieHeader the "cookie=" header to include this request.
|
||||
// */
|
||||
// private requestDownload(cookieHeader?: string) {
|
||||
// this.callbacks.requestSent()
|
||||
// this.req = needle.get(this.url, {
|
||||
// 'follow_max': 10,
|
||||
// 'open_timeout': 5000,
|
||||
// 'headers': Object.assign({
|
||||
// 'Referer': this.url,
|
||||
// 'Accept': '*/*',
|
||||
// },
|
||||
// (cookieHeader ? { 'Cookie': cookieHeader } : undefined)
|
||||
// ),
|
||||
// })
|
||||
|
||||
/**
|
||||
* Uses the Google Drive API to start a download stream for the file with `this.fileID`.
|
||||
*/
|
||||
private startDownloadStream() {
|
||||
limiter.schedule(this.cancelable(async () => {
|
||||
this.callbacks.requestSent()
|
||||
try {
|
||||
this.downloadStream = (await drive.files.get({
|
||||
fileId: this.fileID,
|
||||
alt: 'media',
|
||||
}, {
|
||||
responseType: 'stream',
|
||||
})).data
|
||||
// this.req.on('timeout', this.cancelable((type: string) => {
|
||||
// this.retryCount++
|
||||
// if (this.retryCount <= RETRY_MAX) {
|
||||
// devLog(`TIMEOUT: Retry attempt ${this.retryCount}...`)
|
||||
// this.requestDownload(cookieHeader)
|
||||
// } else {
|
||||
// this.failDownload(downloadErrors.timeout(type))
|
||||
// }
|
||||
// }))
|
||||
|
||||
if (this.wasCanceled) { return }
|
||||
// this.req.on('err', this.cancelable((err: Error) => {
|
||||
// this.failDownload(downloadErrors.connectionError(err))
|
||||
// }))
|
||||
|
||||
this.handleDownloadResponse()
|
||||
} catch (err) {
|
||||
this.retryCount++
|
||||
if (this.retryCount <= RETRY_MAX) {
|
||||
devLog(`Failed to get file: Retry attempt ${this.retryCount}...`)
|
||||
if (this.wasCanceled) { return }
|
||||
this.startDownloadStream()
|
||||
} else {
|
||||
devLog(inspect(err))
|
||||
if (err?.code && err?.response?.statusText) {
|
||||
this.failDownload(downloadErrors.responseError(`${err.code} (${err.response.statusText})`))
|
||||
} else {
|
||||
this.failDownload(downloadErrors.responseError(err?.code ?? 'unknown'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
// this.req.on('header', this.cancelable((statusCode, headers: Headers) => {
|
||||
// if (statusCode != 200) {
|
||||
// this.failDownload(downloadErrors.responseError(statusCode))
|
||||
// return
|
||||
// }
|
||||
|
||||
/**
|
||||
* Pipes the data from a download response to `this.fullPath`.
|
||||
* @param req The download request.
|
||||
*/
|
||||
private handleDownloadResponse() {
|
||||
this.callbacks.downloadProgress(0)
|
||||
let downloadedSize = 0
|
||||
const writeStream = createWriteStream(this.fullPath)
|
||||
// if (headers['content-type'].startsWith('text/html')) {
|
||||
// this.handleHTMLResponse(headers['set-cookie'])
|
||||
// } else {
|
||||
// this.handleDownloadResponse()
|
||||
// }
|
||||
// }))
|
||||
// }
|
||||
|
||||
try {
|
||||
this.downloadStream.pipe(writeStream)
|
||||
} catch (err) {
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
}
|
||||
// /**
|
||||
// * 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 cookieHeader The "cookie=" header of this request.
|
||||
// */
|
||||
// private handleHTMLResponse(cookieHeader: string) {
|
||||
// let virusScanHTML = ''
|
||||
// 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 {
|
||||
// try {
|
||||
// const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g
|
||||
// const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML)
|
||||
// const confirmToken = confirmTokenResults[1]
|
||||
// const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length)
|
||||
// this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}`
|
||||
// const warningCode = /download_warning_([^=]*)=/.exec(cookieHeader)[1]
|
||||
// const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%')
|
||||
// const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}`
|
||||
// this.requestDownload(newHeader)
|
||||
// } catch (e) {
|
||||
// this.saveHTMLError(virusScanHTML).then(path => {
|
||||
// this.failDownload(downloadErrors.htmlError(path))
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }))
|
||||
// }
|
||||
|
||||
this.downloadStream.on('data', this.cancelable((chunk: Buffer) => {
|
||||
downloadedSize += chunk.length
|
||||
}))
|
||||
// /**
|
||||
// * Pipes the data from a download response to `this.fullPath`.
|
||||
// * @param req The download request.
|
||||
// */
|
||||
// private handleDownloadResponse() {
|
||||
// this.callbacks.downloadProgress(0)
|
||||
// let downloadedSize = 0
|
||||
// this.req.pipe(createWriteStream(this.fullPath))
|
||||
// this.req.on('data', this.cancelable(data => {
|
||||
// downloadedSize += data.length
|
||||
// this.callbacks.downloadProgress(downloadedSize)
|
||||
// }))
|
||||
|
||||
const progressUpdater = setInterval(() => {
|
||||
this.callbacks.downloadProgress(downloadedSize)
|
||||
}, 100)
|
||||
// this.req.on('err', this.cancelable((err: Error) => {
|
||||
// this.failDownload(downloadErrors.connectionError(err))
|
||||
// }))
|
||||
|
||||
this.downloadStream.on('error', this.cancelable((err: Error) => {
|
||||
clearInterval(progressUpdater)
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
}))
|
||||
// this.req.on('end', this.cancelable(() => {
|
||||
// this.callbacks.complete()
|
||||
// }))
|
||||
// }
|
||||
|
||||
this.downloadStream.on('end', this.cancelable(() => {
|
||||
clearInterval(progressUpdater)
|
||||
writeStream.end()
|
||||
this.downloadStream.destroy()
|
||||
this.downloadStream = null
|
||||
// private async saveHTMLError(text: string) {
|
||||
// const errorPath = join(tempPath, 'HTMLError.html')
|
||||
// await writeFile(errorPath, text)
|
||||
// return errorPath
|
||||
// }
|
||||
|
||||
this.callbacks.complete()
|
||||
}))
|
||||
}
|
||||
// /**
|
||||
// * Display an error message and provide a function to retry the download.
|
||||
// */
|
||||
// private failDownload(error: DownloadError) {
|
||||
// this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
||||
// }
|
||||
|
||||
/**
|
||||
* Display an error message and provide a function to retry the download.
|
||||
*/
|
||||
private failDownload(error: DownloadError) {
|
||||
this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
||||
}
|
||||
// /**
|
||||
// * Stop the process of downloading the file. (no more events will be fired after this is called)
|
||||
// */
|
||||
// cancelDownload() {
|
||||
// this.wasCanceled = true
|
||||
// googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
||||
// if (this.req) {
|
||||
// // TODO: destroy request
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Stop the process of downloading the file. (no more events will be fired after this is called)
|
||||
*/
|
||||
cancelDownload() {
|
||||
this.wasCanceled = true
|
||||
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
||||
if (this.downloadStream) {
|
||||
this.downloadStream.destroy()
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
||||
// */
|
||||
// private cancelable<F extends AnyFunction>(fn: F) {
|
||||
// return (...args: Parameters<F>): ReturnType<F> => {
|
||||
// if (this.wasCanceled) { return }
|
||||
// return fn(...Array.from(args))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
||||
*/
|
||||
private cancelable<F extends AnyFunction>(fn: F) {
|
||||
return (...args: Parameters<F>): ReturnType<F> => {
|
||||
if (this.wasCanceled) { return }
|
||||
return fn(...Array.from(args))
|
||||
}
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Downloads a file from `url` to `fullPath`.
|
||||
// * On error, provides the ability to retry.
|
||||
// */
|
||||
// class APIFileDownloader {
|
||||
// private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u
|
||||
|
||||
/**
|
||||
* Downloads a file from `url` to `fullPath`.
|
||||
* Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
||||
* On error, provides the ability to retry.
|
||||
* Will only send download requests once every `getSettings().rateLimitDelay` seconds.
|
||||
*/
|
||||
class SlowFileDownloader {
|
||||
// private callbacks = {} as Callbacks
|
||||
// private retryCount: number
|
||||
// private wasCanceled = false
|
||||
// private fileID: string
|
||||
// private downloadStream: Readable
|
||||
|
||||
private callbacks = {} as Callbacks
|
||||
private retryCount: number
|
||||
private wasCanceled = false
|
||||
private req: NodeJS.ReadableStream
|
||||
// /**
|
||||
// * @param url The download link.
|
||||
// * @param fullPath The full path to where this file should be stored (including the filename).
|
||||
// */
|
||||
// constructor(private url: string, private fullPath: string) {
|
||||
// // url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download"
|
||||
// this.fileID = this.URL_REGEX.exec(url)[1]
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param url The download link.
|
||||
* @param fullPath The full path to where this file should be stored (including the filename).
|
||||
*/
|
||||
constructor(private url: string, private fullPath: 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
|
||||
// }
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
// /**
|
||||
// * Download the file after waiting for the google rate limit.
|
||||
// */
|
||||
// beginDownload() {
|
||||
// if (this.fileID == undefined) {
|
||||
// this.failDownload(downloadErrors.linkError(this.url))
|
||||
// }
|
||||
|
||||
/**
|
||||
* Download the file after waiting for the google rate limit.
|
||||
*/
|
||||
beginDownload() {
|
||||
googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => {
|
||||
this.callbacks.waitProgress(remainingSeconds, totalSeconds)
|
||||
}))
|
||||
// this.startDownloadStream()
|
||||
// }
|
||||
|
||||
googleTimer.on('complete', this.cancelable(() => {
|
||||
this.requestDownload()
|
||||
}))
|
||||
}
|
||||
// /**
|
||||
// * Uses the Google Drive API to start a download stream for the file with `this.fileID`.
|
||||
// */
|
||||
// private startDownloadStream() {
|
||||
// limiter.schedule(this.cancelable(async () => {
|
||||
// this.callbacks.requestSent()
|
||||
// try {
|
||||
// this.downloadStream = (await drive.files.get({
|
||||
// fileId: this.fileID,
|
||||
// alt: 'media',
|
||||
// }, {
|
||||
// responseType: 'stream',
|
||||
// })).data
|
||||
|
||||
/**
|
||||
* Sends a request to download the file at `this.url`.
|
||||
* @param cookieHeader the "cookie=" header to include this request.
|
||||
*/
|
||||
private requestDownload(cookieHeader?: string) {
|
||||
this.callbacks.requestSent()
|
||||
this.req = needle.get(this.url, {
|
||||
'follow_max': 10,
|
||||
'open_timeout': 5000,
|
||||
'headers': Object.assign({
|
||||
'Referer': this.url,
|
||||
'Accept': '*/*',
|
||||
},
|
||||
(cookieHeader ? { 'Cookie': cookieHeader } : undefined)
|
||||
),
|
||||
})
|
||||
// if (this.wasCanceled) { return }
|
||||
|
||||
this.req.on('timeout', this.cancelable((type: string) => {
|
||||
this.retryCount++
|
||||
if (this.retryCount <= RETRY_MAX) {
|
||||
devLog(`TIMEOUT: Retry attempt ${this.retryCount}...`)
|
||||
this.requestDownload(cookieHeader)
|
||||
} else {
|
||||
this.failDownload(downloadErrors.timeout(type))
|
||||
}
|
||||
}))
|
||||
// this.handleDownloadResponse()
|
||||
// } catch (err) {
|
||||
// this.retryCount++
|
||||
// if (this.retryCount <= RETRY_MAX) {
|
||||
// devLog(`Failed to get file: Retry attempt ${this.retryCount}...`)
|
||||
// if (this.wasCanceled) { return }
|
||||
// this.startDownloadStream()
|
||||
// } else {
|
||||
// devLog(inspect(err))
|
||||
// if (err?.code && err?.response?.statusText) {
|
||||
// this.failDownload(downloadErrors.responseError(`${err.code} (${err.response.statusText})`))
|
||||
// } else {
|
||||
// this.failDownload(downloadErrors.responseError(err?.code ?? 'unknown'))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }))
|
||||
// }
|
||||
|
||||
this.req.on('err', this.cancelable((err: Error) => {
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
}))
|
||||
// /**
|
||||
// * Pipes the data from a download response to `this.fullPath`.
|
||||
// * @param req The download request.
|
||||
// */
|
||||
// private handleDownloadResponse() {
|
||||
// this.callbacks.downloadProgress(0)
|
||||
// let downloadedSize = 0
|
||||
// const writeStream = createWriteStream(this.fullPath)
|
||||
|
||||
this.req.on('header', this.cancelable((statusCode, headers: Headers) => {
|
||||
if (statusCode != 200) {
|
||||
this.failDownload(downloadErrors.responseError(statusCode))
|
||||
return
|
||||
}
|
||||
// try {
|
||||
// this.downloadStream.pipe(writeStream)
|
||||
// } catch (err) {
|
||||
// this.failDownload(downloadErrors.connectionError(err))
|
||||
// }
|
||||
|
||||
if (headers['content-type'].startsWith('text/html')) {
|
||||
this.handleHTMLResponse(headers['set-cookie'])
|
||||
} else {
|
||||
this.handleDownloadResponse()
|
||||
}
|
||||
}))
|
||||
}
|
||||
// this.downloadStream.on('data', this.cancelable((chunk: Buffer) => {
|
||||
// downloadedSize += chunk.length
|
||||
// }))
|
||||
|
||||
/**
|
||||
* 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 cookieHeader The "cookie=" header of this request.
|
||||
*/
|
||||
private handleHTMLResponse(cookieHeader: string) {
|
||||
let virusScanHTML = ''
|
||||
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 {
|
||||
try {
|
||||
const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g
|
||||
const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML)
|
||||
const confirmToken = confirmTokenResults[1]
|
||||
const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length)
|
||||
this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}`
|
||||
const warningCode = /download_warning_([^=]*)=/.exec(cookieHeader)[1]
|
||||
const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%')
|
||||
const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}`
|
||||
this.requestDownload(newHeader)
|
||||
} catch (e) {
|
||||
this.saveHTMLError(virusScanHTML).then(path => {
|
||||
this.failDownload(downloadErrors.htmlError(path))
|
||||
})
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
// const progressUpdater = setInterval(() => {
|
||||
// this.callbacks.downloadProgress(downloadedSize)
|
||||
// }, 100)
|
||||
|
||||
/**
|
||||
* Pipes the data from a download response to `this.fullPath`.
|
||||
* @param req The download request.
|
||||
*/
|
||||
private handleDownloadResponse() {
|
||||
this.callbacks.downloadProgress(0)
|
||||
let downloadedSize = 0
|
||||
this.req.pipe(createWriteStream(this.fullPath))
|
||||
this.req.on('data', this.cancelable(data => {
|
||||
downloadedSize += data.length
|
||||
this.callbacks.downloadProgress(downloadedSize)
|
||||
}))
|
||||
// this.downloadStream.on('error', this.cancelable((err: Error) => {
|
||||
// clearInterval(progressUpdater)
|
||||
// this.failDownload(downloadErrors.connectionError(err))
|
||||
// }))
|
||||
|
||||
this.req.on('err', this.cancelable((err: Error) => {
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
}))
|
||||
// this.downloadStream.on('end', this.cancelable(() => {
|
||||
// clearInterval(progressUpdater)
|
||||
// writeStream.end()
|
||||
// this.downloadStream.destroy()
|
||||
// this.downloadStream = null
|
||||
|
||||
this.req.on('end', this.cancelable(() => {
|
||||
this.callbacks.complete()
|
||||
}))
|
||||
}
|
||||
// this.callbacks.complete()
|
||||
// }))
|
||||
// }
|
||||
|
||||
private async saveHTMLError(text: string) {
|
||||
const errorPath = join(tempPath, 'HTMLError.html')
|
||||
await writeFile(errorPath, text)
|
||||
return errorPath
|
||||
}
|
||||
// /**
|
||||
// * Display an error message and provide a function to retry the download.
|
||||
// */
|
||||
// private failDownload(error: DownloadError) {
|
||||
// this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
||||
// }
|
||||
|
||||
/**
|
||||
* Display an error message and provide a function to retry the download.
|
||||
*/
|
||||
private failDownload(error: DownloadError) {
|
||||
this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
||||
}
|
||||
// /**
|
||||
// * Stop the process of downloading the file. (no more events will be fired after this is called)
|
||||
// */
|
||||
// cancelDownload() {
|
||||
// this.wasCanceled = true
|
||||
// googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
||||
// if (this.downloadStream) {
|
||||
// this.downloadStream.destroy()
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Stop the process of downloading the file. (no more events will be fired after this is called)
|
||||
*/
|
||||
cancelDownload() {
|
||||
this.wasCanceled = true
|
||||
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
||||
if (this.req) {
|
||||
// TODO: destroy request
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
||||
// */
|
||||
// private cancelable<F extends AnyFunction>(fn: F) {
|
||||
// return (...args: Parameters<F>): ReturnType<F> => {
|
||||
// if (this.wasCanceled) { return }
|
||||
// return fn(...Array.from(args))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
||||
*/
|
||||
private cancelable<F extends AnyFunction>(fn: F) {
|
||||
return (...args: Parameters<F>): ReturnType<F> => {
|
||||
if (this.wasCanceled) { return }
|
||||
return fn(...Array.from(args))
|
||||
}
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Downloads a file from `url` to `fullPath`.
|
||||
// * Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
||||
// * On error, provides the ability to retry.
|
||||
// * Will only send download requests once every `getSettings().rateLimitDelay` seconds if a Google account has not been authenticated.
|
||||
// * @param url The download link.
|
||||
// * @param fullPath The full path to where this file should be stored (including the filename).
|
||||
// */
|
||||
// export function getDownloader(url: string, fullPath: string): FileDownloader {
|
||||
// return new SlowFileDownloader(url, fullPath)
|
||||
// }
|
||||
|
||||
@@ -1,164 +1,170 @@
|
||||
import * as zipBin from '7zip-bin'
|
||||
import { mkdir as _mkdir, readdir, unlink } from 'fs'
|
||||
import * as node7z from 'node-7z'
|
||||
import * as unrarjs from 'node-unrar-js' // TODO find better rar library that has async extraction
|
||||
import { FailReason } from 'node-unrar-js/dist/js/extractor'
|
||||
import { extname, join } from 'path'
|
||||
import { promisify } from 'util'
|
||||
// import * as zipBin from '7zip-bin'
|
||||
// import { mkdir as _mkdir, readdir, unlink } from 'fs'
|
||||
// import * as node7z from 'node-7z'
|
||||
// import * as unrarjs from 'node-unrar-js' // TODO find better rar library that has async extraction
|
||||
// import { FailReason } from 'node-unrar-js/dist/js/extractor'
|
||||
// import { extname, join } from 'path'
|
||||
// import { promisify } from 'util'
|
||||
|
||||
import { devLog } from '../../shared/ElectronUtilFunctions'
|
||||
import { AnyFunction } from '../../shared/UtilFunctions'
|
||||
import { DownloadError } from './ChartDownload'
|
||||
// import { devLog } from '../../shared/ElectronUtilFunctions'
|
||||
// import { AnyFunction } from '../../shared/UtilFunctions'
|
||||
// import { DownloadError } from './ChartDownload'
|
||||
|
||||
const mkdir = promisify(_mkdir)
|
||||
// const mkdir = promisify(_mkdir)
|
||||
|
||||
interface EventCallback {
|
||||
'start': (filename: string) => void
|
||||
'extractProgress': (percent: number, fileCount: number) => void
|
||||
'error': (err: DownloadError, retry: () => void | Promise<void>) => void
|
||||
'complete': () => void
|
||||
}
|
||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||
// interface EventCallback {
|
||||
// 'start': (filename: string) => void
|
||||
// 'extractProgress': (percent: number, fileCount: number) => void
|
||||
// 'error': (err: DownloadError, retry: () => void | Promise<void>) => void
|
||||
// 'complete': () => void
|
||||
// }
|
||||
// type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||
|
||||
const extractErrors = {
|
||||
readError: (err: NodeJS.ErrnoException) => { return { header: `Failed to read file (${err.code})`, body: `${err.name}: ${err.message}` } },
|
||||
emptyError: () => { return { header: 'Failed to extract archive', body: 'File archive was downloaded but could not be found' } },
|
||||
rarmkdirError: (err: NodeJS.ErrnoException, sourceFile: string) => {
|
||||
return { header: `Extracting archive failed. (${err.code})`, body: `${err.name}: ${err.message} (${sourceFile})` }
|
||||
},
|
||||
rarextractError: (result: { reason: FailReason; msg: string }, sourceFile: string) => {
|
||||
return { header: `Extracting archive failed: ${result.reason}`, body: `${result.msg} (${sourceFile})` }
|
||||
},
|
||||
}
|
||||
// const extractErrors = {
|
||||
// readError: (err: NodeJS.ErrnoException) => ({ header: `Failed to read file (${err.code})`, body: `${err.name}: ${err.message}` }),
|
||||
// emptyError: () => ({ header: 'Failed to extract archive', body: 'File archive was downloaded but could not be found' }),
|
||||
// rarmkdirError: (err: NodeJS.ErrnoException, sourceFile: string) => {
|
||||
// return { header: `Extracting archive failed. (${err.code})`, body: `${err.name}: ${err.message} (${sourceFile})` }
|
||||
// },
|
||||
// rarextractError: (result: { reason: FailReason; msg: string }, sourceFile: string) => {
|
||||
// return { header: `Extracting archive failed: ${result.reason}`, body: `${result.msg} (${sourceFile})` }
|
||||
// },
|
||||
// }
|
||||
|
||||
export class FileExtractor {
|
||||
// export class FileExtractor {
|
||||
|
||||
private callbacks = {} as Callbacks
|
||||
private wasCanceled = false
|
||||
constructor(private sourceFolder: string) { }
|
||||
// private callbacks = {} as Callbacks
|
||||
// private wasCanceled = false
|
||||
// constructor(private sourceFolder: string) { }
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
// /**
|
||||
// * 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
|
||||
// }
|
||||
|
||||
/**
|
||||
* Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder)
|
||||
*/
|
||||
beginExtract() {
|
||||
setTimeout(this.cancelable(() => {
|
||||
readdir(this.sourceFolder, (err, files) => {
|
||||
if (err) {
|
||||
this.callbacks.error(extractErrors.readError(err), () => this.beginExtract())
|
||||
} else if (files.length == 0) {
|
||||
this.callbacks.error(extractErrors.emptyError(), () => this.beginExtract())
|
||||
} else {
|
||||
this.callbacks.start(files[0])
|
||||
this.extract(join(this.sourceFolder, files[0]), extname(files[0]) == '.rar')
|
||||
}
|
||||
})
|
||||
}), 100) // Wait for filesystem to process downloaded file
|
||||
}
|
||||
// /**
|
||||
// * Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder)
|
||||
// */
|
||||
// beginExtract() {
|
||||
// setTimeout(this.cancelable(() => {
|
||||
// readdir(this.sourceFolder, (err, files) => {
|
||||
// if (err) {
|
||||
// this.callbacks.error(extractErrors.readError(err), () => this.beginExtract())
|
||||
// } else if (files.length == 0) {
|
||||
// this.callbacks.error(extractErrors.emptyError(), () => this.beginExtract())
|
||||
// } else {
|
||||
// this.callbacks.start(files[0])
|
||||
// this.extract(join(this.sourceFolder, files[0]), extname(files[0]) == '.rar')
|
||||
// }
|
||||
// })
|
||||
// }), 100) // Wait for filesystem to process downloaded file
|
||||
// }
|
||||
|
||||
/**
|
||||
* Extracts the file at `fullPath` to `this.sourceFolder`.
|
||||
*/
|
||||
private async extract(fullPath: string, useRarExtractor: boolean) {
|
||||
if (useRarExtractor) {
|
||||
await this.extractRar(fullPath) // Use node-unrar-js to extract the archive
|
||||
} else {
|
||||
this.extract7z(fullPath) // Use node-7z to extract the archive
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Extracts the file at `fullPath` to `this.sourceFolder`.
|
||||
// */
|
||||
// private async extract(fullPath: string, useRarExtractor: boolean) {
|
||||
// if (useRarExtractor) {
|
||||
// await this.extractRar(fullPath) // Use node-unrar-js to extract the archive
|
||||
// } else {
|
||||
// this.extract7z(fullPath) // Use node-7z to extract the archive
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Extracts a .rar archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
|
||||
* @throws an `ExtractError` if this fails.
|
||||
*/
|
||||
private async extractRar(fullPath: string) {
|
||||
const extractor = unrarjs.createExtractorFromFile(fullPath, this.sourceFolder)
|
||||
// /**
|
||||
// * Extracts a .rar archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
|
||||
// * @throws an `ExtractError` if this fails.
|
||||
// */
|
||||
// private async extractRar(fullPath: string) {
|
||||
// const extractor = unrarjs.createExtractorFromFile(fullPath, this.sourceFolder)
|
||||
|
||||
const fileList = extractor.getFileList()
|
||||
// const fileList = extractor.getFileList()
|
||||
|
||||
if (fileList[0].state != 'FAIL') {
|
||||
// if (fileList[0].state != 'FAIL') {
|
||||
|
||||
// Create directories for nested archives (because unrarjs didn't feel like handling that automatically)
|
||||
const headers = fileList[1].fileHeaders
|
||||
for (const header of headers) {
|
||||
if (header.flags.directory) {
|
||||
try {
|
||||
await mkdir(join(this.sourceFolder, header.name), { recursive: true })
|
||||
} catch (err) {
|
||||
this.callbacks.error(extractErrors.rarmkdirError(err, fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar'))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// // Create directories for nested archives (because unrarjs didn't feel like handling that automatically)
|
||||
// const headers = fileList[1].fileHeaders
|
||||
// for (const header of headers) {
|
||||
// if (header.flags.directory) {
|
||||
// try {
|
||||
// await mkdir(join(this.sourceFolder, header.name), { recursive: true })
|
||||
// } catch (err) {
|
||||
// this.callbacks.error(
|
||||
// extractErrors.rarmkdirError(err, fullPath),
|
||||
// () => this.extract(fullPath, extname(fullPath) == '.rar'),
|
||||
// )
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const extractResult = extractor.extractAll()
|
||||
// const extractResult = extractor.extractAll()
|
||||
|
||||
if (extractResult[0].state == 'FAIL') {
|
||||
this.callbacks.error(extractErrors.rarextractError(extractResult[0], fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar'))
|
||||
} else {
|
||||
this.deleteArchive(fullPath)
|
||||
}
|
||||
}
|
||||
// if (extractResult[0].state == 'FAIL') {
|
||||
// this.callbacks.error(
|
||||
// extractErrors.rarextractError(extractResult[0], fullPath),
|
||||
// () => this.extract(fullPath, extname(fullPath) == '.rar'),
|
||||
// )
|
||||
// } else {
|
||||
// this.deleteArchive(fullPath)
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
|
||||
*/
|
||||
private extract7z(fullPath: string) {
|
||||
const zipBinPath = zipBin.path7za.replace('app.asar', 'app.asar.unpacked') // I love electron-builder packaging :)
|
||||
const stream = node7z.extractFull(fullPath, this.sourceFolder, { $progress: true, $bin: zipBinPath })
|
||||
// /**
|
||||
// * Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
|
||||
// */
|
||||
// private extract7z(fullPath: string) {
|
||||
// const zipBinPath = zipBin.path7za.replace('app.asar', 'app.asar.unpacked') // I love electron-builder packaging :)
|
||||
// const stream = node7z.extractFull(fullPath, this.sourceFolder, { $progress: true, $bin: zipBinPath })
|
||||
|
||||
stream.on('progress', this.cancelable((progress: { percent: number; fileCount: number }) => {
|
||||
this.callbacks.extractProgress(progress.percent, isNaN(progress.fileCount) ? 0 : progress.fileCount)
|
||||
}))
|
||||
// stream.on('progress', this.cancelable((progress: { percent: number; fileCount: number }) => {
|
||||
// this.callbacks.extractProgress(progress.percent, isNaN(progress.fileCount) ? 0 : progress.fileCount)
|
||||
// }))
|
||||
|
||||
let extractErrorOccured = false
|
||||
stream.on('error', this.cancelable(() => {
|
||||
extractErrorOccured = true
|
||||
devLog(`Failed to extract [${fullPath}]; retrying with .rar extractor...`)
|
||||
this.extract(fullPath, true)
|
||||
}))
|
||||
// let extractErrorOccured = false
|
||||
// stream.on('error', this.cancelable(() => {
|
||||
// extractErrorOccured = true
|
||||
// devLog(`Failed to extract [${fullPath}]; retrying with .rar extractor...`)
|
||||
// this.extract(fullPath, true)
|
||||
// }))
|
||||
|
||||
stream.on('end', this.cancelable(() => {
|
||||
if (!extractErrorOccured) {
|
||||
this.deleteArchive(fullPath)
|
||||
}
|
||||
}))
|
||||
}
|
||||
// stream.on('end', this.cancelable(() => {
|
||||
// if (!extractErrorOccured) {
|
||||
// this.deleteArchive(fullPath)
|
||||
// }
|
||||
// }))
|
||||
// }
|
||||
|
||||
/**
|
||||
* Tries to delete the archive at `fullPath`.
|
||||
*/
|
||||
private deleteArchive(fullPath: string) {
|
||||
unlink(fullPath, this.cancelable(err => {
|
||||
if (err && err.code != 'ENOENT') {
|
||||
devLog(`Warning: failed to delete archive at [${fullPath}]`)
|
||||
}
|
||||
// /**
|
||||
// * Tries to delete the archive at `fullPath`.
|
||||
// */
|
||||
// private deleteArchive(fullPath: string) {
|
||||
// unlink(fullPath, this.cancelable(err => {
|
||||
// if (err && err.code != 'ENOENT') {
|
||||
// devLog(`Warning: failed to delete archive at [${fullPath}]`)
|
||||
// }
|
||||
|
||||
this.callbacks.complete()
|
||||
}))
|
||||
}
|
||||
// this.callbacks.complete()
|
||||
// }))
|
||||
// }
|
||||
|
||||
/**
|
||||
* Stop the process of extracting the file. (no more events will be fired after this is called)
|
||||
*/
|
||||
cancelExtract() {
|
||||
this.wasCanceled = true
|
||||
}
|
||||
// /**
|
||||
// * Stop the process of extracting the file. (no more events will be fired after this is called)
|
||||
// */
|
||||
// cancelExtract() {
|
||||
// this.wasCanceled = true
|
||||
// }
|
||||
|
||||
/**
|
||||
* Wraps a function that is able to be prevented if `this.cancelExtract()` was called.
|
||||
*/
|
||||
private cancelable<F extends AnyFunction>(fn: F) {
|
||||
return (...args: Parameters<F>): ReturnType<F> => {
|
||||
if (this.wasCanceled) { return }
|
||||
return fn(...Array.from(args))
|
||||
}
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Wraps a function that is able to be prevented if `this.cancelExtract()` was called.
|
||||
// */
|
||||
// private cancelable<F extends AnyFunction>(fn: F) {
|
||||
// return (...args: Parameters<F>): ReturnType<F> => {
|
||||
// if (this.wasCanceled) { return }
|
||||
// return fn(...Array.from(args))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dirent, readdir as _readdir } from 'fs'
|
||||
import * as mv from 'mv'
|
||||
import mv from 'mv'
|
||||
import { join } from 'path'
|
||||
import { rimraf } from 'rimraf'
|
||||
import { promisify } from 'util'
|
||||
@@ -20,7 +20,10 @@ 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.${err.code == 'EPERM' ? ' (does the chart already exist?)' : ''}`),
|
||||
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) {
|
||||
|
||||
@@ -19,7 +19,7 @@ interface EventCallback {
|
||||
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.' } },
|
||||
libraryFolder: () => ({ 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, isLink: true }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import * as windowStateKeeper from 'electron-window-state'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import * as path from 'path'
|
||||
import * as url from 'url'
|
||||
|
||||
@@ -9,12 +9,13 @@ import { updateChecker } from './ipc/UpdateHandler.ipc'
|
||||
import { getIPCEmitHandlers, getIPCInvokeHandlers, IPCEmitEvents } from './shared/IPCHandler'
|
||||
import { dataPath } from './shared/Paths'
|
||||
|
||||
require('electron-unhandled')({ showDialog: true })
|
||||
import unhandled = require('electron-unhandled')
|
||||
unhandled({ showDialog: true })
|
||||
|
||||
export let mainWindow: BrowserWindow
|
||||
const args = process.argv.slice(1)
|
||||
const isDevBuild = args.some(val => val == '--dev')
|
||||
const remote = require('@electron/remote/main')
|
||||
import remote = require('@electron/remote/main')
|
||||
|
||||
|
||||
remote.initialize()
|
||||
@@ -66,7 +67,7 @@ function handleOSXWindowClosed() {
|
||||
/**
|
||||
* Launches and initializes Bridge's main window.
|
||||
*/
|
||||
function createBridgeWindow() {
|
||||
async function createBridgeWindow() {
|
||||
|
||||
// Load window size and maximized/restored state from previous session
|
||||
const windowState = windowStateKeeper({
|
||||
@@ -89,7 +90,7 @@ function createBridgeWindow() {
|
||||
getIPCEmitHandlers().map(handler => ipcMain.on(handler.event, (_event, ...args) => handler.handler(args[0])))
|
||||
|
||||
// Load angular app
|
||||
mainWindow.loadURL(getLoadUrl())
|
||||
await loadWindow()
|
||||
|
||||
if (isDevBuild) {
|
||||
mainWindow.webContents.openDevTools()
|
||||
@@ -115,10 +116,9 @@ function createBrowserWindow(windowState: windowStateKeeper.State) {
|
||||
frame: false,
|
||||
title: 'Bridge',
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
// preload:
|
||||
allowRunningInsecureContent: (isDevBuild) ? true : false,
|
||||
textAreasAreResizable: false,
|
||||
contextIsolation: false,
|
||||
},
|
||||
simpleFullscreen: true,
|
||||
fullscreenable: false,
|
||||
@@ -132,6 +132,15 @@ function createBrowserWindow(windowState: windowStateKeeper.State) {
|
||||
return new BrowserWindow(options)
|
||||
}
|
||||
|
||||
async function loadWindow(retries = 0) {
|
||||
if (retries > 10) { throw new Error('Angular frontend did not load') }
|
||||
try {
|
||||
await mainWindow.loadURL(getLoadUrl())
|
||||
} catch (err) {
|
||||
await loadWindow(retries + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load from localhost during development; load from index.html in production
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,6 @@ export function hasVideoExtension(name: string) {
|
||||
* Log a message in the main BrowserWindow's console.
|
||||
* Note: Error objects can't be serialized by this; use inspect(err) before passing it here.
|
||||
*/
|
||||
export function devLog(...messages: any[]) {
|
||||
export function devLog(...messages: unknown[]) {
|
||||
emitIPCEvent('log', messages)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as randomBytes from 'randombytes'
|
||||
|
||||
const sanitize = require('sanitize-filename')
|
||||
import randomBytes from 'randombytes'
|
||||
import sanitize from 'sanitize-filename'
|
||||
|
||||
// WARNING: do not import anything related to Electron; the code will not compile correctly.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
|
||||
Reference in New Issue
Block a user