(url: string, dest: string)
| 136 | // ─── Model download + staging ──────────────────────────────── |
| 137 | |
| 138 | export async function downloadFile(url: string, dest: string): Promise<void> { |
| 139 | const res = await fetch(url); |
| 140 | if (!res.ok || !res.body) { |
| 141 | throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); |
| 142 | } |
| 143 | const tmp = `${dest}.tmp.${process.pid}`; |
| 144 | const writer = fs.createWriteStream(tmp); |
| 145 | // @ts-ignore — Node stream compat |
| 146 | const reader = res.body.getReader(); |
| 147 | try { |
| 148 | let done = false; |
| 149 | while (!done) { |
| 150 | const chunk = await reader.read(); |
| 151 | if (chunk.done) { done = true; break; } |
| 152 | writer.write(chunk.value); |
| 153 | } |
| 154 | await new Promise<void>((resolve, reject) => { |
| 155 | writer.end((err?: Error | null) => (err ? reject(err) : resolve())); |
| 156 | }); |
| 157 | fs.renameSync(tmp, dest); |
| 158 | } catch (err) { |
| 159 | // Drop the half-written tmp so we don't ship a truncated model file to |
| 160 | // a retry's renameSync. Wait for the writer to close fully before |
| 161 | // unlinking: Node's createWriteStream lazily opens the FD and flushes |
| 162 | // buffered writes during destroy(), so a naive unlinkSync hits ENOENT |
| 163 | // first and the writer re-creates the file on the next tick. |
| 164 | await new Promise<void>((resolve) => { |
| 165 | writer.once('close', () => resolve()); |
| 166 | writer.destroy(); |
| 167 | }); |
| 168 | try { fs.unlinkSync(tmp); } catch { /* nothing to clean */ } |
| 169 | throw err; |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | async function ensureTestsavantStaged(onProgress?: (msg: string) => void): Promise<void> { |
| 174 | mkdirSecure(path.join(TESTSAVANT_DIR, 'onnx')); |
no test coverage detected