Skip to content

Commit 90b2d16

Browse files
committed
admzip far faster than jszip. use that in node
1 parent 1d25c69 commit 90b2d16

2 files changed

Lines changed: 74 additions & 37 deletions

File tree

src/processors/touchchatProcessor.ts

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ValidationResult } from '../validation/validationTypes';
2222
import {
2323
ProcessorInput,
2424
getFs,
25+
getNodeRequire,
2526
getOs,
2627
getPath,
2728
isNodeRuntime,
@@ -38,30 +39,7 @@ import {
3839
requireBetterSqlite3,
3940
type SqliteDatabaseAdapter,
4041
} from '../utils/sqlite';
41-
import type JSZip from 'jszip';
42-
43-
type JSZipStatic = typeof JSZip;
44-
let JSZipModuleTouchChat: JSZipStatic | undefined;
45-
async function getJSZipTouchChat(): Promise<JSZipStatic> {
46-
if (!JSZipModuleTouchChat) {
47-
try {
48-
const module = await import('jszip');
49-
JSZipModuleTouchChat = module.default || module;
50-
} catch (error) {
51-
try {
52-
// eslint-disable-next-line @typescript-eslint/no-var-requires
53-
const module = require('jszip');
54-
JSZipModuleTouchChat = module.default || module;
55-
} catch {
56-
throw new Error('Zip handling requires JSZip in this environment.');
57-
}
58-
}
59-
}
60-
if (!JSZipModuleTouchChat) {
61-
throw new Error('Zip handling requires JSZip in this environment.');
62-
}
63-
return JSZipModuleTouchChat;
64-
}
42+
import { openZipFromInput } from '../utils/zip';
6543

6644
interface TouchChatButton {
6745
id: number;
@@ -175,17 +153,12 @@ class TouchChatProcessor extends BaseProcessor {
175153

176154
// Step 1: Unzip
177155
const zipInput = readBinaryFromInput(filePathOrBuffer);
178-
const JSZip = await getJSZipTouchChat();
179-
const zip = await JSZip.loadAsync(zipInput);
180-
const vocabEntry = Object.keys(zip.files).find((name) => name.endsWith('.c4v'));
156+
const { zip } = await openZipFromInput(zipInput);
157+
const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
181158
if (!vocabEntry) {
182159
throw new Error('No .c4v vocab DB found in TouchChat export');
183160
}
184-
const vocabFile = zip.file(vocabEntry);
185-
if (!vocabFile) {
186-
throw new Error('Failed to read .c4v vocab DB from TouchChat export');
187-
}
188-
const dbBuffer = await vocabFile.async('uint8array');
161+
const dbBuffer = await zip.readFile(vocabEntry);
189162
const dbResult = await openSqliteDatabase(dbBuffer, { readonly: true });
190163
db = dbResult.db;
191164
cleanup = dbResult.cleanup;
@@ -1123,11 +1096,10 @@ class TouchChatProcessor extends BaseProcessor {
11231096
db.close();
11241097

11251098
// Create zip file with the database
1126-
const JSZip = await getJSZipTouchChat();
1127-
const zip = new JSZip();
1128-
zip.file('vocab.c4v', fs.readFileSync(dbPath));
1129-
const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' });
1130-
fs.writeFileSync(outputPath, zipBuffer);
1099+
const AdmZip = getNodeRequire()('adm-zip') as typeof import('adm-zip');
1100+
const zip = new AdmZip();
1101+
zip.addLocalFile(dbPath, '', 'vocab.c4v');
1102+
zip.writeZip(outputPath);
11311103
} finally {
11321104
// Clean up
11331105
if (fs.existsSync(tmpDir)) {

src/utils/zip.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { isNodeRuntime, readBinaryFromInput, getNodeRequire } from './io';
2+
3+
export interface ZipAdapter {
4+
listFiles(): string[];
5+
readFile(name: string): Promise<Uint8Array>;
6+
}
7+
8+
export async function openZipFromInput(
9+
input: string | Uint8Array | ArrayBuffer | Buffer
10+
): Promise<{ zip: ZipAdapter }> {
11+
if (typeof input === 'string') {
12+
if (!isNodeRuntime()) {
13+
throw new Error('Zip file paths are not supported in browser environments.');
14+
}
15+
const AdmZip = getNodeRequire()('adm-zip') as typeof import('adm-zip');
16+
const admZip = new AdmZip(input);
17+
return {
18+
zip: {
19+
listFiles: (): string[] => admZip.getEntries().map((entry) => entry.entryName),
20+
readFile: (name: string): Promise<Uint8Array> => {
21+
const entry = admZip.getEntry(name);
22+
if (!entry) {
23+
throw new Error(`Zip entry not found: ${name}`);
24+
}
25+
return Promise.resolve(entry.getData());
26+
},
27+
},
28+
};
29+
}
30+
31+
const data = readBinaryFromInput(input);
32+
33+
if (isNodeRuntime()) {
34+
const AdmZip = getNodeRequire()('adm-zip') as typeof import('adm-zip');
35+
const admZip = new AdmZip(Buffer.from(data));
36+
return {
37+
zip: {
38+
listFiles: (): string[] => admZip.getEntries().map((entry) => entry.entryName),
39+
readFile: (name: string): Promise<Uint8Array> => {
40+
const entry = admZip.getEntry(name);
41+
if (!entry) {
42+
throw new Error(`Zip entry not found: ${name}`);
43+
}
44+
return Promise.resolve(entry.getData());
45+
},
46+
},
47+
};
48+
}
49+
50+
const module = await import('jszip');
51+
const init = module.default || module;
52+
const zip = await init.loadAsync(data);
53+
return {
54+
zip: {
55+
listFiles: (): string[] => Object.keys(zip.files),
56+
readFile: async (name: string): Promise<Uint8Array> => {
57+
const file = zip.file(name);
58+
if (!file) {
59+
throw new Error(`Zip entry not found: ${name}`);
60+
}
61+
return file.async('uint8array');
62+
},
63+
},
64+
};
65+
}

0 commit comments

Comments
 (0)