@zintrust/storage 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/build-manifest.json +55 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +1 -0
- package/dist/registerStreamingMultipartParser.d.ts +12 -0
- package/dist/registerStreamingMultipartParser.js +199 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @zintrust/storage
|
|
2
|
+
|
|
3
|
+
Core storage (disk driver) abstraction for ZinTrust with multipart form parsing support.
|
|
4
|
+
|
|
5
|
+
- Docs: https://zintrust.com/storage
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @zintrust/storage
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Register storage drivers at startup:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import '@zintrust/storage/register';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then configure storage disks and use `Storage` to interact with drivers.
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
This package and its dependencies are MIT licensed, permitting free commercial use.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zintrust/storage",
|
|
3
|
+
"version": "0.1.19",
|
|
4
|
+
"buildDate": "2026-01-28T12:31:13.892Z",
|
|
5
|
+
"buildEnvironment": {
|
|
6
|
+
"node": "v22.20.0",
|
|
7
|
+
"platform": "darwin",
|
|
8
|
+
"arch": "arm64"
|
|
9
|
+
},
|
|
10
|
+
"git": {
|
|
11
|
+
"commit": "2d1585bf",
|
|
12
|
+
"branch": "dev"
|
|
13
|
+
},
|
|
14
|
+
"package": {
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20.0.0"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": [
|
|
19
|
+
"busboy"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": [
|
|
22
|
+
"@zintrust/core"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"files": {
|
|
26
|
+
"build-manifest.json": {
|
|
27
|
+
"size": 1416,
|
|
28
|
+
"sha256": "aaa9b782b10341a8568819bed53b81979529e8404debd57402f2747cd888c890"
|
|
29
|
+
},
|
|
30
|
+
"index.d.ts": {
|
|
31
|
+
"size": 484,
|
|
32
|
+
"sha256": "f34f75d41253ceea8da492ff343a0e18b458f54be00e1a83c1f76f86d0baf24e"
|
|
33
|
+
},
|
|
34
|
+
"index.js": {
|
|
35
|
+
"size": 486,
|
|
36
|
+
"sha256": "01fb41ef99a9d83ec7d715c3644394e22ad06a596875c8e1e44e9dbbd4273ffa"
|
|
37
|
+
},
|
|
38
|
+
"register.d.ts": {
|
|
39
|
+
"size": 178,
|
|
40
|
+
"sha256": "2a1118ead443b7146d48400a425197511d5e97ede5a080993609a6533a604594"
|
|
41
|
+
},
|
|
42
|
+
"register.js": {
|
|
43
|
+
"size": 87,
|
|
44
|
+
"sha256": "03abea656f44f29f5905140258f0a0a457a2f08d5ab07bc59be93fc7ef94f427"
|
|
45
|
+
},
|
|
46
|
+
"registerStreamingMultipartParser.d.ts": {
|
|
47
|
+
"size": 372,
|
|
48
|
+
"sha256": "7b599c14cc4bed64dbbeb89da8e8161ea553de660d9bb1041eb02d84722fa3ef"
|
|
49
|
+
},
|
|
50
|
+
"registerStreamingMultipartParser.js": {
|
|
51
|
+
"size": 6432,
|
|
52
|
+
"sha256": "c9146380136edd8e2697e0e67e67136b789c7505b543a45dd1c97f81bf39f97c"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { MultipartFieldValue, MultipartParseInput, MultipartParserProvider, ParsedMultipartData, } from '@zintrust/core';
|
|
2
|
+
export { registerStreamingMultipartParser, type StreamingMultipartParserOptions, } from './registerStreamingMultipartParser';
|
|
3
|
+
/**
|
|
4
|
+
* Package version and build metadata
|
|
5
|
+
* Available at runtime for debugging and health checks
|
|
6
|
+
*/
|
|
7
|
+
export declare const _ZINTRUST_STORAGE_VERSION = "0.1.19";
|
|
8
|
+
export declare const _ZINTRUST_STORAGE_BUILD_DATE = "__BUILD_DATE__";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { registerStreamingMultipartParser, } from './registerStreamingMultipartParser';
|
|
2
|
+
/**
|
|
3
|
+
* Package version and build metadata
|
|
4
|
+
* Available at runtime for debugging and health checks
|
|
5
|
+
*/
|
|
6
|
+
export const _ZINTRUST_STORAGE_VERSION = '0.1.19';
|
|
7
|
+
export const _ZINTRUST_STORAGE_BUILD_DATE = '__BUILD_DATE__';
|
package/dist/register.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerStreamingMultipartParser } from './registerStreamingMultipartParser';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type StreamingMultipartParserOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Where uploaded files are buffered.
|
|
4
|
+
* Defaults to `os.tmpdir()/zintrust/uploads`.
|
|
5
|
+
*/
|
|
6
|
+
tmpDir?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Prefix for generated filenames.
|
|
9
|
+
*/
|
|
10
|
+
filenamePrefix?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function registerStreamingMultipartParser(options?: StreamingMultipartParserOptions): void;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { MultipartParserRegistry, NodeSingletons, } from '@zintrust/core';
|
|
2
|
+
import Busboy from 'busboy';
|
|
3
|
+
const defaultTmpDir = () => NodeSingletons.path.join(NodeSingletons.os.tmpdir(), 'zintrust', 'uploads');
|
|
4
|
+
const ensureDir = async (dir) => {
|
|
5
|
+
await NodeSingletons.fs.fsPromises.mkdir(dir, { recursive: true });
|
|
6
|
+
};
|
|
7
|
+
const safeUnlink = async (filePath) => {
|
|
8
|
+
try {
|
|
9
|
+
await NodeSingletons.fs.fsPromises.unlink(filePath);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// best-effort
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const addFieldValue = (fields, name, value) => {
|
|
16
|
+
const existing = fields[name];
|
|
17
|
+
if (existing === undefined) {
|
|
18
|
+
fields[name] = value;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(existing)) {
|
|
22
|
+
existing.push(value);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
fields[name] = [existing, value];
|
|
26
|
+
};
|
|
27
|
+
const makeTmpFilename = (input) => {
|
|
28
|
+
// Avoid trusting originalName for filesystem paths.
|
|
29
|
+
const uuid = typeof globalThis?.crypto?.randomUUID === 'function'
|
|
30
|
+
? globalThis.crypto.randomUUID()
|
|
31
|
+
: NodeSingletons.randomBytes(16).toString('hex');
|
|
32
|
+
const base = `${input.prefix}${uuid}`;
|
|
33
|
+
const ext = (() => {
|
|
34
|
+
const normalized = input.originalName.trim();
|
|
35
|
+
const idx = normalized.lastIndexOf('.');
|
|
36
|
+
if (idx <= 0)
|
|
37
|
+
return '';
|
|
38
|
+
const raw = normalized.slice(idx).toLowerCase();
|
|
39
|
+
// Keep extension length bounded.
|
|
40
|
+
if (raw.length > 12)
|
|
41
|
+
return '';
|
|
42
|
+
if (!/^[.][a-z0-9]+$/.test(raw))
|
|
43
|
+
return '';
|
|
44
|
+
return raw;
|
|
45
|
+
})();
|
|
46
|
+
return `${base}${ext}`;
|
|
47
|
+
};
|
|
48
|
+
const handleFileUpload = (fieldName, fileStream, info, ctx) => {
|
|
49
|
+
const originalName = info.filename ?? '';
|
|
50
|
+
const mimeType = info.mimeType ?? 'application/octet-stream';
|
|
51
|
+
const encoding = info.encoding;
|
|
52
|
+
const filename = makeTmpFilename({
|
|
53
|
+
prefix: ctx.opts.filenamePrefix,
|
|
54
|
+
originalName,
|
|
55
|
+
});
|
|
56
|
+
const tmpPath = NodeSingletons.path.join(ctx.opts.tmpDir, filename);
|
|
57
|
+
ctx.createdPaths.add(tmpPath);
|
|
58
|
+
const writeStream = NodeSingletons.fs.createWriteStream(tmpPath, { flags: 'wx' });
|
|
59
|
+
let size = 0;
|
|
60
|
+
const sha256 = NodeSingletons.createHash('sha256');
|
|
61
|
+
fileStream.on('data', (chunk) => {
|
|
62
|
+
size += chunk.length;
|
|
63
|
+
sha256.update(chunk);
|
|
64
|
+
});
|
|
65
|
+
fileStream.on('limit', () => {
|
|
66
|
+
fileStream.unpipe(writeStream);
|
|
67
|
+
try {
|
|
68
|
+
writeStream.destroy();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// best-effort
|
|
72
|
+
}
|
|
73
|
+
ctx.rejectOnce(new Error('File too large'));
|
|
74
|
+
});
|
|
75
|
+
writeStream.on('error', (err) => {
|
|
76
|
+
ctx.rejectOnce(err);
|
|
77
|
+
});
|
|
78
|
+
const uploadedFile = {
|
|
79
|
+
fieldName,
|
|
80
|
+
originalName,
|
|
81
|
+
mimeType,
|
|
82
|
+
encoding,
|
|
83
|
+
size: 0,
|
|
84
|
+
path: tmpPath,
|
|
85
|
+
stream: () => NodeSingletons.fs.createReadStream(tmpPath),
|
|
86
|
+
cleanup: async () => {
|
|
87
|
+
await safeUnlink(tmpPath);
|
|
88
|
+
ctx.createdPaths.delete(tmpPath);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
ctx.files[fieldName] ??= [];
|
|
92
|
+
ctx.files[fieldName]?.push(uploadedFile);
|
|
93
|
+
writeStream.on('close', () => {
|
|
94
|
+
uploadedFile.size = size;
|
|
95
|
+
// Expose hash for advanced validation without reading into memory.
|
|
96
|
+
uploadedFile.sha256 = sha256.digest('hex');
|
|
97
|
+
});
|
|
98
|
+
fileStream.pipe(writeStream);
|
|
99
|
+
};
|
|
100
|
+
const setupBusboyHandlers = (bb, ctx) => {
|
|
101
|
+
bb.on('field', (name, value) => {
|
|
102
|
+
addFieldValue(ctx.fields, name, value);
|
|
103
|
+
});
|
|
104
|
+
bb.on('file', (fieldName, fileStream, info) => {
|
|
105
|
+
handleFileUpload(fieldName, fileStream, info, ctx);
|
|
106
|
+
});
|
|
107
|
+
bb.on('filesLimit', () => ctx.rejectOnce(new Error('Too many files')));
|
|
108
|
+
bb.on('fieldsLimit', () => ctx.rejectOnce(new Error('Too many fields')));
|
|
109
|
+
bb.on('partsLimit', () => ctx.rejectOnce(new Error('Too many parts')));
|
|
110
|
+
bb.on('error', (err) => ctx.rejectOnce(err));
|
|
111
|
+
};
|
|
112
|
+
const createBusboyInstance = (input) => {
|
|
113
|
+
return Busboy({
|
|
114
|
+
headers: input.req.headers,
|
|
115
|
+
limits: {
|
|
116
|
+
fileSize: input.limits.maxFileSizeBytes,
|
|
117
|
+
files: input.limits.maxFiles,
|
|
118
|
+
fields: input.limits.maxFields,
|
|
119
|
+
fieldSize: input.limits.maxFieldSizeBytes,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const createSettlementHandlers = (createdPaths) => {
|
|
124
|
+
let settled = false;
|
|
125
|
+
const cleanupAll = async () => {
|
|
126
|
+
await Promise.allSettled(Array.from(createdPaths).map((p) => safeUnlink(p)));
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
resolveOnce: (value, resolve) => {
|
|
130
|
+
if (settled)
|
|
131
|
+
return;
|
|
132
|
+
settled = true;
|
|
133
|
+
resolve(value);
|
|
134
|
+
},
|
|
135
|
+
rejectOnce: (err, reject) => {
|
|
136
|
+
if (settled)
|
|
137
|
+
return;
|
|
138
|
+
settled = true;
|
|
139
|
+
cleanupAll()
|
|
140
|
+
.finally(() => reject(err))
|
|
141
|
+
.catch(() => {
|
|
142
|
+
// Ignore cleanup errors during rejection
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
isSettled: () => settled,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const executeParsing = (input, opts, fields, files, createdPaths, resolve, reject) => {
|
|
149
|
+
let finished = false;
|
|
150
|
+
const settlement = createSettlementHandlers(createdPaths);
|
|
151
|
+
const resolveOnce = (value) => {
|
|
152
|
+
settlement.resolveOnce(value, resolve);
|
|
153
|
+
};
|
|
154
|
+
const rejectOnce = (err) => {
|
|
155
|
+
settlement.rejectOnce(err, reject);
|
|
156
|
+
};
|
|
157
|
+
const bb = createBusboyInstance(input);
|
|
158
|
+
const onAbortOrClose = () => {
|
|
159
|
+
if (finished)
|
|
160
|
+
return;
|
|
161
|
+
rejectOnce(new Error('Upload aborted'));
|
|
162
|
+
};
|
|
163
|
+
input.req.once('aborted', onAbortOrClose);
|
|
164
|
+
input.req.once('close', onAbortOrClose);
|
|
165
|
+
const ctx = {
|
|
166
|
+
fields,
|
|
167
|
+
files,
|
|
168
|
+
createdPaths,
|
|
169
|
+
finished: false,
|
|
170
|
+
rejectOnce,
|
|
171
|
+
opts,
|
|
172
|
+
};
|
|
173
|
+
setupBusboyHandlers(bb, ctx);
|
|
174
|
+
bb.on('finish', () => {
|
|
175
|
+
finished = true;
|
|
176
|
+
ctx.finished = true;
|
|
177
|
+
resolveOnce({ fields, files });
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
input.req.pipe(bb);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
rejectOnce(err);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const parseWithBusboy = async (input, opts) => {
|
|
187
|
+
await ensureDir(opts.tmpDir);
|
|
188
|
+
const fields = {};
|
|
189
|
+
const files = {};
|
|
190
|
+
const createdPaths = new Set();
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
executeParsing(input, opts, fields, files, createdPaths, resolve, reject);
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
export function registerStreamingMultipartParser(options = {}) {
|
|
196
|
+
const tmpDir = options.tmpDir ?? defaultTmpDir();
|
|
197
|
+
const filenamePrefix = options.filenamePrefix ?? 'upload-';
|
|
198
|
+
MultipartParserRegistry.register((input) => parseWithBusboy(input, { tmpDir, filenamePrefix }));
|
|
199
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zintrust/storage",
|
|
3
|
+
"version": "0.1.27",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./register": {
|
|
17
|
+
"types": "./dist/register.d.ts",
|
|
18
|
+
"default": "./dist/register.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.0.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@zintrust/core": "^0.1.27"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"busboy": "^1.6.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
}
|
|
37
|
+
}
|