@tus/server
Advanced tools
Comparing version 1.5.0 to 1.6.0
/// <reference types="node" /> | ||
/// <reference types="node" /> | ||
import EventEmitter from 'node:events'; | ||
import type http from 'node:http'; | ||
import type { ServerOptions } from '../types'; | ||
import type { DataStore, CancellationContext } from '@tus/utils'; | ||
import type http from 'node:http'; | ||
import { Upload } from '@tus/utils'; | ||
@@ -21,3 +21,3 @@ export declare class BaseHandler extends EventEmitter { | ||
protected acquireLock(req: http.IncomingMessage, id: string, context: CancellationContext): Promise<import("@tus/utils").Lock>; | ||
protected writeToStore(req: http.IncomingMessage, id: string, offset: number, maxFileSize: number, context: CancellationContext): Promise<number>; | ||
protected writeToStore(req: http.IncomingMessage, upload: Upload, maxFileSize: number, context: CancellationContext): Promise<number>; | ||
getConfiguredMaxSize(req: http.IncomingMessage, id: string | null): number | Promise<number>; | ||
@@ -24,0 +24,0 @@ /** |
@@ -8,5 +8,6 @@ "use strict"; | ||
const node_events_1 = __importDefault(require("node:events")); | ||
const promises_1 = __importDefault(require("node:stream/promises")); | ||
const node_stream_1 = require("node:stream"); | ||
const utils_1 = require("@tus/utils"); | ||
const promises_1 = __importDefault(require("node:stream/promises")); | ||
const stream_1 = require("stream"); | ||
const lodash_throttle_1 = __importDefault(require("lodash.throttle")); | ||
const reExtractFileID = /([^/]+)\/?$/; | ||
@@ -97,3 +98,3 @@ const reForwardedHost = /host="?([^";]+)/; | ||
} | ||
writeToStore(req, id, offset, maxFileSize, context) { | ||
writeToStore(req, upload, maxFileSize, context) { | ||
return new Promise(async (resolve, reject) => { | ||
@@ -107,4 +108,4 @@ // Abort early if the operation has been cancelled. | ||
// This allows for aborting the write process without affecting the incoming request stream. | ||
const proxy = new stream_1.PassThrough(); | ||
(0, stream_1.addAbortSignal)(context.signal, proxy); | ||
const proxy = new node_stream_1.PassThrough(); | ||
(0, node_stream_1.addAbortSignal)(context.signal, proxy); | ||
proxy.on('error', (err) => { | ||
@@ -114,2 +115,10 @@ req.unpipe(proxy); | ||
}); | ||
const postReceive = (0, lodash_throttle_1.default)((offset) => { | ||
this.emit(utils_1.EVENTS.POST_RECEIVE_V2, req, { ...upload, offset }); | ||
}, this.options.postReceiveInterval, { leading: false }); | ||
let tempOffset = upload.offset; | ||
proxy.on('data', (chunk) => { | ||
tempOffset += chunk.byteLength; | ||
postReceive(tempOffset); | ||
}); | ||
req.on('error', () => { | ||
@@ -127,3 +136,3 @@ if (!proxy.closed) { | ||
.pipeline(req.pipe(proxy), new utils_1.StreamLimiter(maxFileSize), async (stream) => { | ||
return this.store.write(stream, id, offset); | ||
return this.store.write(stream, upload.id, upload.offset); | ||
}) | ||
@@ -130,0 +139,0 @@ .then(resolve) |
@@ -81,3 +81,3 @@ "use strict"; | ||
const maxBodySize = await this.calculateMaxBodySize(req, upload, maxFileSize); | ||
newOffset = await this.writeToStore(req, id, offset, maxBodySize, context); | ||
newOffset = await this.writeToStore(req, upload, maxBodySize, context); | ||
} | ||
@@ -89,5 +89,29 @@ finally { | ||
this.emit(utils_1.EVENTS.POST_RECEIVE, req, res, upload); | ||
//Recommended response defaults | ||
const responseData = { | ||
status: 204, | ||
headers: { | ||
'Upload-Offset': newOffset, | ||
}, | ||
body: '', | ||
}; | ||
if (newOffset === upload.size && this.options.onUploadFinish) { | ||
try { | ||
res = await this.options.onUploadFinish(req, res, upload); | ||
const resOrObject = await this.options.onUploadFinish(req, res, upload); | ||
// Backwards compatibility, remove in next major | ||
// Ugly check because we can't use `instanceof` because we mock the instance in tests | ||
if (typeof resOrObject.write === 'function' && | ||
typeof resOrObject.writeHead === 'function') { | ||
res = resOrObject; | ||
} | ||
else { | ||
const obj = resOrObject; | ||
res = obj.res; | ||
if (obj.status_code) | ||
responseData.status = obj.status_code; | ||
if (obj.body) | ||
responseData.body = obj.body; | ||
if (obj.headers) | ||
responseData.headers = Object.assign(obj.headers, responseData.headers); | ||
} | ||
} | ||
@@ -99,5 +123,2 @@ catch (error) { | ||
} | ||
const headers = { | ||
'Upload-Offset': newOffset, | ||
}; | ||
if (this.store.hasExtension('expiration') && | ||
@@ -110,6 +131,6 @@ this.store.getExpiration() > 0 && | ||
const dateString = new Date(creation.getTime() + this.store.getExpiration()).toUTCString(); | ||
headers['Upload-Expires'] = dateString; | ||
responseData.headers['Upload-Expires'] = dateString; | ||
} | ||
// The Server MUST acknowledge successful PATCH requests with the 204 | ||
const writtenRes = this.write(res, 204, headers); | ||
const writtenRes = this.write(res, responseData.status, responseData.headers, responseData.body); | ||
if (newOffset === upload.size) { | ||
@@ -116,0 +137,0 @@ this.emit(utils_1.EVENTS.POST_FINISH, req, writtenRes, upload); |
@@ -96,3 +96,8 @@ "use strict"; | ||
let url; | ||
let headers; | ||
//Recommended response defaults | ||
const responseData = { | ||
status: 201, | ||
headers: {}, | ||
body: '', | ||
}; | ||
try { | ||
@@ -103,8 +108,7 @@ await this.store.create(upload); | ||
isFinal = upload.size === 0 && !upload.sizeIsDeferred; | ||
headers = {}; | ||
// The request MIGHT include a Content-Type header when using creation-with-upload extension | ||
if ((0, HeaderValidator_1.validateHeader)('content-type', req.headers['content-type'])) { | ||
const bodyMaxSize = await this.calculateMaxBodySize(req, upload, maxFileSize); | ||
const newOffset = await this.writeToStore(req, id, 0, bodyMaxSize, context); | ||
headers['Upload-Offset'] = newOffset.toString(); | ||
const newOffset = await this.writeToStore(req, upload, bodyMaxSize, context); | ||
responseData.headers['Upload-Offset'] = newOffset.toString(); | ||
isFinal = newOffset === Number.parseInt(upload_length, 10); | ||
@@ -123,3 +127,19 @@ upload.offset = newOffset; | ||
try { | ||
res = await this.options.onUploadFinish(req, res, upload); | ||
const resOrObject = await this.options.onUploadFinish(req, res, upload); | ||
// Backwards compatibility, remove in next major | ||
// Ugly check because we can't use `instanceof` because we mock the instance in tests | ||
if (typeof resOrObject.write === 'function' && | ||
typeof resOrObject.writeHead === 'function') { | ||
res = resOrObject; | ||
} | ||
else { | ||
const obj = resOrObject; | ||
res = obj.res; | ||
if (obj.status_code) | ||
responseData.status = obj.status_code; | ||
if (obj.body) | ||
responseData.body = obj.body; | ||
if (obj.headers) | ||
responseData.headers = Object.assign(obj.headers, responseData.headers); | ||
} | ||
} | ||
@@ -140,6 +160,11 @@ catch (error) { | ||
// Value MUST be in RFC 7231 datetime format | ||
headers['Upload-Expires'] = new Date(creation.getTime() + this.store.getExpiration()).toUTCString(); | ||
responseData.headers['Upload-Expires'] = new Date(creation.getTime() + this.store.getExpiration()).toUTCString(); | ||
} | ||
} | ||
const writtenRes = this.write(res, 201, { Location: url, ...headers }); | ||
//Only append Location header if its valid for the final http status (201 or 3xx) | ||
if (responseData.status === 201 || | ||
(responseData.status >= 300 && responseData.status < 400)) { | ||
responseData.headers['Location'] = url; | ||
} | ||
const writtenRes = this.write(res, responseData.status, responseData.headers, responseData.body); | ||
if (isFinal) { | ||
@@ -146,0 +171,0 @@ this.emit(utils_1.EVENTS.POST_FINISH, req, writtenRes, upload); |
@@ -27,3 +27,5 @@ /// <reference types="node" /> | ||
[EVENTS.POST_CREATE]: (req: http.IncomingMessage, res: http.ServerResponse, upload: Upload, url: string) => void; | ||
/** @deprecated this is almost the same as POST_FINISH, use POST_RECEIVE_V2 instead */ | ||
[EVENTS.POST_RECEIVE]: (req: http.IncomingMessage, res: http.ServerResponse, upload: Upload) => void; | ||
[EVENTS.POST_RECEIVE_V2]: (req: http.IncomingMessage, upload: Upload) => void; | ||
[EVENTS.POST_FINISH]: (req: http.IncomingMessage, res: http.ServerResponse, upload: Upload) => void; | ||
@@ -30,0 +32,0 @@ [EVENTS.POST_TERMINATE]: (req: http.IncomingMessage, res: http.ServerResponse, id: string) => void; |
@@ -39,2 +39,5 @@ "use strict"; | ||
} | ||
if (!options.postReceiveInterval) { | ||
options.postReceiveInterval = 1000; | ||
} | ||
const { datastore, ...rest } = options; | ||
@@ -41,0 +44,0 @@ this.options = rest; |
@@ -30,2 +30,6 @@ /// <reference types="node" /> | ||
/** | ||
* Interval in milliseconds for sending progress of an upload over `EVENTS.POST_RECEIVE_V2` | ||
*/ | ||
postReceiveInterval?: number; | ||
/** | ||
* Control how the upload URL is generated. | ||
@@ -83,3 +87,4 @@ * @param req - The incoming HTTP request. | ||
* `onUploadFinish` will be invoked after an upload is completed but before a response is returned to the client. | ||
* If the function returns the (modified) response, the upload will finish. | ||
* You can optionally return `status_code`, `headers` and `body` to modify the response. | ||
* Note that the tus specification does not allow sending response body nor status code other than 204, but most clients support it. | ||
* If an error is thrown, the HTTP request will be aborted, and the provided `body` and `status_code` | ||
@@ -91,3 +96,8 @@ * (or their fallbacks) will be sent to the client. This can be used to implement post-processing validation. | ||
*/ | ||
onUploadFinish?: (req: http.IncomingMessage, res: http.ServerResponse, upload: Upload) => Promise<http.ServerResponse>; | ||
onUploadFinish?: (req: http.IncomingMessage, res: http.ServerResponse, upload: Upload) => Promise<http.ServerResponse | { | ||
res: http.ServerResponse; | ||
status_code?: number; | ||
headers?: Record<string, string | number>; | ||
body?: string; | ||
}>; | ||
/** | ||
@@ -94,0 +104,0 @@ * `onIncomingRequest` will be invoked when an incoming request is received. |
{ | ||
"$schema": "https://json.schemastore.org/package.json", | ||
"name": "@tus/server", | ||
"version": "1.5.0", | ||
"version": "1.6.0", | ||
"description": "Tus resumable upload protocol in Node.js", | ||
@@ -24,7 +24,9 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"@tus/utils": "^0.1.0", | ||
"debug": "^4.3.4" | ||
"@tus/utils": "^0.2.0", | ||
"debug": "^4.3.4", | ||
"lodash.throttle": "^4.1.1" | ||
}, | ||
"devDependencies": { | ||
"@types/debug": "^4.1.12", | ||
"@types/lodash.throttle": "^4.1.9", | ||
"@types/mocha": "^10.0.6", | ||
@@ -31,0 +33,0 @@ "@types/node": "^20.11.5", |
@@ -72,2 +72,7 @@ # `@tus/server` | ||
#### `options.postReceiveInterval` | ||
Interval in milliseconds for sending progress of an upload over | ||
[`POST_RECEIVE_V2`](#eventspost_receive_v2) (`number`). | ||
#### `options.relativeLocation` | ||
@@ -156,7 +161,9 @@ | ||
`onUploadFinish` will be invoked after an upload is completed but before a response is | ||
returned to the client (`(req, res, upload) => Promise<res>`). | ||
returned to the client (`(req, res, upload) => Promise<{ res: http.ServerResponse, status_code?: number, headers?: Record<string, string | number>, body?: string }>`). | ||
If the function returns the (modified) response, the upload will finish. You can `throw` | ||
an Object and the HTTP request will be aborted with the provided `body` and `status_code` | ||
(or their fallbacks). | ||
- You can optionally return `status_code`, `headers` and `body` to modify the response. | ||
Note that the tus specification does not allow sending response body nor status code | ||
other than 204, but most clients support it. Use at your own risk. | ||
- You can `throw` an Object and the HTTP request will be aborted with the provided `body` | ||
and `status_code` (or their fallbacks). | ||
@@ -233,4 +240,9 @@ This can be used to implement post-processing validation. | ||
Called every time a `PATCH` request is handled. | ||
**Deprecated**. | ||
Called every time an upload finished writing to the store. This event is emitted whenever | ||
the request handling is completed (which is the same as `onUploadFinish`, almost the same | ||
as `POST_FINISH`), whereas the `POST_RECEIVE_V2` event is emitted _while_ the request is | ||
being handled. | ||
```js | ||
@@ -242,2 +254,19 @@ const {EVENTS} = require('@tus/server') | ||
#### `POST_RECEIVE_V2` | ||
Called every [`postReceiveInterval`](#optionspostreceiveinterval) milliseconds for every | ||
upload while it‘s being written to the store. | ||
This means you are not guaranteed to get (all) events for an upload. For instance if | ||
`postReceiveInterval` is set to 1000ms and an PATCH request takes 500ms, no event is emitted. | ||
If the PATCH request takes 2500ms, you would get the offset at 2000ms, but not at 2500ms. | ||
Use `POST_FINISH` if you need to know when an upload is done. | ||
```js | ||
const {EVENTS} = require('@tus/server') | ||
// ... | ||
server.on(EVENTS.POST_RECEIVE_V2, (req, upload => {}) | ||
``` | ||
#### `POST_FINISH` | ||
@@ -244,0 +273,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
118825
2498
636
4
15
+ Addedlodash.throttle@^4.1.1
+ Added@tus/utils@0.2.0(transitive)
+ Addedlodash.throttle@4.1.1(transitive)
- Removed@tus/utils@0.1.0(transitive)
Updated@tus/utils@^0.2.0