Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@tus/server: allow metadata changes in onUploadCreate #599

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all 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: 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
Loading