mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 14:19:38 +00:00
369 lines
13 KiB
TypeScript
369 lines
13 KiB
TypeScript
// 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'
|
|
|
|
// 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)
|
|
|
|
// 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}` } },
|
|
// }
|
|
|
|
// /**
|
|
// * 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 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) { }
|
|
|
|
// /**
|
|
// * 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)
|
|
// }))
|
|
|
|
// googleTimer.on('complete', this.cancelable(() => {
|
|
// this.requestDownload()
|
|
// }))
|
|
// }
|
|
|
|
// /**
|
|
// * 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)
|
|
// ),
|
|
// })
|
|
|
|
// 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.req.on('err', this.cancelable((err: Error) => {
|
|
// this.failDownload(downloadErrors.connectionError(err))
|
|
// }))
|
|
|
|
// this.req.on('header', this.cancelable((statusCode, headers: Headers) => {
|
|
// if (statusCode !== 200) {
|
|
// this.failDownload(downloadErrors.responseError(statusCode))
|
|
// return
|
|
// }
|
|
|
|
// if (headers['content-type'].startsWith('text/html')) {
|
|
// this.handleHTMLResponse(headers['set-cookie'])
|
|
// } else {
|
|
// this.handleDownloadResponse()
|
|
// }
|
|
// }))
|
|
// }
|
|
|
|
// /**
|
|
// * 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))
|
|
// })
|
|
// }
|
|
// }
|
|
// }))
|
|
// }
|
|
|
|
// /**
|
|
// * 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.req.on('err', this.cancelable((err: Error) => {
|
|
// this.failDownload(downloadErrors.connectionError(err))
|
|
// }))
|
|
|
|
// this.req.on('end', this.cancelable(() => {
|
|
// 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()))
|
|
// }
|
|
|
|
// /**
|
|
// * 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))
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// /**
|
|
// * 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 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) {
|
|
// // 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
|
|
// }
|
|
|
|
// /**
|
|
// * Download the file after waiting for the google rate limit.
|
|
// */
|
|
// beginDownload() {
|
|
// if (this.fileID === undefined) {
|
|
// this.failDownload(downloadErrors.linkError(this.url))
|
|
// }
|
|
|
|
// this.startDownloadStream()
|
|
// }
|
|
|
|
// /**
|
|
// * 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
|
|
|
|
// if (this.wasCanceled) { return }
|
|
|
|
// 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'))
|
|
// }
|
|
// }
|
|
// }
|
|
// }))
|
|
// }
|
|
|
|
// /**
|
|
// * 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)
|
|
|
|
// try {
|
|
// this.downloadStream.pipe(writeStream)
|
|
// } catch (err) {
|
|
// this.failDownload(downloadErrors.connectionError(err))
|
|
// }
|
|
|
|
// this.downloadStream.on('data', this.cancelable((chunk: Buffer) => {
|
|
// downloadedSize += chunk.length
|
|
// }))
|
|
|
|
// const progressUpdater = setInterval(() => {
|
|
// this.callbacks.downloadProgress(downloadedSize)
|
|
// }, 100)
|
|
|
|
// this.downloadStream.on('error', this.cancelable((err: Error) => {
|
|
// clearInterval(progressUpdater)
|
|
// this.failDownload(downloadErrors.connectionError(err))
|
|
// }))
|
|
|
|
// this.downloadStream.on('end', this.cancelable(() => {
|
|
// clearInterval(progressUpdater)
|
|
// writeStream.end()
|
|
// this.downloadStream.destroy()
|
|
// this.downloadStream = null
|
|
|
|
// 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()))
|
|
// }
|
|
|
|
// /**
|
|
// * 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))
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// /**
|
|
// * 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)
|
|
// }
|