@opra/http 1.25.6 → 1.26.0
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/impl/local-file.d.ts +23 -0
- package/impl/local-file.js +70 -0
- package/impl/multipart-reader.d.ts +21 -22
- package/impl/multipart-reader.js +15 -18
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +4 -4
- package/utils/body-reader.d.ts +5 -0
- package/utils/body-reader.js +37 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare class LocalFile {
|
|
2
|
+
private _autoDelete;
|
|
3
|
+
readonly storedPath: string;
|
|
4
|
+
filename: string;
|
|
5
|
+
type?: string;
|
|
6
|
+
encoding?: BufferEncoding;
|
|
7
|
+
constructor(storedPath: string, options?: LocalFile.Options);
|
|
8
|
+
text(): Promise<string>;
|
|
9
|
+
buffer(): Promise<Buffer>;
|
|
10
|
+
delete(): Promise<void>;
|
|
11
|
+
get size(): number;
|
|
12
|
+
get autoDelete(): boolean;
|
|
13
|
+
set autoDelete(value: boolean);
|
|
14
|
+
static tempFilename(filename?: string, tempDirectory?: string): string;
|
|
15
|
+
}
|
|
16
|
+
export declare namespace LocalFile {
|
|
17
|
+
interface Options {
|
|
18
|
+
filename?: string;
|
|
19
|
+
autoDelete?: boolean;
|
|
20
|
+
type?: string;
|
|
21
|
+
encoding?: BufferEncoding;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsAsync from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { uid } from 'uid';
|
|
6
|
+
const registry = new FinalizationRegistry((storedPath) => {
|
|
7
|
+
fs.unlink(storedPath, () => undefined);
|
|
8
|
+
});
|
|
9
|
+
export class LocalFile {
|
|
10
|
+
_autoDelete = false;
|
|
11
|
+
storedPath;
|
|
12
|
+
filename;
|
|
13
|
+
type;
|
|
14
|
+
encoding;
|
|
15
|
+
constructor(storedPath, options = {}) {
|
|
16
|
+
this.storedPath = storedPath;
|
|
17
|
+
this.filename = options.filename ?? path.basename(storedPath);
|
|
18
|
+
this.type = options.type;
|
|
19
|
+
if (options?.autoDelete)
|
|
20
|
+
this.autoDelete = true;
|
|
21
|
+
}
|
|
22
|
+
async text() {
|
|
23
|
+
return fsAsync.readFile(this.storedPath, this.encoding || 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
async buffer() {
|
|
26
|
+
return fsAsync.readFile(this.storedPath);
|
|
27
|
+
}
|
|
28
|
+
async delete() {
|
|
29
|
+
if (fs.existsSync(this.storedPath)) {
|
|
30
|
+
try {
|
|
31
|
+
await fsAsync.unlink(this.storedPath);
|
|
32
|
+
this.autoDelete = false;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error(`Failed to delete file ${this.storedPath}: ${error}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
get size() {
|
|
40
|
+
return fs.statSync(this.storedPath).size;
|
|
41
|
+
}
|
|
42
|
+
get autoDelete() {
|
|
43
|
+
return this._autoDelete;
|
|
44
|
+
}
|
|
45
|
+
set autoDelete(value) {
|
|
46
|
+
if (value === this._autoDelete)
|
|
47
|
+
return;
|
|
48
|
+
this._autoDelete = value;
|
|
49
|
+
if (value) {
|
|
50
|
+
registry.register(this, this.storedPath, this); // GC-based
|
|
51
|
+
// exit-based
|
|
52
|
+
process.finalization?.register(this, obj => obj.delete());
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
registry.unregister(this);
|
|
56
|
+
process.finalization?.unregister(this);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
static tempFilename(filename, tempDirectory) {
|
|
60
|
+
let filePath;
|
|
61
|
+
let prefix = '';
|
|
62
|
+
tempDirectory = tempDirectory || os.tmpdir();
|
|
63
|
+
while (true) {
|
|
64
|
+
filePath = path.posix.join(tempDirectory, filename ? prefix + filename : 'opra-' + uid(12));
|
|
65
|
+
if (!fs.existsSync(filePath))
|
|
66
|
+
return filePath;
|
|
67
|
+
prefix = uid(6) + '-';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -3,28 +3,7 @@ import busboy from 'busboy';
|
|
|
3
3
|
import { EventEmitter } from 'events';
|
|
4
4
|
import type { StrictOmit } from 'ts-gems';
|
|
5
5
|
import type { HttpContext } from '../http-context.js';
|
|
6
|
-
|
|
7
|
-
interface Options extends StrictOmit<busboy.BusboyConfig, 'headers'> {
|
|
8
|
-
tempDirectory?: string;
|
|
9
|
-
scope?: string;
|
|
10
|
-
}
|
|
11
|
-
interface FieldInfo {
|
|
12
|
-
kind: 'field';
|
|
13
|
-
field: string;
|
|
14
|
-
value?: any;
|
|
15
|
-
mimeType?: string;
|
|
16
|
-
encoding?: string;
|
|
17
|
-
}
|
|
18
|
-
interface FileInfo {
|
|
19
|
-
kind: 'file';
|
|
20
|
-
field: string;
|
|
21
|
-
filename: string;
|
|
22
|
-
storedPath: string;
|
|
23
|
-
mimeType?: string;
|
|
24
|
-
encoding?: string;
|
|
25
|
-
}
|
|
26
|
-
type Item = FieldInfo | FileInfo;
|
|
27
|
-
}
|
|
6
|
+
import { LocalFile } from './local-file.js';
|
|
28
7
|
export declare class MultipartReader extends EventEmitter {
|
|
29
8
|
protected context: HttpContext;
|
|
30
9
|
protected mediaType?: HttpMediaType | undefined;
|
|
@@ -46,3 +25,23 @@ export declare class MultipartReader extends EventEmitter {
|
|
|
46
25
|
pause(): void;
|
|
47
26
|
purge(): Promise<PromiseSettledResult<any>[]>;
|
|
48
27
|
}
|
|
28
|
+
export declare class MultipartFile extends LocalFile {
|
|
29
|
+
readonly kind = "file";
|
|
30
|
+
readonly field: string;
|
|
31
|
+
constructor(field: string, storedPath: string, options?: LocalFile.Options);
|
|
32
|
+
}
|
|
33
|
+
export declare namespace MultipartReader {
|
|
34
|
+
interface Options extends StrictOmit<busboy.BusboyConfig, 'headers'> {
|
|
35
|
+
tempDirectory?: string;
|
|
36
|
+
scope?: string;
|
|
37
|
+
}
|
|
38
|
+
interface Field {
|
|
39
|
+
kind: 'field';
|
|
40
|
+
field: string;
|
|
41
|
+
value?: any;
|
|
42
|
+
mimeType?: string;
|
|
43
|
+
encoding?: string;
|
|
44
|
+
}
|
|
45
|
+
type File = MultipartFile;
|
|
46
|
+
type Item = Field | File;
|
|
47
|
+
}
|
package/impl/multipart-reader.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
-
import nodePath from 'node:path';
|
|
4
3
|
import typeIs from '@browsery/type-is';
|
|
5
4
|
import { BadRequestError } from '@opra/common';
|
|
6
5
|
import busboy from 'busboy';
|
|
7
6
|
import { EventEmitter } from 'events';
|
|
8
7
|
import fsPromise from 'fs/promises';
|
|
9
|
-
import { uid } from 'uid';
|
|
10
8
|
import { isNotNullish } from 'valgen';
|
|
9
|
+
import { LocalFile } from './local-file.js';
|
|
11
10
|
export class MultipartReader extends EventEmitter {
|
|
12
11
|
context;
|
|
13
12
|
mediaType;
|
|
@@ -52,17 +51,15 @@ export class MultipartReader extends EventEmitter {
|
|
|
52
51
|
this.emit('item', item);
|
|
53
52
|
});
|
|
54
53
|
form.on('file', (field, file, info) => {
|
|
55
|
-
const saveTo =
|
|
54
|
+
const saveTo = LocalFile.tempFilename(info.filename, this.tempDirectory);
|
|
56
55
|
file.pipe(fs.createWriteStream(saveTo));
|
|
57
56
|
file.once('end', () => {
|
|
58
|
-
const item = {
|
|
59
|
-
kind: 'file',
|
|
60
|
-
field,
|
|
61
|
-
storedPath: saveTo,
|
|
57
|
+
const item = new MultipartFile(field, saveTo, {
|
|
62
58
|
filename: info.filename,
|
|
63
|
-
|
|
59
|
+
type: info.mimeType,
|
|
64
60
|
encoding: info.encoding,
|
|
65
|
-
|
|
61
|
+
autoDelete: true,
|
|
62
|
+
});
|
|
66
63
|
this._items.push(item);
|
|
67
64
|
this._stack.push(item);
|
|
68
65
|
this.emit('file', item);
|
|
@@ -119,7 +116,7 @@ export class MultipartReader extends EventEmitter {
|
|
|
119
116
|
const arr = Array.isArray(field.contentType)
|
|
120
117
|
? field.contentType
|
|
121
118
|
: [field.contentType];
|
|
122
|
-
if (!(item.
|
|
119
|
+
if (!(item.type && arr.find(ct => typeIs.is(item.type, [ct])))) {
|
|
123
120
|
throw new BadRequestError(`Multipart field (${item.field}) do not accept this content type`);
|
|
124
121
|
}
|
|
125
122
|
}
|
|
@@ -202,13 +199,13 @@ export class MultipartReader extends EventEmitter {
|
|
|
202
199
|
return Promise.allSettled(promises);
|
|
203
200
|
}
|
|
204
201
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
202
|
+
export class MultipartFile extends LocalFile {
|
|
203
|
+
kind = 'file';
|
|
204
|
+
field;
|
|
205
|
+
constructor(field, storedPath, options = {
|
|
206
|
+
autoDelete: true,
|
|
207
|
+
}) {
|
|
208
|
+
super(storedPath, options);
|
|
209
|
+
this.field = field;
|
|
213
210
|
}
|
|
214
211
|
}
|
package/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './express-adapter.js';
|
|
|
7
7
|
export * from './http-adapter.js';
|
|
8
8
|
export * from './http-context.js';
|
|
9
9
|
export * from './http-handler.js';
|
|
10
|
+
export * from './impl/local-file.js';
|
|
10
11
|
export * from './impl/multipart-reader.js';
|
|
11
12
|
export * from './interfaces/http-incoming.interface.js';
|
|
12
13
|
export * from './interfaces/http-outgoing.interface.js';
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from './express-adapter.js';
|
|
|
7
7
|
export * from './http-adapter.js';
|
|
8
8
|
export * from './http-context.js';
|
|
9
9
|
export * from './http-handler.js';
|
|
10
|
+
export * from './impl/local-file.js';
|
|
10
11
|
export * from './impl/multipart-reader.js';
|
|
11
12
|
export * from './interfaces/http-incoming.interface.js';
|
|
12
13
|
export * from './interfaces/http-outgoing.interface.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opra/http",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "Opra Http Server Adapter",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"base64-stream": "^1.0.0",
|
|
14
14
|
"busboy": "^1.6.0",
|
|
15
15
|
"bytes": "^3.1.2",
|
|
16
|
-
"content-disposition": "^1.0
|
|
16
|
+
"content-disposition": "^1.1.0",
|
|
17
17
|
"content-type": "^1.0.5",
|
|
18
18
|
"cookie": "^1.1.1",
|
|
19
19
|
"cookie-signature": "^1.2.2",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"yaml": "^2.8.3"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@opra/common": "^1.
|
|
42
|
-
"@opra/core": "^1.
|
|
41
|
+
"@opra/common": "^1.26.0",
|
|
42
|
+
"@opra/core": "^1.26.0"
|
|
43
43
|
},
|
|
44
44
|
"optionalDependencies": {
|
|
45
45
|
"express": "^4.0.0 || ^5.0.0",
|
package/utils/body-reader.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import nodeStream from 'node:stream';
|
|
2
3
|
import { EventEmitter } from 'events';
|
|
4
|
+
import { LocalFile } from '../impl/local-file.js';
|
|
3
5
|
import type { HttpIncoming } from '../interfaces/http-incoming.interface.js';
|
|
4
6
|
/**
|
|
5
7
|
*
|
|
@@ -8,6 +10,7 @@ import type { HttpIncoming } from '../interfaces/http-incoming.interface.js';
|
|
|
8
10
|
export declare namespace BodyReader {
|
|
9
11
|
interface Options {
|
|
10
12
|
limit?: number | string;
|
|
13
|
+
toFile?: boolean | string;
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
type Callback = (...args: any[]) => any;
|
|
@@ -22,6 +25,8 @@ export declare class BodyReader extends EventEmitter {
|
|
|
22
25
|
protected _buffer?: string | Buffer[];
|
|
23
26
|
protected _completed?: boolean | undefined;
|
|
24
27
|
protected _receivedSize: number;
|
|
28
|
+
protected _file?: LocalFile;
|
|
29
|
+
protected _fileStream?: fs.WriteStream;
|
|
25
30
|
protected cleanup: Callback;
|
|
26
31
|
protected onAborted: Callback;
|
|
27
32
|
protected onData: Callback;
|
package/utils/body-reader.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import typeIs from '@browsery/type-is';
|
|
2
4
|
import { BadRequestError, InternalServerError, OpraHttpError, } from '@opra/common';
|
|
3
5
|
import { Base64Decode } from 'base64-stream';
|
|
@@ -7,6 +9,7 @@ import { EventEmitter } from 'events';
|
|
|
7
9
|
import iconv from 'iconv-lite';
|
|
8
10
|
import { Writable } from 'stream';
|
|
9
11
|
import * as zlib from 'zlib';
|
|
12
|
+
import { LocalFile } from '../impl/local-file.js';
|
|
10
13
|
/**
|
|
11
14
|
*
|
|
12
15
|
* @class BodyReader
|
|
@@ -18,6 +21,8 @@ export class BodyReader extends EventEmitter {
|
|
|
18
21
|
_buffer;
|
|
19
22
|
_completed = false;
|
|
20
23
|
_receivedSize = 0;
|
|
24
|
+
_file;
|
|
25
|
+
_fileStream;
|
|
21
26
|
cleanup;
|
|
22
27
|
onAborted;
|
|
23
28
|
onData;
|
|
@@ -34,6 +39,19 @@ export class BodyReader extends EventEmitter {
|
|
|
34
39
|
? options.limit
|
|
35
40
|
: byteParser(options.limit) || undefined
|
|
36
41
|
: undefined;
|
|
42
|
+
if (options?.toFile === true) {
|
|
43
|
+
this._file = new LocalFile(LocalFile.tempFilename());
|
|
44
|
+
}
|
|
45
|
+
else if (typeof options?.toFile === 'string') {
|
|
46
|
+
if (path.isAbsolute(options?.toFile)) {
|
|
47
|
+
this._file = new LocalFile(options?.toFile);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this._file = new LocalFile(LocalFile.tempFilename(options?.toFile));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (this._file)
|
|
54
|
+
this._file.autoDelete = true;
|
|
37
55
|
}
|
|
38
56
|
async read() {
|
|
39
57
|
/* istanbul ignore next */
|
|
@@ -118,6 +136,10 @@ export class BodyReader extends EventEmitter {
|
|
|
118
136
|
stream = newStream;
|
|
119
137
|
}
|
|
120
138
|
this._stream = stream;
|
|
139
|
+
if (this._file) {
|
|
140
|
+
this._fileStream = fs.createWriteStream(this._file.storedPath);
|
|
141
|
+
stream.pipe(this._fileStream);
|
|
142
|
+
}
|
|
121
143
|
// attach listeners
|
|
122
144
|
stream.on('aborted', this.onAborted);
|
|
123
145
|
stream.on('close', this.cleanup);
|
|
@@ -133,9 +155,22 @@ export class BodyReader extends EventEmitter {
|
|
|
133
155
|
if (error) {
|
|
134
156
|
this._stream?.unpipe();
|
|
135
157
|
this._stream?.pause();
|
|
158
|
+
this._fileStream?.close(() => {
|
|
159
|
+
this._file?.delete();
|
|
160
|
+
});
|
|
136
161
|
}
|
|
137
|
-
if (error)
|
|
162
|
+
if (error) {
|
|
138
163
|
this.emit('finish', error);
|
|
164
|
+
}
|
|
165
|
+
else if (this._file) {
|
|
166
|
+
if (this._fileStream) {
|
|
167
|
+
this._fileStream.once('finish', () => {
|
|
168
|
+
this.emit('finish', undefined, this._file);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
this.emit('finish', error, undefined);
|
|
173
|
+
}
|
|
139
174
|
else if (Array.isArray(this._buffer))
|
|
140
175
|
this.emit('finish', error, Buffer.concat(this._buffer));
|
|
141
176
|
else
|
|
@@ -163,7 +198,7 @@ export class BodyReader extends EventEmitter {
|
|
|
163
198
|
}));
|
|
164
199
|
}
|
|
165
200
|
_onData(chunk) {
|
|
166
|
-
if (this._completed)
|
|
201
|
+
if (this._completed || this._file)
|
|
167
202
|
return;
|
|
168
203
|
if (typeof chunk === 'string') {
|
|
169
204
|
this._buffer = this._buffer || '';
|