Socket
Socket
Sign inDemoInstall

@tus/server

Package Overview
Dependencies
Maintainers
3
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tus/server - npm Package Compare versions

Comparing version 1.0.1 to 1.1.0

dist/lockers/index.d.ts

8

dist/constants.d.ts

@@ -12,2 +12,10 @@ export declare const REQUEST_METHODS: readonly ["POST", "HEAD", "PATCH", "OPTIONS", "DELETE"];

};
readonly ABORTED: {
readonly status_code: 400;
readonly body: "Request aborted due to lock acquired";
};
readonly ERR_LOCK_TIMEOUT: {
readonly status_code: 500;
readonly body: "failed to acquire lock before timeout";
};
readonly INVALID_CONTENT_TYPE: {

@@ -14,0 +22,0 @@ readonly status_code: 403;

@@ -35,2 +35,10 @@ "use strict";

},
ABORTED: {
status_code: 400,
body: 'Request aborted due to lock acquired',
},
ERR_LOCK_TIMEOUT: {
status_code: 500,
body: 'failed to acquire lock before timeout',
},
INVALID_CONTENT_TYPE: {

@@ -37,0 +45,0 @@ status_code: 403,

11

dist/handlers/BaseHandler.d.ts

@@ -5,3 +5,3 @@ /// <reference types="node" />

import type { ServerOptions } from '../types';
import type { DataStore } from '../models';
import type { DataStore, CancellationContext } from '../models';
import type http from 'node:http';

@@ -14,3 +14,10 @@ export declare class BaseHandler extends EventEmitter {

generateUrl(req: http.IncomingMessage, id: string): string;
getFileIdFromRequest(req: http.IncomingMessage): string | false;
getFileIdFromRequest(req: http.IncomingMessage): string | void;
protected extractHostAndProto(req: http.IncomingMessage): {
host: string;
proto: string;
};
protected getLocker(req: http.IncomingMessage): Promise<import("../models").Locker>;
protected acquireLock(req: http.IncomingMessage, id: string, context: CancellationContext): Promise<import("../models").Lock>;
protected writeToStore(req: http.IncomingMessage, id: string, offset: number, context: CancellationContext): Promise<number>;
}

@@ -8,2 +8,4 @@ "use strict";

const node_events_1 = __importDefault(require("node:events"));
const node_stream_1 = __importDefault(require("node:stream"));
const constants_1 = require("../constants");
const reExtractFileID = /([^/]+)\/?$/;

@@ -31,13 +33,39 @@ const reForwardedHost = /host="?([^";]+)/;

generateUrl(req, id) {
id = encodeURIComponent(id);
const forwarded = req.headers.forwarded;
// @ts-expect-error req.baseUrl does exist
const baseUrl = req.baseUrl ?? '';
const path = this.options.path === '/' ? '' : this.options.path;
// @ts-expect-error baseUrl type doesn't exist?
const baseUrl = req.baseUrl ?? '';
let proto;
let host;
if (this.options.generateUrl) {
// user-defined generateUrl function
const { proto, host } = this.extractHostAndProto(req);
return this.options.generateUrl(req, {
proto,
host,
// @ts-expect-error we can pass undefined
baseUrl: req.baseUrl,
path: path,
id,
});
}
// Default implementation
if (this.options.relativeLocation) {
return `${baseUrl}${path}/${id}`;
}
const { proto, host } = this.extractHostAndProto(req);
return `${proto}://${host}${baseUrl}${path}/${id}`;
}
getFileIdFromRequest(req) {
if (this.options.getFileIdFromRequest) {
return this.options.getFileIdFromRequest(req);
}
const match = reExtractFileID.exec(req.url);
if (!match || this.options.path.includes(match[1])) {
return;
}
return decodeURIComponent(match[1]);
}
extractHostAndProto(req) {
let proto;
let host;
if (this.options.respectForwardedHeaders) {
const forwarded = req.headers.forwarded;
if (forwarded) {

@@ -57,12 +85,44 @@ host ?? (host = reForwardedHost.exec(forwarded)?.[1]);

proto ?? (proto = 'http');
return `${proto}://${host}${baseUrl}${path}/${id}`;
return { host: host, proto };
}
getFileIdFromRequest(req) {
const match = reExtractFileID.exec(req.url);
if (!match || this.options.path.includes(match[1])) {
return false;
async getLocker(req) {
if (typeof this.options.locker === 'function') {
return this.options.locker(req);
}
return decodeURIComponent(match[1]);
return this.options.locker;
}
async acquireLock(req, id, context) {
const locker = await this.getLocker(req);
const lock = locker.newLock(id);
await lock.lock(() => {
context.cancel();
});
return lock;
}
writeToStore(req, id, offset, context) {
return new Promise(async (resolve, reject) => {
if (context.signal.aborted) {
reject(constants_1.ERRORS.ABORTED);
return;
}
const proxy = new node_stream_1.default.PassThrough();
node_stream_1.default.addAbortSignal(context.signal, proxy);
proxy.on('error', (err) => {
req.unpipe(proxy);
if (err.name === 'AbortError') {
reject(constants_1.ERRORS.ABORTED);
}
else {
reject(err);
}
});
req.on('error', (err) => {
if (!proxy.closed) {
proxy.destroy(err);
}
});
this.store.write(req.pipe(proxy), id, offset).then(resolve).catch(reject);
});
}
}
exports.BaseHandler = BaseHandler;

3

dist/handlers/DeleteHandler.d.ts
/// <reference types="node" />
import { BaseHandler } from './BaseHandler';
import { CancellationContext } from '../models';
import type http from 'node:http';
export declare class DeleteHandler extends BaseHandler {
send(req: http.IncomingMessage, res: http.ServerResponse): Promise<http.ServerResponse<http.IncomingMessage>>;
send(req: http.IncomingMessage, res: http.ServerResponse, context: CancellationContext): Promise<http.ServerResponse<http.IncomingMessage>>;
}

@@ -7,8 +7,17 @@ "use strict";

class DeleteHandler extends BaseHandler_1.BaseHandler {
async send(req, res) {
async send(req, res, context) {
const id = this.getFileIdFromRequest(req);
if (id === false) {
if (!id) {
throw constants_1.ERRORS.FILE_NOT_FOUND;
}
await this.store.remove(id);
if (this.options.onIncomingRequest) {
await this.options.onIncomingRequest(req, res, id);
}
const lock = await this.acquireLock(req, id, context);
try {
await this.store.remove(id);
}
finally {
await lock.unlock();
}
const writtenRes = this.write(res, 204, {});

@@ -15,0 +24,0 @@ this.emit(constants_1.EVENTS.POST_TERMINATE, req, writtenRes, id);

@@ -32,5 +32,8 @@ "use strict";

const id = this.getFileIdFromRequest(req);
if (id === false) {
if (!id) {
throw constants_1.ERRORS.FILE_NOT_FOUND;
}
if (this.options.onIncomingRequest) {
await this.options.onIncomingRequest(req, res, id);
}
const stats = await this.store.getUpload(id);

@@ -37,0 +40,0 @@ if (!stats || stats.offset !== stats.size) {

/// <reference types="node" />
import { BaseHandler } from './BaseHandler';
import { CancellationContext } from '../models';
import type http from 'node:http';
export declare class HeadHandler extends BaseHandler {
send(req: http.IncomingMessage, res: http.ServerResponse): Promise<http.ServerResponse<http.IncomingMessage>>;
send(req: http.IncomingMessage, res: http.ServerResponse, context: CancellationContext): Promise<http.ServerResponse<http.IncomingMessage>>;
}

@@ -8,8 +8,18 @@ "use strict";

class HeadHandler extends BaseHandler_1.BaseHandler {
async send(req, res) {
async send(req, res, context) {
const id = this.getFileIdFromRequest(req);
if (id === false) {
if (!id) {
throw constants_1.ERRORS.FILE_NOT_FOUND;
}
const file = await this.store.getUpload(id);
if (this.options.onIncomingRequest) {
await this.options.onIncomingRequest(req, res, id);
}
const lock = await this.acquireLock(req, id, context);
let file;
try {
file = await this.store.getUpload(id);
}
finally {
await lock.unlock();
}
// If a Client does attempt to resume an upload which has since

@@ -16,0 +26,0 @@ // been removed by the Server, the Server SHOULD respond with the

@@ -10,4 +10,5 @@ "use strict";

async send(_, res) {
const allowedHeaders = [...constants_1.HEADERS, ...(this.options.allowedHeaders ?? [])];
res.setHeader('Access-Control-Allow-Methods', constants_1.ALLOWED_METHODS);
res.setHeader('Access-Control-Allow-Headers', constants_1.ALLOWED_HEADERS);
res.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(', '));
res.setHeader('Access-Control-Max-Age', constants_1.MAX_AGE);

@@ -14,0 +15,0 @@ if (this.store.extensions.length > 0) {

/// <reference types="node" />
import { BaseHandler } from './BaseHandler';
import type http from 'node:http';
import { CancellationContext } from '../models';
export declare class PatchHandler extends BaseHandler {

@@ -8,3 +9,3 @@ /**

*/
send(req: http.IncomingMessage, res: http.ServerResponse): Promise<http.ServerResponse<http.IncomingMessage>>;
send(req: http.IncomingMessage, res: http.ServerResponse, context: CancellationContext): Promise<http.ServerResponse<http.IncomingMessage>>;
}

@@ -15,85 +15,101 @@ "use strict";

*/
async send(req, res) {
const id = this.getFileIdFromRequest(req);
if (id === false) {
throw constants_1.ERRORS.FILE_NOT_FOUND;
}
// The request MUST include a Upload-Offset header
if (req.headers['upload-offset'] === undefined) {
throw constants_1.ERRORS.MISSING_OFFSET;
}
const offset = Number.parseInt(req.headers['upload-offset'], 10);
// The request MUST include a Content-Type header
const content_type = req.headers['content-type'];
if (content_type === undefined) {
throw constants_1.ERRORS.INVALID_CONTENT_TYPE;
}
const upload = await this.store.getUpload(id);
// If a Client does attempt to resume an upload which has since
// been removed by the Server, the Server SHOULD respond with the
// with the 404 Not Found or 410 Gone status. The latter one SHOULD
// be used if the Server is keeping track of expired uploads.
const now = Date.now();
const creation = upload.creation_date ? new Date(upload.creation_date).getTime() : now;
const expiration = creation + this.store.getExpiration();
if (this.store.hasExtension('expiration') &&
this.store.getExpiration() > 0 &&
now > expiration) {
throw constants_1.ERRORS.FILE_NO_LONGER_EXISTS;
}
if (upload.offset !== offset) {
// If the offsets do not match, the Server MUST respond with the 409 Conflict status without modifying the upload resource.
log(`[PatchHandler] send: Incorrect offset - ${offset} sent but file is ${upload.offset}`);
throw constants_1.ERRORS.INVALID_OFFSET;
}
// The request MUST validate upload-length related headers
const upload_length = req.headers['upload-length'];
if (upload_length !== undefined) {
const size = Number.parseInt(upload_length, 10);
// Throw error if extension is not supported
if (!this.store.hasExtension('creation-defer-length')) {
throw constants_1.ERRORS.UNSUPPORTED_CREATION_DEFER_LENGTH_EXTENSION;
async send(req, res, context) {
try {
const id = this.getFileIdFromRequest(req);
if (!id) {
throw constants_1.ERRORS.FILE_NOT_FOUND;
}
// Throw error if upload-length is already set.
if (upload.size !== undefined) {
throw constants_1.ERRORS.INVALID_LENGTH;
// The request MUST include a Upload-Offset header
if (req.headers['upload-offset'] === undefined) {
throw constants_1.ERRORS.MISSING_OFFSET;
}
if (size < upload.offset) {
throw constants_1.ERRORS.INVALID_LENGTH;
const offset = Number.parseInt(req.headers['upload-offset'], 10);
// The request MUST include a Content-Type header
const content_type = req.headers['content-type'];
if (content_type === undefined) {
throw constants_1.ERRORS.INVALID_CONTENT_TYPE;
}
await this.store.declareUploadLength(id, size);
upload.size = size;
}
const newOffset = await this.store.write(req, id, offset);
upload.offset = newOffset;
this.emit(constants_1.EVENTS.POST_RECEIVE, req, res, upload);
if (newOffset === upload.size && this.options.onUploadFinish) {
const lock = await this.acquireLock(req, id, context);
let upload;
let newOffset;
try {
res = await this.options.onUploadFinish(req, res, upload);
upload = await this.store.getUpload(id);
// If a Client does attempt to resume an upload which has since
// been removed by the Server, the Server SHOULD respond with the
// with the 404 Not Found or 410 Gone status. The latter one SHOULD
// be used if the Server is keeping track of expired uploads.
const now = Date.now();
const creation = upload.creation_date
? new Date(upload.creation_date).getTime()
: now;
const expiration = creation + this.store.getExpiration();
if (this.store.hasExtension('expiration') &&
this.store.getExpiration() > 0 &&
now > expiration) {
throw constants_1.ERRORS.FILE_NO_LONGER_EXISTS;
}
if (upload.offset !== offset) {
// If the offsets do not match, the Server MUST respond with the 409 Conflict status without modifying the upload resource.
log(`[PatchHandler] send: Incorrect offset - ${offset} sent but file is ${upload.offset}`);
throw constants_1.ERRORS.INVALID_OFFSET;
}
// The request MUST validate upload-length related headers
const upload_length = req.headers['upload-length'];
if (upload_length !== undefined) {
const size = Number.parseInt(upload_length, 10);
// Throw error if extension is not supported
if (!this.store.hasExtension('creation-defer-length')) {
throw constants_1.ERRORS.UNSUPPORTED_CREATION_DEFER_LENGTH_EXTENSION;
}
// Throw error if upload-length is already set.
if (upload.size !== undefined) {
throw constants_1.ERRORS.INVALID_LENGTH;
}
if (size < upload.offset) {
throw constants_1.ERRORS.INVALID_LENGTH;
}
await this.store.declareUploadLength(id, size);
upload.size = size;
}
newOffset = await this.writeToStore(req, id, offset, context);
}
catch (error) {
log(`onUploadFinish: ${error.body}`);
throw error;
finally {
await lock.unlock();
}
upload.offset = newOffset;
this.emit(constants_1.EVENTS.POST_RECEIVE, req, res, upload);
if (newOffset === upload.size && this.options.onUploadFinish) {
try {
res = await this.options.onUploadFinish(req, res, upload);
}
catch (error) {
log(`onUploadFinish: ${error.body}`);
throw error;
}
}
const headers = {
'Upload-Offset': newOffset,
};
if (this.store.hasExtension('expiration') &&
this.store.getExpiration() > 0 &&
upload.creation_date &&
(upload.size === undefined || newOffset < upload.size)) {
const creation = new Date(upload.creation_date);
// Value MUST be in RFC 7231 datetime format
const dateString = new Date(creation.getTime() + this.store.getExpiration()).toUTCString();
headers['Upload-Expires'] = dateString;
}
// The Server MUST acknowledge successful PATCH requests with the 204
const writtenRes = this.write(res, 204, headers);
if (newOffset === upload.size) {
this.emit(constants_1.EVENTS.POST_FINISH, req, writtenRes, upload);
}
return writtenRes;
}
const headers = {
'Upload-Offset': newOffset,
};
if (this.store.hasExtension('expiration') &&
this.store.getExpiration() > 0 &&
upload.creation_date &&
(upload.size === undefined || newOffset < upload.size)) {
const creation = new Date(upload.creation_date);
// Value MUST be in RFC 7231 datetime format
const dateString = new Date(creation.getTime() + this.store.getExpiration()).toUTCString();
headers['Upload-Expires'] = dateString;
catch (e) {
context.abort();
throw e;
}
// The Server MUST acknowledge successful PATCH requests with the 204
const writtenRes = this.write(res, 204, headers);
if (newOffset === upload.size) {
this.emit(constants_1.EVENTS.POST_FINISH, req, writtenRes, upload);
}
return writtenRes;
}
}
exports.PatchHandler = PatchHandler;
/// <reference types="node" />
import { BaseHandler } from './BaseHandler';
import type http from 'node:http';
import type { ServerOptions } from '../types';
import type { DataStore } from '../models';
import type { ServerOptions, WithRequired } from '../types';
import { DataStore, CancellationContext } from '../models';
export declare class PostHandler extends BaseHandler {
options: Required<Pick<ServerOptions, 'namingFunction'>> & Omit<ServerOptions, 'namingFunction'>;
options: WithRequired<ServerOptions, 'namingFunction'>;
constructor(store: DataStore, options: ServerOptions);

@@ -12,3 +12,3 @@ /**

*/
send(req: http.IncomingMessage, res: http.ServerResponse): Promise<http.ServerResponse<http.IncomingMessage>>;
send(req: http.IncomingMessage, res: http.ServerResponse, context: CancellationContext): Promise<http.ServerResponse<http.IncomingMessage>>;
}

@@ -26,3 +26,3 @@ "use strict";

*/
async send(req, res) {
async send(req, res, context) {
if ('upload-concat' in req.headers && !this.store.hasExtension('concatentation')) {

@@ -58,2 +58,5 @@ throw constants_1.ERRORS.UNSUPPORTED_CONCATENATION_EXTENSION;

}
if (this.options.onIncomingRequest) {
await this.options.onIncomingRequest(req, res, id);
}
const upload = new models_1.Upload({

@@ -74,15 +77,27 @@ id,

}
await this.store.create(upload);
const url = this.generateUrl(req, upload.id);
this.emit(constants_1.EVENTS.POST_CREATE, req, res, upload, url);
let newOffset;
let isFinal = upload.size === 0 && !upload.sizeIsDeferred;
const 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'])) {
newOffset = await this.store.write(req, upload.id, 0);
headers['Upload-Offset'] = newOffset.toString();
isFinal = newOffset === Number.parseInt(upload_length, 10);
upload.offset = newOffset;
const lock = await this.acquireLock(req, id, context);
let isFinal;
let url;
let headers;
try {
await this.store.create(upload);
url = this.generateUrl(req, upload.id);
this.emit(constants_1.EVENTS.POST_CREATE, req, res, upload, url);
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 newOffset = await this.writeToStore(req, id, 0, context);
headers['Upload-Offset'] = newOffset.toString();
isFinal = newOffset === Number.parseInt(upload_length, 10);
upload.offset = newOffset;
}
}
catch (e) {
context.abort();
throw e;
}
finally {
await lock.unlock();
}
if (isFinal && this.options.onUploadFinish) {

@@ -89,0 +104,0 @@ try {

export { Server } from './server';
export * from './types';
export * from './models';
export * from './lockers';
export * from './constants';

@@ -22,2 +22,3 @@ "use strict";

__exportStar(require("./models"), exports);
__exportStar(require("./lockers"), exports);
__exportStar(require("./constants"), exports);

@@ -6,1 +6,3 @@ export { DataStore } from './DataStore';

export { Upload } from './Upload';
export { Locker, Lock, RequestRelease } from './Locker';
export { CancellationContext } from './Context';
/// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
import http from 'node:http';

@@ -14,4 +15,4 @@ import { EventEmitter } from 'node:events';

import type stream from 'node:stream';
import type { ServerOptions, RouteHandler } from './types';
import type { DataStore, Upload } from './models';
import type { ServerOptions, RouteHandler, WithOptional } from './types';
import type { DataStore, Upload, CancellationContext } from './models';
type Handlers = {

@@ -43,3 +44,3 @@ GET: InstanceType<typeof GetHandler>;

options: ServerOptions;
constructor(options: ServerOptions & {
constructor(options: WithOptional<ServerOptions, 'locker'> & {
datastore: DataStore;

@@ -52,6 +53,11 @@ });

handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<http.ServerResponse | stream.Writable | void>;
write(res: http.ServerResponse, status: number, body?: string, headers?: {}): http.ServerResponse<http.IncomingMessage>;
write(context: CancellationContext, req: http.IncomingMessage, res: http.ServerResponse, status: number, body?: string, headers?: {}): http.ServerResponse<http.IncomingMessage>;
listen(...args: any[]): http.Server;
cleanUpExpiredUploads(): Promise<number>;
protected createContext(req: http.IncomingMessage): {
signal: AbortSignal;
abort: () => void;
cancel: () => void;
};
}
export {};

@@ -18,2 +18,3 @@ "use strict";

const constants_1 = require("./constants");
const lockers_1 = require("./lockers");
const log = (0, debug_1.default)('tus-node-server');

@@ -33,2 +34,5 @@ // eslint-disable-next-line no-redeclare

}
if (!options.locker) {
options.locker = new lockers_1.MemoryLocker();
}
const { datastore, ...rest } = options;

@@ -48,3 +52,3 @@ this.options = rest;

// Any handlers assigned to this object with the method as the key
// will be used to repond to those requests. They get set/re-set
// will be used to respond to those requests. They get set/re-set
// when a datastore is assigned to the server.

@@ -78,2 +82,3 @@ // Remove any event listeners from each handler as they are removed

) {
const context = this.createContext(req);
log(`[TusServer] handle: ${req.method} ${req.url}`);

@@ -86,13 +91,14 @@ // Allow overriding the HTTP method. The reason for this is

}
const onError = (error) => {
const status_code = error.status_code || constants_1.ERRORS.UNKNOWN_ERROR.status_code;
const body = error.body || `${constants_1.ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
return this.write(res, status_code, body);
const onError = async (error) => {
let status_code = error.status_code || constants_1.ERRORS.UNKNOWN_ERROR.status_code;
let body = error.body || `${constants_1.ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
if (this.options.onResponseError) {
const errorMapping = await this.options.onResponseError(req, res, error);
if (errorMapping) {
status_code = errorMapping.status_code;
body = errorMapping.body;
}
}
return this.write(context, req, res, status_code, body);
};
try {
await this.options.onIncomingRequest?.(req, res);
}
catch (err) {
return onError(err);
}
if (req.method === 'GET') {

@@ -107,3 +113,3 @@ const handler = this.handlers.GET;

if (req.method !== 'OPTIONS' && req.headers['tus-resumable'] === undefined) {
return this.write(res, 412, 'Tus-Resumable Required\n');
return this.write(context, req, res, 412, 'Tus-Resumable Required\n');
}

@@ -130,3 +136,3 @@ // Validate all required headers to adhere to the tus protocol

if (invalid_headers.length > 0) {
return this.write(res, 400, `Invalid ${invalid_headers.join(' ')}\n`);
return this.write(context, req, res, 400, `Invalid ${invalid_headers.join(' ')}\n`);
}

@@ -141,7 +147,8 @@ // Enable CORS

if (handler) {
return handler.send(req, res).catch(onError);
return handler.send(req, res, context).catch(onError);
}
return this.write(res, 404, 'Not found\n');
return this.write(context, req, res, 404, 'Not found\n');
}
write(res, status, body = '', headers = {}) {
write(context, req, res, status, body = '', headers = {}) {
const isAborted = context.signal.aborted;
if (status !== 204) {

@@ -151,2 +158,19 @@ // @ts-expect-error not explicitly typed but possible

}
if (isAborted) {
// This condition handles situations where the request has been flagged as aborted.
// In such cases, the server informs the client that the connection will be closed.
// This is communicated by setting the 'Connection' header to 'close' in the response.
// This step is essential to prevent the server from continuing to process a request
// that is no longer needed, thereby saving resources.
// @ts-expect-error not explicitly typed but possible
headers['Connection'] = 'close';
// An event listener is added to the response ('res') for the 'finish' event.
// The 'finish' event is triggered when the response has been sent to the client.
// Once the response is complete, the request ('req') object is destroyed.
// Destroying the request object is a crucial step to release any resources
// tied to this request, as it has already been aborted.
res.on('finish', () => {
req.destroy();
});
}
res.writeHead(status, headers);

@@ -166,3 +190,36 @@ res.write(body);

}
createContext(req) {
// Initialize two AbortControllers:
// 1. `requestAbortController` for instant request termination, particularly useful for stopping clients to upload when errors occur.
// 2. `abortWithDelayController` to introduce a delay before aborting, allowing the server time to complete ongoing operations.
// This is particularly useful when a future request may need to acquire a lock currently held by this request.
const requestAbortController = new AbortController();
const abortWithDelayController = new AbortController();
const onDelayedAbort = (err) => {
abortWithDelayController.signal.removeEventListener('abort', onDelayedAbort);
setTimeout(() => {
requestAbortController.abort(err);
}, 3000);
};
abortWithDelayController.signal.addEventListener('abort', onDelayedAbort);
req.on('close', () => {
abortWithDelayController.signal.removeEventListener('abort', onDelayedAbort);
});
return {
signal: requestAbortController.signal,
abort: () => {
// abort the request immediately
if (!requestAbortController.signal.aborted) {
requestAbortController.abort(constants_1.ERRORS.ABORTED);
}
},
cancel: () => {
// Initiates the delayed abort sequence unless it's already in progress.
if (!abortWithDelayController.signal.aborted) {
abortWithDelayController.abort(constants_1.ERRORS.ABORTED);
}
},
};
}
}
exports.Server = Server;
/// <reference types="node" />
import type http from 'node:http';
import type { Upload } from './models';
import type { Locker, Upload } from './models';
/**
* Represents the configuration options for a server.
*/
export type ServerOptions = {
/**
* The route to accept requests.
*/
path: string;
/**
* Return a relative URL as the `Location` header.
*/
relativeLocation?: boolean;
/**
* Allow `Forwarded`, `X-Forwarded-Proto`, and `X-Forwarded-Host` headers
* to override the `Location` header returned by the server.
*/
respectForwardedHeaders?: boolean;
/**
* Additional headers sent in `Access-Control-Allow-Headers`.
*/
allowedHeaders?: string[];
/**
* Control how the upload URL is generated.
* @param req - The incoming HTTP request.
* @param options - Options for generating the URL.
*/
generateUrl?: (req: http.IncomingMessage, options: {
proto: string;
host: string;
baseUrl: string;
path: string;
id: string;
}) => string;
/**
* Control how the Upload-ID is extracted from the request.
* @param req - The incoming HTTP request.
*/
getFileIdFromRequest?: (req: http.IncomingMessage) => string | void;
/**
* Control how you want to name files.
* It is important to make these unique to prevent data loss.
* Only use it if you really need to.
* Default uses `crypto.randomBytes(16).toString('hex')`.
* @param req - The incoming HTTP request.
*/
namingFunction?: (req: http.IncomingMessage) => string;
/**
* The Lock interface defines methods for implementing a locking mechanism.
* It is primarily used to ensure exclusive access to resources, such as uploads and their metadata.
*/
locker: Locker | Promise<Locker> | ((req: http.IncomingMessage) => Locker | Promise<Locker>);
/**
* `onUploadCreate` will be invoked before a new upload is created.
* If the function returns the (modified) response, the upload will be created.
* If an error is thrown, the HTTP request will be aborted, and the provided `body` and `status_code`
* (or their fallbacks) will be sent to the client. This can be used to implement validation of upload
* metadata or add headers.
* @param req - The incoming HTTP request.
* @param res - The HTTP response.
* @param upload - The Upload object.
*/
onUploadCreate?: (req: http.IncomingMessage, res: http.ServerResponse, upload: Upload) => Promise<http.ServerResponse>;
/**
* `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.
* If an error is thrown, the HTTP request will be aborted, and the provided `body` and `status_code`
* (or their fallbacks) will be sent to the client. This can be used to implement post-processing validation.
* @param req - The incoming HTTP request.
* @param res - The HTTP response.
* @param upload - The Upload object.
*/
onUploadFinish?: (req: http.IncomingMessage, res: http.ServerResponse, upload: Upload) => Promise<http.ServerResponse>;
onIncomingRequest?: (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void>;
/**
* `onIncomingRequest` will be invoked when an incoming request is received.
* @param req - The incoming HTTP request.
* @param res - The HTTP response.
* @param uploadId - The ID of the upload.
*/
onIncomingRequest?: (req: http.IncomingMessage, res: http.ServerResponse, uploadId: string) => Promise<void>;
/**
* `onResponseError` will be invoked when an error response is about to be sent by the server.
* Use this function to map custom errors to tus errors or for custom observability.
* @param req - The incoming HTTP request.
* @param res - The HTTP response.
* @param err - The error object or response.
*/
onResponseError?: (req: http.IncomingMessage, res: http.ServerResponse, err: Error | {
status_code: number;
body: string;
}) => Promise<{
status_code: number;
body: string;
} | void> | {
status_code: number;
body: string;
} | void;
};
export type RouteHandler = (req: http.IncomingMessage, res: http.ServerResponse) => void;
export type WithOptional<T, K extends keyof T> = Omit<T, K> & {
[P in K]+?: T[P];
};
export type WithRequired<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@tus/server",
"version": "1.0.1",
"version": "1.1.0",
"description": "Tus resumable upload protocol in Node.js",

@@ -6,0 +6,0 @@ "main": "dist/index.js",

@@ -71,2 +71,12 @@ # `@tus/server`

#### `options.allowedHeaders`
Additional headers sent in `Access-Control-Allow-Headers` (`string[]`).
#### `options.generateUrl`
Control how the upload url is generated (`(req, { proto, host, baseUrl, path, id }) => string)`)
#### `options.getFileIdFromRequest`
Control how the Upload-ID is extracted from the request (`(req) => string | void`)
#### `options.namingFunction`

@@ -104,2 +114,7 @@

#### `options.onResponseError`
`onResponseError` will be invoked when an error response is about to be sent by the server.
you use this function to map custom errors to tus errors or for custom observability. (`(req, res, err) => Promise<{status_code: number; body: string} | void> | {status_code: number; body: string} | void`)
#### `server.handle(req, res)`

@@ -198,2 +213,3 @@

const server = new Server({
path: '/uploads',
datastore: new FileStore({directory: '/files'}),

@@ -200,0 +216,0 @@ })

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc