@opra/http 1.25.6 → 1.26.1

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/http-context.d.ts CHANGED
@@ -23,7 +23,9 @@ export declare class HttpContext extends ExecutionContext {
23
23
  constructor(init: HttpContext.Initiator);
24
24
  get isMultipart(): boolean;
25
25
  getMultipartReader(): Promise<MultipartReader>;
26
- getBody<T>(): Promise<T>;
26
+ getBody<T>(args?: {
27
+ toFile: boolean | string;
28
+ }): Promise<T>;
27
29
  }
28
30
  export declare namespace HttpContext {
29
31
  interface Initiator extends Omit<ExecutionContext.Initiator, '__adapter' | '__docNode' | 'transport'> {
package/http-context.js CHANGED
@@ -72,7 +72,7 @@ export class HttpContext extends ExecutionContext {
72
72
  this._multipartReader = reader;
73
73
  return reader;
74
74
  }
75
- async getBody() {
75
+ async getBody(args) {
76
76
  if (this._body !== undefined)
77
77
  return this._body;
78
78
  try {
@@ -87,6 +87,7 @@ export class HttpContext extends ExecutionContext {
87
87
  }
88
88
  this._body = await this.request.readBody({
89
89
  limit: __oprDef?.requestBody?.maxContentSize,
90
+ toFile: args?.toFile,
90
91
  });
91
92
  if (this._body != null) {
92
93
  const encoding = request.characterEncoding();
@@ -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
- export declare namespace MultipartReader {
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
+ }
@@ -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 = generateFileName(info, this.tempDirectory);
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
- mimeType: info.mimeType,
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.mimeType && arr.find(ct => typeIs.is(item.mimeType, [ct])))) {
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
- function generateFileName(info, tempDirectory) {
206
- let filename;
207
- let prefix = '';
208
- while (true) {
209
- filename = nodePath.posix.join(tempDirectory, info.filename ? prefix + info.filename : 'opra-' + uid(12));
210
- if (!fs.existsSync(filename))
211
- return filename;
212
- prefix = uid(6) + '-';
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.25.6",
3
+ "version": "1.26.1",
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.1",
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.25.6",
42
- "@opra/core": "^1.25.6"
41
+ "@opra/common": "^1.26.1",
42
+ "@opra/core": "^1.26.1"
43
43
  },
44
44
  "optionalDependencies": {
45
45
  "express": "^4.0.0 || ^5.0.0",
@@ -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;
@@ -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 || '';