Skip to content

Commit

Permalink
@tus/server: allow metadata changes in onUploadCreate (#599)
Browse files Browse the repository at this point in the history
* @tus/server: allow metadata changes in onUploadCreate

* Do not use instanceof :(

* Add changeset

* Add example in comment
  • Loading branch information
Murderlon authored Apr 16, 2024
1 parent b16e71b commit 7f0c368
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-actors-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tus/server': minor
---

Allow onUploadCreate hook to override metadata
17 changes: 10 additions & 7 deletions packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,12 @@ finished uploads. (`boolean`)
#### `options.onUploadCreate`

`onUploadCreate` will be invoked before a new upload is created.
(`(req, res, upload) => Promise<res>`).
(`(req, res, upload) => Promise<{ res: http.ServerResponse, metadata?: Record<string, string>}>`).

If the function returns the (modified) response, the upload will be created. You can
`throw` an Object and the HTTP request will be aborted with the provided `body` and
`status_code` (or their fallbacks).
- If the function returns the (modified) response the upload will be created.
- You can optionally return `metadata` which will override (not merge!) `upload.metadata`.
- You can `throw` an Object and the HTTP request will be aborted with the provided `body`
and `status_code` (or their fallbacks).

This can be used to implement validation of upload metadata or add headers.

Expand Down Expand Up @@ -445,13 +446,15 @@ const {Server} = require('@tus/server')
const server = new Server({
// ..
async onUploadCreate(req, res, upload) {
const {ok, expected, received} = validateMetadata(upload)
const {ok, expected, received} = validateMetadata(upload) // your logic
if (!ok) {
const body = `Expected "${expected}" in "Upload-Metadata" but received "${received}"`
throw {status_code: 500, body} // if undefined, falls back to 500 with "Internal server error".
}
// We have to return the (modified) response.
return res
// You can optionally return metadata to override the upload metadata,
// such as `{ storagePath: "/upload/123abc..." }`
const extraMeta = getExtraMetadata(req) // your logic
return {res, metadata: {...upload.metadata, ...extraMeta}}
},
})
```
Expand Down
20 changes: 18 additions & 2 deletions packages/server/src/handlers/PostHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@tus/utils'
import {validateHeader} from '../validators/HeaderValidator'

import type http from 'node:http'
import http from 'node:http'
import type {ServerOptions, WithRequired} from '../types'

const log = debug('tus-node-server:handlers:post')
Expand Down Expand Up @@ -100,7 +100,23 @@ export class PostHandler extends BaseHandler {

if (this.options.onUploadCreate) {
try {
res = await this.options.onUploadCreate(req, res, upload)
const resOrObject = await this.options.onUploadCreate(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 as http.ServerResponse).write === 'function' &&
typeof (resOrObject as http.ServerResponse).writeHead === 'function'
) {
res = resOrObject as http.ServerResponse
} else {
// Ugly types because TS only understands instanceof
type ExcludeServerResponse<T> = T extends http.ServerResponse ? never : T
const obj = resOrObject as ExcludeServerResponse<typeof resOrObject>
res = obj.res
if (obj.metadata) {
upload.metadata = obj.metadata
}
}
} catch (error) {
log(`onUploadCreate error: ${error.body}`)
throw error
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export type ServerOptions = {
req: http.IncomingMessage,
res: http.ServerResponse,
upload: Upload
) => Promise<http.ServerResponse>
) => Promise<
// TODO: change in the next major
http.ServerResponse | {res: http.ServerResponse; metadata?: Upload['metadata']}
>

/**
* `onUploadFinish` will be invoked after an upload is completed but before a response is returned to the client.
Expand Down
31 changes: 30 additions & 1 deletion packages/server/test/Server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import request from 'supertest'

import {Server} from '../src'
import {FileStore} from '@tus/file-store'
import {TUS_RESUMABLE, EVENTS, DataStore} from '@tus/utils'
import {TUS_RESUMABLE, EVENTS, DataStore, Metadata} from '@tus/utils'
import httpMocks from 'node-mocks-http'
import sinon from 'sinon'

Expand Down Expand Up @@ -423,6 +423,35 @@ describe('Server', () => {
.expect(500, 'no', done)
})

it('should allow metadata to be changed in onUploadCreate', (done) => {
const filename = 'foo.txt'
const server = new Server({
path: '/test/output',
datastore: new FileStore({directory}),
async onUploadCreate(_, res, upload) {
const metadata = {...upload.metadata, filename}
return {res, metadata}
},
})
const s = server.listen()
request(s)
.post(server.options.path)
.set('Tus-Resumable', TUS_RESUMABLE)
.set('Upload-Length', '4')
.expect(201)
.then((res) => {
request(s)
.head(removeProtocol(res.headers.location))
.set('Tus-Resumable', TUS_RESUMABLE)
.expect(200)
.then((r) => {
const metadata = Metadata.parse(r.headers['upload-metadata'])
assert.equal(metadata.filename, filename)
done()
})
})
})

it('should call onUploadFinish and return its error to the client', (done) => {
const server = new Server({
path: '/test/output',
Expand Down

0 comments on commit 7f0c368

Please sign in to comment.