Skip to content

Commit f0469d5

Browse files
committed
Allow save to disk for mod zip >2GB
Refs TS-2082
1 parent 14c00f4 commit f0469d5

File tree

6 files changed

+73
-41
lines changed

6 files changed

+73
-41
lines changed

src/providers/generic/file/FsProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default abstract class FsProvider {
1616
}
1717

1818
public abstract writeFile(path: string, content: string | Buffer): Promise<void>;
19+
public abstract writeStreamToFile(path: string, content: ReadableStream): Promise<void>;
1920
public abstract readFile(path: string): Promise<Buffer>;
2021
public abstract readdir(path: string): Promise<string[]>;
2122
public abstract rmdir(path: string): Promise<void>;

src/providers/generic/file/NodeFs.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,25 @@ export default class NodeFs extends FsProvider {
9191
})
9292
}
9393

94+
async writeStreamToFile(path: string, content: ReadableStream): Promise<void> {
95+
return new Promise((resolve, reject) => {
96+
NodeFs.lock.acquire(path, async () => {
97+
const writeStream = fs.createWriteStream(path, { flags : 'w' });
98+
writeStream.on('finish', () => resolve());
99+
100+
const reader = content.getReader();
101+
while (true) {
102+
const line = await reader.read();
103+
if (line.done) {
104+
break;
105+
}
106+
writeStream.write(line.value);
107+
}
108+
writeStream.end();
109+
}).catch(reject);
110+
});
111+
}
112+
94113
async rename(path: string, newPath: string): Promise<void> {
95114
return new Promise((resolve, reject) => {
96115
NodeFs.lock.acquire([path, newPath], async () => {

src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export default abstract class ThunderstoreDownloaderProvider {
106106
* @param combo The mod being downloaded.
107107
* @param callback Callback on if saving and extracting has been performed correctly. An error is provided if success is false.
108108
*/
109-
public abstract saveToFile(response: Buffer, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void): void;
109+
public abstract saveToFile(response: ReadableStream, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void): void;
110110

111111
/**
112112
* Check the cache to see if the mod has already been downloaded.

src/r2mm/downloading/BetterThunderstoreDownloader.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,18 +239,18 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
239239
onDownloadProgress: progress => {
240240
callback((progress.loaded / progress.total) * 100, StatusEnum.PENDING, null);
241241
},
242-
responseType: 'arraybuffer',
242+
responseType: 'blob',
243243
headers: {
244244
'Content-Type': 'application/zip',
245245
'Access-Control-Allow-Origin': '*'
246-
}
246+
},
247247
});
248248
}
249249

250250
private async _saveDownloadResponse(response: AxiosResponse, combo: ThunderstoreCombo, callback: (progress: number, status: number, err: R2Error | null) => void): Promise<void> {
251-
const buf: Buffer = Buffer.from(response.data)
251+
const dataStream = response.data.stream() as ReadableStream;
252252
callback(100, StatusEnum.PENDING, null);
253-
await this.saveToFile(buf, combo, (success: boolean, error?: R2Error) => {
253+
await this.saveToFile(dataStream, combo, (success: boolean, error?: R2Error) => {
254254
if (success) {
255255
callback(100, StatusEnum.SUCCESS, error || null);
256256
} else {
@@ -259,27 +259,28 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
259259
});
260260
}
261261

262-
public async saveToFile(response: Buffer, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void) {
262+
public async saveToFile(data: ReadableStream, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void) {
263263
const fs = FsProvider.instance;
264264
const cacheDirectory = path.join(PathResolver.MOD_ROOT, 'cache');
265265
try {
266266
if (! await fs.exists(path.join(cacheDirectory, combo.getMod().getFullName()))) {
267267
await fs.mkdirs(path.join(cacheDirectory, combo.getMod().getFullName()));
268268
}
269-
await fs.writeFile(path.join(
269+
await fs.writeStreamToFile(path.join(
270270
cacheDirectory,
271271
combo.getMod().getFullName(),
272272
combo.getVersion().getVersionNumber().toString() + '.zip'
273-
), response);
273+
), data);
274274
await ZipExtract.extractAndDelete(
275275
path.join(cacheDirectory, combo.getMod().getFullName()),
276276
combo.getVersion().getVersionNumber().toString() + '.zip',
277277
combo.getVersion().getVersionNumber().toString(),
278278
callback
279279
);
280280
} catch(e) {
281+
const err = e as Error;
281282
callback(false, new FileWriteError(
282-
'File write error',
283+
err.name,
283284
`Failed to write downloaded zip of ${combo.getMod().getFullName()} cache folder. \nReason: ${(e as Error).message}`,
284285
`Try running ${ManagerInformation.APP_NAME} as an administrator`
285286
));

test/jest/__tests__/stubs/providers/InMemory.FsProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ export default class InMemoryFsProvider extends FsProvider {
225225
parent.nodes.push(newFile);
226226
}
227227

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

229232
async setModifiedTime(file: string, time: Date): Promise<void> {
230233
const found = this.findFileType(file);
Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,77 @@
11
import FsProvider from '../../../../../src/providers/generic/file/FsProvider';
2-
import StatInterface from '../../../../../src/providers/generic/file/StatInterface';
2+
import Dexie from 'dexie';
3+
import { types } from 'sass';
4+
import * as Buffer from 'buffer';
5+
import Error = types.Error;
6+
import Promise = Dexie.Promise;
37

48
export default class StubFsProvider extends FsProvider {
59

6-
async base64FromZip(path: string): Promise<string> {
10+
base64FromZip = async (path: string) => {
711
throw new Error("Stub access must be mocked or spied");
8-
}
12+
};
913

10-
async chmod(path: string, mode: string | number): Promise<void> {
14+
chmod = async (path: string, mode: string | number) => {
1115
throw new Error("Stub access must be mocked or spied");
12-
}
16+
};
1317

14-
async copyFile(from: string, to: string): Promise<void> {
18+
copyFile = async (from: string, to: string) => {
1519
throw new Error("Stub access must be mocked or spied");
16-
}
20+
};
1721

18-
async copyFolder(from: string, to: string): Promise<void> {
22+
copyFolder = async (from: string, to: string) => {
1923
throw new Error("Stub access must be mocked or spied");
20-
}
24+
};
2125

22-
async exists(path: string): Promise<boolean> {
26+
exists = async (path: string) => {
2327
throw new Error("Stub access must be mocked or spied");
24-
}
28+
};
2529

26-
async lstat(path: string): Promise<StatInterface> {
30+
lstat = async (path: string) => {
2731
throw new Error("Stub access must be mocked or spied");
28-
}
32+
};
2933

30-
async mkdirs(path: string): Promise<void> {
34+
mkdirs = async (path: string) => {
3135
throw new Error("Stub access must be mocked or spied");
32-
}
36+
};
3337

34-
async readFile(path: string): Promise<Buffer> {
38+
readFile = async (path: string) => {
3539
throw new Error("Stub access must be mocked or spied");
36-
}
40+
};
3741

38-
async readdir(path: string): Promise<string[]> {
42+
readdir = async (path: string) => {
3943
throw new Error("Stub access must be mocked or spied");
40-
}
44+
};
4145

42-
async realpath(path: string): Promise<string> {
46+
realpath = async (path: string) => {
4347
throw new Error("Stub access must be mocked or spied");
44-
}
48+
};
4549

46-
async rename(path: string, newPath: string): Promise<void> {
50+
rename = async (path: string, newPath: string) => {
4751
throw new Error("Stub access must be mocked or spied");
48-
}
52+
};
4953

50-
async rmdir(path: string): Promise<void> {
54+
rmdir = async (path: string) => {
5155
throw new Error("Stub access must be mocked or spied");
52-
}
56+
};
5357

54-
async stat(path: string): Promise<StatInterface> {
58+
stat = async (path: string) => {
5559
throw new Error("Stub access must be mocked or spied");
56-
}
60+
};
5761

58-
async unlink(path: string): Promise<void> {
62+
unlink = async (path: string) => {
5963
throw new Error("Stub access must be mocked or spied");
60-
}
64+
};
6165

62-
async writeFile(path: string, content: string | Buffer): Promise<void> {
66+
writeFile = async (path: string, content: string | Buffer) => {
6367
throw new Error("Stub access must be mocked or spied");
64-
}
68+
};
6569

66-
async setModifiedTime(path: string, time: Date): Promise<void> {
70+
writeStreamToFile = async (path: string, content: ReadableStream) => {
6771
throw new Error("Stub access must be mocked or spied");
6872
}
73+
74+
setModifiedTime = async (path: string, time: Date) => {
75+
throw new Error("Stub access must be mocked or spied");
76+
};
6977
}

0 commit comments

Comments
 (0)