( nlpDir: string, dictId: string, onProgress?: (percent: number) => void )
| 58 | } |
| 59 | |
| 60 | export async function downloadDict( |
| 61 | nlpDir: string, |
| 62 | dictId: string, |
| 63 | onProgress?: (percent: number) => void |
| 64 | ): Promise<{ success: boolean; error?: string }> { |
| 65 | if (!AVAILABLE_DICTS.find((d) => d.id === dictId)) { |
| 66 | return { success: false, error: `Unknown dict: ${dictId}` } |
| 67 | } |
| 68 | |
| 69 | fs.mkdirSync(nlpDir, { recursive: true }) |
| 70 | const url = `${DICT_DOWNLOAD_URL_BASE}/${dictId}.dict` |
| 71 | const filePath = getDictFilePath(nlpDir, dictId) |
| 72 | const tmpPath = filePath + '.tmp' |
| 73 | |
| 74 | try { |
| 75 | const response = await fetch(url, { signal: AbortSignal.timeout(120_000) }) |
| 76 | if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`) |
| 77 | |
| 78 | const total = Number(response.headers.get('content-length') || 0) |
| 79 | let buffer: Buffer |
| 80 | |
| 81 | if (!response.body) { |
| 82 | buffer = Buffer.from(await response.arrayBuffer()) |
| 83 | } else { |
| 84 | const reader = response.body.getReader() |
| 85 | const chunks: Buffer[] = [] |
| 86 | let loaded = 0 |
| 87 | while (true) { |
| 88 | const { done, value } = await reader.read() |
| 89 | if (done) break |
| 90 | const chunk = Buffer.from(value) |
| 91 | chunks.push(chunk) |
| 92 | loaded += chunk.length |
| 93 | if (total > 0 && onProgress) onProgress(Math.round((loaded / total) * 100)) |
| 94 | } |
| 95 | buffer = Buffer.concat(chunks) |
| 96 | } |
| 97 | |
| 98 | const MIN_DICT_SIZE = 1_000_000 |
| 99 | if (buffer.length < MIN_DICT_SIZE) { |
| 100 | return { success: false, error: `Downloaded file is invalid (${buffer.length} bytes)` } |
| 101 | } |
| 102 | const head = buffer.subarray(0, 50).toString('utf-8').trim() |
| 103 | if (head.startsWith('<!') || head.startsWith('<html')) { |
| 104 | return { success: false, error: 'Downloaded file is HTML, not a dictionary file' } |
| 105 | } |
| 106 | |
| 107 | const expectedSha256 = DICT_SHA256[dictId] |
| 108 | if (!expectedSha256) { |
| 109 | return { success: false, error: `Missing SHA256 checksum for dict: ${dictId}` } |
| 110 | } |
| 111 | const actualSha256 = createHash('sha256').update(buffer).digest('hex') |
| 112 | if (actualSha256 !== expectedSha256) { |
| 113 | return { success: false, error: 'Dictionary integrity check failed (SHA256 mismatch)' } |
| 114 | } |
| 115 | |
| 116 | fs.writeFileSync(tmpPath, buffer) |
| 117 | if (fs.existsSync(filePath)) fs.unlinkSync(filePath) |
no test coverage detected