Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
"enabled": true
},
"files": {
"ignore": ["./**/dist/**/*"]
"ignore": [".git", "node_modules", "./**/dist/**/*"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noParameterAssign": "off"
},
"performance": {
"noDelete": "off"
}
}
},
Expand Down
21 changes: 19 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
"dependencies": {
"@tus/utils": "^0.5.1",
"debug": "^4.3.4",
"lodash.throttle": "^4.1.1"
"lodash.throttle": "^4.1.1",
"set-cookie-parser": "^2.7.1"
},
"devDependencies": {
"@types/debug": "^4.1.12",
"@types/lodash.throttle": "^4.1.9",
"@types/mocha": "^10.0.6",
"@types/node": "^22.10.1",
"@types/set-cookie-parser": "^2.4.10",
"@types/sinon": "^17.0.3",
"@types/supertest": "^2.0.16",
"mocha": "^11.0.1",
Expand Down
66 changes: 30 additions & 36 deletions packages/server/src/handlers/BaseHandler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import EventEmitter from 'node:events'
import stream from 'node:stream/promises'
import {PassThrough, Readable} from 'node:stream'
import type http from 'node:http'

import type {ServerOptions} from '../types'
import type {DataStore, CancellationContext} from '@tus/utils'
import {ERRORS, type Upload, StreamLimiter, EVENTS} from '@tus/utils'
import throttle from 'lodash.throttle'
import stream from 'node:stream/promises'
import {PassThrough, type Readable} from 'node:stream'

const reExtractFileID = /([^/]+)\/?$/
const reForwardedHost = /host="?([^";]+)/
Expand All @@ -26,23 +25,23 @@ export class BaseHandler extends EventEmitter {
this.options = options
}

write(res: http.ServerResponse, status: number, headers = {}, body = '') {
if (status !== 204) {
// @ts-expect-error not explicitly typed but possible
headers['Content-Length'] = Buffer.byteLength(body, 'utf8')
write(status: number, headers = {}, body?: string) {
const res = new Response(status === 204 ? null : body, {headers, status})
if (status !== 204 && body) {
res.headers.set('Content-Length', Buffer.byteLength(body, 'utf8').toString())
}

res.writeHead(status, headers)
res.write(body)
return res.end()
return res
}

generateUrl(req: http.IncomingMessage, id: string) {
generateUrl(req: Request, id: string) {
const path = this.options.path === '/' ? '' : this.options.path

if (this.options.generateUrl) {
// user-defined generateUrl function
const {proto, host} = this.extractHostAndProto(req)
const {proto, host} = BaseHandler.extractHostAndProto(
req.headers,
this.options.respectForwardedHeaders
)

return this.options.generateUrl(req, {
proto,
Expand All @@ -57,12 +56,15 @@ export class BaseHandler extends EventEmitter {
return `${path}/${id}`
}

const {proto, host} = this.extractHostAndProto(req)
const {proto, host} = BaseHandler.extractHostAndProto(
req.headers,
this.options.respectForwardedHeaders
)

return `${proto}://${host}${path}/${id}`
}

getFileIdFromRequest(req: http.IncomingMessage) {
getFileIdFromRequest(req: Request) {
const match = reExtractFileID.exec(req.url as string)

if (this.options.getFileIdFromRequest) {
Expand All @@ -77,19 +79,19 @@ export class BaseHandler extends EventEmitter {
return decodeURIComponent(match[1])
}

protected extractHostAndProto(req: http.IncomingMessage) {
static extractHostAndProto(headers: Headers, respectForwardedHeaders?: boolean) {
let proto: string | undefined
let host: string | undefined

if (this.options.respectForwardedHeaders) {
const forwarded = req.headers.forwarded as string | undefined
if (respectForwardedHeaders) {
const forwarded = headers.get('forwarded')
if (forwarded) {
host ??= reForwardedHost.exec(forwarded)?.[1]
proto ??= reForwardedProto.exec(forwarded)?.[1]
}

const forwardHost = req.headers['x-forwarded-host']
const forwardProto = req.headers['x-forwarded-proto']
const forwardHost = headers.get('x-forwarded-host')
const forwardProto = headers.get('x-forwarded-proto')

// @ts-expect-error we can pass undefined
if (['http', 'https'].includes(forwardProto)) {
Expand All @@ -99,24 +101,20 @@ export class BaseHandler extends EventEmitter {
host ??= forwardHost as string
}

host ??= req.headers.host
host ??= headers.get('host') as string
proto ??= 'http'

return {host: host as string, proto}
return {host, proto}
}

protected async getLocker(req: http.IncomingMessage) {
protected async getLocker(req: Request) {
if (typeof this.options.locker === 'function') {
return this.options.locker(req)
}
return this.options.locker
}

protected async acquireLock(
req: http.IncomingMessage,
id: string,
context: CancellationContext
) {
protected async acquireLock(req: Request, id: string, context: CancellationContext) {
const locker = await this.getLocker(req)

const lock = locker.newLock(id)
Expand Down Expand Up @@ -190,7 +188,7 @@ export class BaseHandler extends EventEmitter {
})
}

getConfiguredMaxSize(req: http.IncomingMessage, id: string | null) {
getConfiguredMaxSize(req: Request, id: string | null) {
if (typeof this.options.maxSize === 'function') {
return this.options.maxSize(req, id)
}
Expand All @@ -202,19 +200,15 @@ export class BaseHandler extends EventEmitter {
* This function considers both the server's configured maximum size and
* the specifics of the upload, such as whether the size is deferred or fixed.
*/
async calculateMaxBodySize(
req: http.IncomingMessage,
file: Upload,
configuredMaxSize?: number
) {
async calculateMaxBodySize(req: Request, file: Upload, configuredMaxSize?: number) {
// Use the server-configured maximum size if it's not explicitly provided.
configuredMaxSize ??= await this.getConfiguredMaxSize(req, file.id)

// Parse the Content-Length header from the request (default to 0 if not set).
const length = Number.parseInt(req.headers['content-length'] || '0', 10)
const length = Number.parseInt(req.headers.get('content-length') || '0', 10)
const offset = file.offset

const hasContentLengthSet = req.headers['content-length'] !== undefined
const hasContentLengthSet = req.headers.get('content-length') !== null
const hasConfiguredMaxSizeSet = configuredMaxSize > 0

if (file.sizeIsDeferred) {
Expand Down
12 changes: 3 additions & 9 deletions packages/server/src/handlers/DeleteHandler.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import {BaseHandler} from './BaseHandler'
import {ERRORS, EVENTS, type CancellationContext} from '@tus/utils'

import type http from 'node:http'

export class DeleteHandler extends BaseHandler {
async send(
req: http.IncomingMessage,
res: http.ServerResponse,
context: CancellationContext
) {
async send(req: Request, context: CancellationContext, headers = new Headers()) {
const id = this.getFileIdFromRequest(req)
if (!id) {
throw ERRORS.FILE_NOT_FOUND
}

if (this.options.onIncomingRequest) {
await this.options.onIncomingRequest(req, res, id)
await this.options.onIncomingRequest(req, id)
}

const lock = await this.acquireLock(req, id, context)
Expand All @@ -31,7 +25,7 @@ export class DeleteHandler extends BaseHandler {
} finally {
await lock.unlock()
}
const writtenRes = this.write(res, 204, {})
const writtenRes = this.write(204, headers)
this.emit(EVENTS.POST_TERMINATE, req, writtenRes, id)
return writtenRes
}
Expand Down
38 changes: 17 additions & 21 deletions packages/server/src/handlers/GetHandler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import stream from 'node:stream'

import {BaseHandler} from './BaseHandler'
import {ERRORS, type Upload} from '@tus/utils'
import {type CancellationContext, ERRORS, type Upload} from '@tus/utils'

import type http from 'node:http'
import type {RouteHandler} from '../types'

export class GetHandler extends BaseHandler {
Expand Down Expand Up @@ -61,13 +58,15 @@ export class GetHandler extends BaseHandler {
* Read data from the DataStore and send the stream.
*/
async send(
req: http.IncomingMessage,
res: http.ServerResponse
// biome-ignore lint/suspicious/noConfusingVoidType: it's fine
): Promise<stream.Writable | void> {
if (this.paths.has(req.url as string)) {
const handler = this.paths.get(req.url as string) as RouteHandler
return handler(req, res)
req: Request,
context: CancellationContext,
headers = new Headers()
): Promise<Response> {
const path = new URL(req.url).pathname
const handler = this.paths.get(path)

if (handler) {
return handler(req)
}

if (!('read' in this.store)) {
Expand All @@ -80,7 +79,7 @@ export class GetHandler extends BaseHandler {
}

if (this.options.onIncomingRequest) {
await this.options.onIncomingRequest(req, res, id)
await this.options.onIncomingRequest(req, id)
}

const stats = await this.store.getUpload(id)
Expand All @@ -91,17 +90,14 @@ export class GetHandler extends BaseHandler {

const {contentType, contentDisposition} = this.filterContentType(stats)

const lock = await this.acquireLock(req, id, context)
// @ts-expect-error exists if supported
const file_stream = await this.store.read(id)
const headers = {
'Content-Length': stats.offset,
'Content-Type': contentType,
'Content-Disposition': contentDisposition,
}
res.writeHead(200, headers)
return stream.pipeline(file_stream, res, () => {
// We have no need to handle streaming errors
})
await lock.unlock()
headers.set('Content-Length', stats.offset.toString())
headers.set('Content-Type', contentType)
headers.set('Content-Disposition', contentDisposition)
return new Response(file_stream, {headers, status: 200})
}

/**
Expand Down
Loading