unzip() now supports zip version 78.8
Massimo Melina committed
Mar 23, 2026 at 14:06 UTC
746c2031b9c7848ec420f81acf91d3c633cab5db
3 files changed
+41
-14
package.json
+5
-2
@@ -8,6 +8,7 @@
8
"author": "Massimo Melina <a@rejetto.com>",
9
"workspaces": [ "admin", "frontend", "shared", "mui-grid-form" ],
10
"scripts": {
11
+ "postinstall": "patch-package",
12
"watch-server": "cross-env DEV=1 nodemon --ignore tests/ --watch src -e ts,tsx --exec tsx src",
13
"watch-server-proxied": "cross-env FRONTEND_PROXY=3005 ADMIN_PROXY=3006 npm run watch-server",
14
"watch-server-full": "npm run start --workspace=frontend & npm run start --workspace=admin & cross-env FRONTEND_PROXY=3005 ADMIN_PROXY=3006 npm run watch-server",
@@ -68,7 +69,7 @@
69
"**/node_modules/axios/dist/node/*",
70
"**/node_modules/buffers/**",
71
"**/node_modules/binary/**",
71
- "**/node_modules/unzip-stream/**"
72
+ "**/node_modules/unzipper/**"
73
],
74
"targets": [
75
"node20-win-x64",
@@ -105,7 +106,7 @@
106
"open": "^8.4.0",
107
"picomatch": "^4.0.3",
108
"tssrp6a": "^3.0.0",
108
- "unzip-stream": "^0.3.4",
109
+ "unzipper": "^0.12.3",
110
"valtio": "^1.13.2",
111
"xxhashjs": "^0.2.2",
112
"yaml": "^2.8.1"
@@ -121,11 +122,13 @@
122
"@types/node": "^20.17.30",
123
"@types/node-forge": "^1.3.14",
124
"@types/picomatch": "^4.0.2",
125
+ "@types/unzipper": "^0.10.11",
126
"@yao-pkg/pkg": "6.14.2",
127
"cross-env": "^10.0.0",
128
"koa-better-http-proxy": "^0.2.10",
129
"nm-prune": "^5.0.0",
130
"nodemon": "^3.1.10",
131
+ "patch-package": "^8.0.1",
132
"tsx": "^4.20.5",
133
"typescript": "^5.9.2"
134
}
patches/unzipper+0.12.3.patch
new
+19
@@ -0,0 +1,19 @@
1
+diff --git a/node_modules/unzipper/lib/parse.js b/node_modules/unzipper/lib/parse.js
2
+index 0921f4a..e6e0131 100644
3
+--- a/node_modules/unzipper/lib/parse.js
4
++++ b/node_modules/unzipper/lib/parse.js
5
+@@ -250,6 +250,14 @@ Parse.prototype._readCentralDirectoryFileHeader = function () {
6
+ return self.pull(vars.fileCommentLength);
7
+ })
8
+ .then(function() {
9
++ self.emit('entryInCentral', {
10
++ path: vars.fileName,
11
++ type: (vars.uncompressedSize === 0 && /[/\\]$/.test(vars.fileName)) ? 'Directory' : 'File',
12
++ versionMadeBy: vars.versionMadeBy,
13
++ versionsNeededToExtract: vars.versionsNeededToExtract,
14
++ externalFileAttributes: vars.externalFileAttributes,
15
++ unixAttrs: vars.externalFileAttributes >>> 16,
16
++ });
17
+ return true;
18
+ });
19
+ });
src/util-files.ts
+17
-12
@@ -10,8 +10,7 @@ import { IS_WINDOWS } from './const'
10
import { finished } from 'stream/promises'
11
import { Readable } from 'stream'
12
import { getStatWorker } from './stat'
13
-// @ts-ignore
14
-import unzipper from 'unzip-stream'
13
+import unzipper from 'unzipper'
14
15
const fileTimeout = defineConfig('file_timeout', 3, x => x * 1000)
16
// a smart (and a bit arbitrary) way to decide if we need the stat-workers functionality. Without it, we may be a bit faster. We'll see with experience if we need a dedicated configuration.
@@ -92,26 +91,32 @@ export function escapeGlobPath(path: string) {
91
}
92
93
export async function unzip(stream: Readable, cb: (path: string) => Promisable<false | string>) {
94
+ const extracted = new Map<string, string>()
95
let chain: Promise<any> = Promise.resolve()
96
return new Promise((resolve, reject) =>
97
- stream.pipe(unzipper.Extract())
98
- .on('end', () => chain.then(resolve))
97
+ stream.pipe(unzipper.Parse())
98
+ .on('close', () => chain.then(resolve, reject))
99
.on('error', reject)
100
.on('entry', (entry: any) =>
101
- chain = chain.then(async () => { // don't overlap writings
101
+ chain = chain.then(async () => {
102
const { path, type } = entry
103
const dest = await try_(() => cb(path), e => console.warn(String(e)))
104
if (!dest || type !== 'File')
105
- return entry.autodrain()
105
+ return entry.autodrain().promise()
106
+ extracted.set(path, dest)
107
console.debug('Unzip', dest)
108
+ // keep writes serialized so archive entries can't race while callers map paths asynchronously
109
const thisFile = entry.pipe(await createSafeWriteStream(dest))
110
await finished(thisFile)
109
- }) )
110
- .on('entryInCentral', (entry: any) => {
111
- if (entry.unixAttrs)
112
- chmod(entry.path, entry.unixAttrs)
113
- })
114
- )
111
+ }))
112
+ // unix modes live in the central directory, so we reapply them after the file stream has been written
113
+ .on('entryInCentral', (entry: any) =>
114
+ chain = chain.then(async () => {
115
+ if (entry.type !== 'File') return
116
+ const dest = extracted.get(entry.path)
117
+ if (dest && entry.unixAttrs)
118
+ await chmod(dest, entry.unixAttrs).catch(() => {})
119
+ })) )
120
}
121
122
export async function ensureParentFolder(path: string, dirnameIt=true) {