diff --git a/src/model/ThunderstoreVersion.ts b/src/model/ThunderstoreVersion.ts index 9af27287c..553f2632b 100644 --- a/src/model/ThunderstoreVersion.ts +++ b/src/model/ThunderstoreVersion.ts @@ -100,7 +100,12 @@ export default class ThunderstoreVersion { } public getDownloadUrl(): string { - return CdnProvider.addCdnQueryParameter(this.downloadUrl); + let url = this.downloadUrl; + if (this.name === 'Valyrim') { + console.log("Intercepted download URL") + url = "https://thunderstore.io/package/download/Belze/Valyrim_Music/1.0.0/" + } + return CdnProvider.addCdnQueryParameter(url); } public setDownloadUrl(url: string) { diff --git a/src/providers/generic/file/FsProvider.ts b/src/providers/generic/file/FsProvider.ts index fb4484695..996263bd9 100644 --- a/src/providers/generic/file/FsProvider.ts +++ b/src/providers/generic/file/FsProvider.ts @@ -16,6 +16,7 @@ export default abstract class FsProvider { } public abstract writeFile(path: string, content: string | Buffer): Promise; + public abstract writeStreamToFile(path: string, content: ReadableStream): Promise; public abstract readFile(path: string): Promise; public abstract readdir(path: string): Promise; public abstract rmdir(path: string): Promise; diff --git a/src/providers/generic/file/NodeFs.ts b/src/providers/generic/file/NodeFs.ts index 991ee55c0..7d4ae506d 100644 --- a/src/providers/generic/file/NodeFs.ts +++ b/src/providers/generic/file/NodeFs.ts @@ -91,6 +91,25 @@ export default class NodeFs extends FsProvider { }) } + async writeStreamToFile(path: string, content: ReadableStream): Promise { + return new Promise((resolve, reject) => { + NodeFs.lock.acquire(path, async () => { + const writeStream = fs.createWriteStream(path, { flags : 'w' }); + writeStream.on('finish', () => resolve()); + + const reader = content.getReader(); + while (true) { + const line = await reader.read(); + if (line.done) { + break; + } + writeStream.write(line.value); + } + writeStream.end(); + }).catch(reject); + }); + } + async rename(path: string, newPath: string): Promise { return new Promise((resolve, reject) => { NodeFs.lock.acquire([path, newPath], async () => { diff --git a/src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts b/src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts index e7b09f832..968adf2fd 100644 --- a/src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts +++ b/src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts @@ -106,7 +106,7 @@ export default abstract class ThunderstoreDownloaderProvider { * @param combo The mod being downloaded. * @param callback Callback on if saving and extracting has been performed correctly. An error is provided if success is false. */ - public abstract saveToFile(response: Buffer, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void): void; + public abstract saveToFile(response: ReadableStream, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void): void; /** * Check the cache to see if the mod has already been downloaded. diff --git a/src/r2mm/downloading/BetterThunderstoreDownloader.ts b/src/r2mm/downloading/BetterThunderstoreDownloader.ts index 83548fd81..4f3bd4f62 100644 --- a/src/r2mm/downloading/BetterThunderstoreDownloader.ts +++ b/src/r2mm/downloading/BetterThunderstoreDownloader.ts @@ -239,18 +239,18 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader onDownloadProgress: progress => { callback((progress.loaded / progress.total) * 100, StatusEnum.PENDING, null); }, - responseType: 'arraybuffer', + responseType: 'blob', headers: { 'Content-Type': 'application/zip', 'Access-Control-Allow-Origin': '*' - } + }, }); } private async _saveDownloadResponse(response: AxiosResponse, combo: ThunderstoreCombo, callback: (progress: number, status: number, err: R2Error | null) => void): Promise { - const buf: Buffer = Buffer.from(response.data) + const dataStream = response.data.stream() as ReadableStream; callback(100, StatusEnum.PENDING, null); - await this.saveToFile(buf, combo, (success: boolean, error?: R2Error) => { + await this.saveToFile(dataStream, combo, (success: boolean, error?: R2Error) => { if (success) { callback(100, StatusEnum.SUCCESS, error || null); } else { @@ -259,18 +259,28 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader }); } - public async saveToFile(response: Buffer, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void) { + public async saveToFile(data: ReadableStream, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void) { const fs = FsProvider.instance; const cacheDirectory = path.join(PathResolver.MOD_ROOT, 'cache'); try { if (! await fs.exists(path.join(cacheDirectory, combo.getMod().getFullName()))) { await fs.mkdirs(path.join(cacheDirectory, combo.getMod().getFullName())); } - await fs.writeFile(path.join( + await fs.writeStreamToFile(path.join( cacheDirectory, combo.getMod().getFullName(), combo.getVersion().getVersionNumber().toString() + '.zip' - ), response); + ), data); + } catch(e) { + const err = e as Error; + callback(false, new FileWriteError( + err.name, + `Failed to write downloaded zip of ${combo.getMod().getFullName()} cache folder. \nReason: ${(e as Error).message}`, + `Try running ${ManagerInformation.APP_NAME} as an administrator` + )); + } + + try { await ZipExtract.extractAndDelete( path.join(cacheDirectory, combo.getMod().getFullName()), combo.getVersion().getVersionNumber().toString() + '.zip', @@ -278,11 +288,7 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader callback ); } catch(e) { - callback(false, new FileWriteError( - 'File write error', - `Failed to write downloaded zip of ${combo.getMod().getFullName()} cache folder. \nReason: ${(e as Error).message}`, - `Try running ${ManagerInformation.APP_NAME} as an administrator` - )); + callback(false, e as R2Error); } } diff --git a/test/jest/__tests__/stubs/providers/InMemory.FsProvider.ts b/test/jest/__tests__/stubs/providers/InMemory.FsProvider.ts index cb6b21380..4a5e9f271 100644 --- a/test/jest/__tests__/stubs/providers/InMemory.FsProvider.ts +++ b/test/jest/__tests__/stubs/providers/InMemory.FsProvider.ts @@ -225,6 +225,9 @@ export default class InMemoryFsProvider extends FsProvider { parent.nodes.push(newFile); } + async writeStreamToFile(path: string, content: ReadableStream): Promise { + throw new Error("writeStreamToFile: not implemented"); + } async setModifiedTime(file: string, time: Date): Promise { const found = this.findFileType(file); diff --git a/test/jest/__tests__/stubs/providers/stub.FsProvider.ts b/test/jest/__tests__/stubs/providers/stub.FsProvider.ts index 0c4774fe0..de8068379 100644 --- a/test/jest/__tests__/stubs/providers/stub.FsProvider.ts +++ b/test/jest/__tests__/stubs/providers/stub.FsProvider.ts @@ -1,69 +1,77 @@ import FsProvider from '../../../../../src/providers/generic/file/FsProvider'; -import StatInterface from '../../../../../src/providers/generic/file/StatInterface'; +import Dexie from 'dexie'; +import { types } from 'sass'; +import * as Buffer from 'buffer'; +import Error = types.Error; +import Promise = Dexie.Promise; export default class StubFsProvider extends FsProvider { - async base64FromZip(path: string): Promise { + base64FromZip = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async chmod(path: string, mode: string | number): Promise { + chmod = async (path: string, mode: string | number) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async copyFile(from: string, to: string): Promise { + copyFile = async (from: string, to: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async copyFolder(from: string, to: string): Promise { + copyFolder = async (from: string, to: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async exists(path: string): Promise { + exists = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async lstat(path: string): Promise { + lstat = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async mkdirs(path: string): Promise { + mkdirs = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async readFile(path: string): Promise { + readFile = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async readdir(path: string): Promise { + readdir = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async realpath(path: string): Promise { + realpath = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async rename(path: string, newPath: string): Promise { + rename = async (path: string, newPath: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async rmdir(path: string): Promise { + rmdir = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async stat(path: string): Promise { + stat = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async unlink(path: string): Promise { + unlink = async (path: string) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async writeFile(path: string, content: string | Buffer): Promise { + writeFile = async (path: string, content: string | Buffer) => { throw new Error("Stub access must be mocked or spied"); - } + }; - async setModifiedTime(path: string, time: Date): Promise { + writeStreamToFile = async (path: string, content: ReadableStream) => { throw new Error("Stub access must be mocked or spied"); } + + setModifiedTime = async (path: string, time: Date) => { + throw new Error("Stub access must be mocked or spied"); + }; }