( zipPath: string, targetDir: string, )
| 329 | * @param targetDir - Directory to extract into |
| 330 | */ |
| 331 | export async function extractZipToDirectory( |
| 332 | zipPath: string, |
| 333 | targetDir: string, |
| 334 | ): Promise<void> { |
| 335 | const zipBuf = await getFsImplementation().readFileBytes(zipPath) |
| 336 | const files = await unzipFile(zipBuf) |
| 337 | // fflate doesn't surface external_attr — parse the central directory so |
| 338 | // exec bits survive extraction (hooks/scripts need +x to run via `sh -c`). |
| 339 | const modes = parseZipModes(zipBuf) |
| 340 | |
| 341 | await getFsImplementation().mkdir(targetDir) |
| 342 | |
| 343 | for (const [relPath, data] of Object.entries(files)) { |
| 344 | // Skip directory entries (trailing slash) |
| 345 | if (relPath.endsWith('/')) { |
| 346 | await getFsImplementation().mkdir(join(targetDir, relPath)) |
| 347 | continue |
| 348 | } |
| 349 | |
| 350 | const fullPath = join(targetDir, relPath) |
| 351 | await getFsImplementation().mkdir(dirname(fullPath)) |
| 352 | await writeFile(fullPath, data) |
| 353 | const mode = modes[relPath] |
| 354 | if (mode && mode & 0o111) { |
| 355 | // Swallow EPERM/ENOTSUP (NFS root_squash, some FUSE mounts) — losing +x |
| 356 | // is the pre-PR behavior and better than aborting mid-extraction. |
| 357 | await chmod(fullPath, mode & 0o777).catch(() => {}) |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | logForDebugging( |
| 362 | `Extracted ZIP to ${targetDir}: ${Object.keys(files).length} entries`, |
| 363 | ) |
| 364 | } |
| 365 | |
| 366 | /** |
| 367 | * Convert a plugin directory to a ZIP in-place: zip → atomic write → delete dir. |
no test coverage detected