Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/model/ThunderstoreVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/providers/generic/file/FsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default abstract class FsProvider {
}

public abstract writeFile(path: string, content: string | Buffer): Promise<void>;
public abstract writeStreamToFile(path: string, content: ReadableStream): Promise<void>;
public abstract readFile(path: string): Promise<Buffer>;
public abstract readdir(path: string): Promise<string[]>;
public abstract rmdir(path: string): Promise<void>;
Expand Down
19 changes: 19 additions & 0 deletions src/providers/generic/file/NodeFs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,25 @@ export default class NodeFs extends FsProvider {
})
}

async writeStreamToFile(path: string, content: ReadableStream): Promise<void> {
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<void> {
return new Promise((resolve, reject) => {
NodeFs.lock.acquire([path, newPath], async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 18 additions & 12 deletions src/r2mm/downloading/BetterThunderstoreDownloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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 {
Expand All @@ -259,30 +259,36 @@ 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',
combo.getVersion().getVersionNumber().toString(),
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);
}
}

Expand Down
3 changes: 3 additions & 0 deletions test/jest/__tests__/stubs/providers/InMemory.FsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ export default class InMemoryFsProvider extends FsProvider {
parent.nodes.push(newFile);
}

async writeStreamToFile(path: string, content: ReadableStream): Promise<void> {
throw new Error("writeStreamToFile: not implemented");
}

async setModifiedTime(file: string, time: Date): Promise<void> {
const found = this.findFileType(file);
Expand Down
72 changes: 40 additions & 32 deletions test/jest/__tests__/stubs/providers/stub.FsProvider.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
base64FromZip = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async chmod(path: string, mode: string | number): Promise<void> {
chmod = async (path: string, mode: string | number) => {
throw new Error("Stub access must be mocked or spied");
}
};

async copyFile(from: string, to: string): Promise<void> {
copyFile = async (from: string, to: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async copyFolder(from: string, to: string): Promise<void> {
copyFolder = async (from: string, to: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async exists(path: string): Promise<boolean> {
exists = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async lstat(path: string): Promise<StatInterface> {
lstat = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async mkdirs(path: string): Promise<void> {
mkdirs = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async readFile(path: string): Promise<Buffer> {
readFile = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async readdir(path: string): Promise<string[]> {
readdir = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async realpath(path: string): Promise<string> {
realpath = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async rename(path: string, newPath: string): Promise<void> {
rename = async (path: string, newPath: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async rmdir(path: string): Promise<void> {
rmdir = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async stat(path: string): Promise<StatInterface> {
stat = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async unlink(path: string): Promise<void> {
unlink = async (path: string) => {
throw new Error("Stub access must be mocked or spied");
}
};

async writeFile(path: string, content: string | Buffer): Promise<void> {
writeFile = async (path: string, content: string | Buffer) => {
throw new Error("Stub access must be mocked or spied");
}
};

async setModifiedTime(path: string, time: Date): Promise<void> {
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");
};
}
Loading