Skip to content

Commit

Permalink
feat: Max file size
Browse files Browse the repository at this point in the history
  • Loading branch information
fenos committed Nov 13, 2023
1 parent 612ac24 commit a222d1d
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 3 deletions.
8 changes: 8 additions & 0 deletions packages/server/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export const ERRORS = {
status_code: 410,
body: 'The file for this url no longer exists\n',
},
ERR_SIZE_EXCEEDED: {
status_code: 413,
body: "upload's size exceeded\n",
},
ERR_MAX_SIZE_EXCEEDED: {
status_code: 413,
body: 'Maximum size exceeded\n',
},
INVALID_LENGTH: {
status_code: 400,
body: 'Upload-Length or Upload-Defer-Length header required\n',
Expand Down
46 changes: 46 additions & 0 deletions packages/server/src/handlers/BaseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import EventEmitter from 'node:events'
import type {ServerOptions} from '../types'
import type {DataStore} from '../models'
import type http from 'node:http'
import {Upload} from '../models'
import {ERRORS} from '../constants'

const reExtractFileID = /([^/]+)\/?$/
const reForwardedHost = /host="?([^";]+)/
Expand Down Expand Up @@ -78,4 +80,48 @@ export class BaseHandler extends EventEmitter {

return decodeURIComponent(match[1])
}

getConfiguredMaxSize(req: http.IncomingMessage, id: string) {
if (typeof this.options.maxSize === 'function') {
return this.options.maxSize(req, id)
}
return this.options.maxSize ?? 0
}

async getBodyMaxSize(
req: http.IncomingMessage,
info: Upload,
configuredMaxSize?: number
) {
configuredMaxSize =
configuredMaxSize ?? (await this.getConfiguredMaxSize(req, info.id))

const length = parseInt(req.headers['content-length'] || '0', 10)
const offset = info.offset

// Test if this upload fits into the file's size
if (!info.sizeIsDeferred && offset + length > (info.size || 0)) {
throw ERRORS.ERR_SIZE_EXCEEDED
}

let maxSize = (info.size || 0) - offset
// If the upload's length is deferred and the PATCH request does not contain the Content-Length
// header (which is allowed if 'Transfer-Encoding: chunked' is used), we still need to set limits for
// the body size.
if (info.sizeIsDeferred) {
if (configuredMaxSize > 0) {
// Ensure that the upload does not exceed the maximum upload size
maxSize = configuredMaxSize - offset
} else {
// If no upload limit is given, we allow arbitrary sizes
maxSize = Number.MAX_SAFE_INTEGER
}
}

if (length > 0) {
maxSize = length
}

return maxSize
}
}
8 changes: 7 additions & 1 deletion packages/server/src/handlers/OptionsHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import type http from 'node:http'
// A successful response indicated by the 204 No Content status MUST contain
// the Tus-Version header. It MAY include the Tus-Extension and Tus-Max-Size headers.
export class OptionsHandler extends BaseHandler {
async send(_: http.IncomingMessage, res: http.ServerResponse) {
async send(req: http.IncomingMessage, res: http.ServerResponse) {
const maxSize = await this.getConfiguredMaxSize(req, '')

if (maxSize > 0) {
res.setHeader('Tus-Max-Size', maxSize)
}

res.setHeader('Access-Control-Allow-Methods', ALLOWED_METHODS)
res.setHeader('Access-Control-Allow-Headers', ALLOWED_HEADERS)
res.setHeader('Access-Control-Max-Age', MAX_AGE)
Expand Down
13 changes: 12 additions & 1 deletion packages/server/src/handlers/PatchHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {BaseHandler} from './BaseHandler'
import {ERRORS, EVENTS} from '../constants'

import type http from 'node:http'
import stream from 'node:stream'
import {StreamLimiter} from '../models/StreamLimiter'

const log = debug('tus-node-server:handlers:patch')

Expand Down Expand Up @@ -55,6 +57,8 @@ export class PatchHandler extends BaseHandler {
throw ERRORS.INVALID_OFFSET
}

const maxFileSize = await this.getConfiguredMaxSize(req, id)

// The request MUST validate upload-length related headers
const upload_length = req.headers['upload-length'] as string | undefined
if (upload_length !== undefined) {
Expand All @@ -73,11 +77,18 @@ export class PatchHandler extends BaseHandler {
throw ERRORS.INVALID_LENGTH
}

if (size > maxFileSize) {
throw ERRORS.ERR_MAX_SIZE_EXCEEDED
}

await this.store.declareUploadLength(id, size)
upload.size = size
}

const newOffset = await this.store.write(req, id, offset)
const bodyMaxSize = await this.getBodyMaxSize(req, upload, maxFileSize)
const reqBody = stream.pipeline(req, new StreamLimiter(bodyMaxSize), () => {})

const newOffset = await this.store.write(reqBody, id, offset)
upload.offset = newOffset
this.emit(EVENTS.POST_RECEIVE, req, res, upload)
if (newOffset === upload.size && this.options.onUploadFinish) {
Expand Down
13 changes: 12 additions & 1 deletion packages/server/src/handlers/PostHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {EVENTS, ERRORS} from '../constants'
import type http from 'node:http'
import type {ServerOptions} from '../types'
import type {DataStore} from '../models'
import stream from 'node:stream'
import {StreamLimiter} from '../models/StreamLimiter'

const log = debug('tus-node-server:handlers:post')

Expand Down Expand Up @@ -39,6 +41,7 @@ export class PostHandler extends BaseHandler {
const upload_length = req.headers['upload-length'] as string | undefined
const upload_defer_length = req.headers['upload-defer-length'] as string | undefined
const upload_metadata = req.headers['upload-metadata'] as string | undefined
const is_defered = upload_length === undefined

Check failure on line 44 in packages/server/src/handlers/PostHandler.ts

View workflow job for this annotation

GitHub Actions / lts/hydrogen

'is_defered' is assigned a value but never used

if (
upload_defer_length !== undefined && // Throw error if extension is not supported
Expand Down Expand Up @@ -75,6 +78,11 @@ export class PostHandler extends BaseHandler {
metadata,
})

const maxFileSize = await this.getConfiguredMaxSize(req, id)
if (maxFileSize > 0 && (upload.size || 0) > maxFileSize) {
throw ERRORS.ERR_MAX_SIZE_EXCEEDED
}

if (this.options.onUploadCreate) {
try {
res = await this.options.onUploadCreate(req, res, upload)
Expand All @@ -98,7 +106,10 @@ export class PostHandler extends BaseHandler {

// The request MIGHT include a Content-Type header when using creation-with-upload extension
if (validateHeader('content-type', req.headers['content-type'])) {
newOffset = await this.store.write(req, upload.id, 0)
const bodyMaxSize = await this.getBodyMaxSize(req, upload, maxFileSize)
const reqBody = stream.pipeline(req, new StreamLimiter(bodyMaxSize), () => {})

newOffset = await this.store.write(reqBody, upload.id, 0)
headers['Upload-Offset'] = newOffset.toString()
isFinal = newOffset === Number.parseInt(upload_length as string, 10)
upload.offset = newOffset
Expand Down
22 changes: 22 additions & 0 deletions packages/server/src/models/StreamLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Transform, TransformCallback} from 'stream'

export class StreamLimiter extends Transform {
private maxSize: number
private currentSize = 0

constructor(maxSize: number) {
super()
this.maxSize = maxSize
}

_transform(chunk: Buffer, encoding: BufferEncoding, callback: TransformCallback): void {
this.currentSize += chunk.length
if (this.currentSize > this.maxSize) {
this.push(null) // End the stream
callback()
} else {
this.push(chunk)
callback()
}
}
}
4 changes: 4 additions & 0 deletions packages/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import type {Upload} from './models'
export type ServerOptions = {
// The route to accept requests.
path: string
// Number of
maxSize?:
| number
| ((req: http.IncomingMessage, uploadId: string) => Promise<number> | number)
// Return a relative URL as the `Location` header.
relativeLocation?: boolean
// Allow `Forwarded`, `X-Forwarded-Proto`, and `X-Forwarded-Host` headers
Expand Down

0 comments on commit a222d1d

Please sign in to comment.