Skip to content

Commit beb2836

Browse files
committed
fix: tus-metadata
1 parent 830ea71 commit beb2836

File tree

11 files changed

+1327
-1282
lines changed

11 files changed

+1327
-1282
lines changed

package-lock.json

Lines changed: 1217 additions & 1158 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
"@fastify/swagger": "^8.3.1",
3535
"@fastify/swagger-ui": "^1.7.0",
3636
"@isaacs/ttlcache": "^1.4.1",
37-
"@tus/file-store": "1.1.0",
38-
"@tus/s3-store": "1.2.0",
39-
"@tus/server": "1.2.0",
37+
"@tus/file-store": "1.2.0",
38+
"@tus/s3-store": "1.3.0",
39+
"@tus/server": "1.3.0",
4040
"agentkeepalive": "^4.2.1",
4141
"async-retry": "^1.3.3",
4242
"axios": "^1.6.3",

src/http/routes/tus/handlers.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/http/routes/tus/index.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
2-
import { Server } from '@tus/server'
2+
import * as http from 'http'
3+
import { Server, ServerOptions, DataStore } from '@tus/server'
34
import { jwt, storage, db, dbSuperUser } from '../../plugins'
45
import { getConfig } from '../../../config'
5-
import * as http from 'http'
6+
import { getFileSizeLimit } from '../../../storage/limits'
67
import { Storage } from '../../../storage'
8+
import {
9+
FileStore,
10+
LockNotifier,
11+
PgLocker,
12+
S3Store,
13+
UploadId,
14+
AlsMemoryKV,
15+
} from '../../../storage/tus'
716
import {
817
namingFunction,
918
onCreate,
@@ -13,15 +22,8 @@ import {
1322
generateUrl,
1423
getFileIdFromRequest,
1524
} from './lifecycle'
16-
import { ServerOptions, DataStore } from '@tus/server'
17-
import { getFileSizeLimit } from '../../../storage/limits'
18-
import { UploadId } from './upload-id'
19-
import { FileStore } from './file-store'
2025
import { TenantConnection } from '../../../database/connection'
21-
import { PgLocker, LockNotifier } from './postgres-locker'
2226
import { PubSub } from '../../../database/pubsub'
23-
import { S3Store } from './s3-store'
24-
import { DeleteHandler } from './handlers'
2527

2628
const {
2729
storageS3Bucket,
@@ -49,6 +51,7 @@ function createTusStore() {
4951
return new S3Store({
5052
partSize: 6 * 1024 * 1024, // Each uploaded part will have ~6MB,
5153
expirationPeriodInMilliseconds: tusUrlExpiryMs,
54+
cache: new AlsMemoryKV(),
5255
s3ClientConfig: {
5356
bucket: storageS3Bucket,
5457
region: storageS3Region,
@@ -70,6 +73,7 @@ function createTusServer(lockNotifier: LockNotifier) {
7073
} = {
7174
path: tusPath,
7275
datastore: datastore,
76+
disableTerminationForFinishedUploads: true,
7377
locker: (rawReq: http.IncomingMessage) => {
7478
const req = rawReq as MultiPartRequest
7579
return new PgLocker(req.upload.storage.db, lockNotifier)
@@ -106,9 +110,7 @@ function createTusServer(lockNotifier: LockNotifier) {
106110
return fileSizeLimit
107111
},
108112
}
109-
const server = new Server(serverOptions)
110-
server.handlers.DELETE = new DeleteHandler(datastore, serverOptions)
111-
return server
113+
return new Server(serverOptions)
112114
}
113115

114116
export default async function routes(fastify: FastifyInstance) {
@@ -126,6 +128,12 @@ export default async function routes(fastify: FastifyInstance) {
126128
fastify.register(db)
127129
fastify.register(storage)
128130

131+
fastify.addHook('onRequest', (req, res, done) => {
132+
AlsMemoryKV.localStorage.run(new Map(), () => {
133+
done()
134+
})
135+
})
136+
129137
fastify.addHook('preHandler', async (req) => {
130138
;(req.raw as MultiPartRequest).log = req.log
131139
;(req.raw as MultiPartRequest).upload = {

src/http/routes/tus/lifecycle.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import http from 'http'
2-
import { isRenderableError, Storage } from '../../../storage'
3-
import { Metadata, Upload } from '@tus/server'
4-
import { getConfig } from '../../../config'
2+
import { BaseLogger } from 'pino'
3+
import { Upload } from '@tus/server'
54
import { randomUUID } from 'crypto'
6-
import { UploadId } from './upload-id'
5+
import { isRenderableError, Storage, StorageBackendError } from '../../../storage'
6+
import { getConfig } from '../../../config'
77
import { Uploader } from '../../../storage/uploader'
88
import { TenantConnection } from '../../../database/connection'
9-
import { BaseLogger } from 'pino'
9+
import { UploadId } from '../../../storage/tus'
1010

1111
const { storageS3Bucket, tusPath } = getConfig()
1212
const reExtractFileID = /([^/]+)\/?$/
@@ -22,6 +22,9 @@ export type MultiPartRequest = http.IncomingMessage & {
2222
}
2323
}
2424

25+
/**
26+
* Runs on every TUS incoming request
27+
*/
2528
export async function onIncomingRequest(
2629
rawReq: http.IncomingMessage,
2730
res: http.ServerResponse,
@@ -51,22 +54,24 @@ export async function onIncomingRequest(
5154
})
5255
}
5356

57+
/**
58+
* Generate URL for TUS upload, it encodes the uploadID to base64url
59+
*/
5460
export function generateUrl(
5561
_: http.IncomingMessage,
56-
{
57-
proto,
58-
host,
59-
baseUrl,
60-
path,
61-
id,
62-
}: { proto: string; host: string; baseUrl: string; path: string; id: string }
62+
{ proto, host, path, id }: { proto: string; host: string; path: string; id: string }
6363
) {
6464
proto = process.env.NODE_ENV === 'production' ? 'https' : proto
65+
66+
// remove the tenant-id from the url, since we'll be using the tenant-id from the request
6567
id = id.split('/').slice(1).join('/')
6668
id = Buffer.from(id, 'utf-8').toString('base64url')
6769
return `${proto}://${host}${path}/${id}`
6870
}
6971

72+
/**
73+
* Extract the uploadId from the request and decodes it from base64url
74+
*/
7075
export function getFileIdFromRequest(rawRwq: http.IncomingMessage) {
7176
const req = rawRwq as MultiPartRequest
7277
const match = reExtractFileID.exec(req.url as string)
@@ -79,35 +84,43 @@ export function getFileIdFromRequest(rawRwq: http.IncomingMessage) {
7984
return req.upload.tenantId + '/' + idMatch
8085
}
8186

82-
export function namingFunction(rawReq: http.IncomingMessage) {
87+
/**
88+
* Generate the uploadId for the TUS upload
89+
* the URL structure is as follows:
90+
*
91+
* /tenant-id/bucket-name/object-name/version
92+
*/
93+
export function namingFunction(
94+
rawReq: http.IncomingMessage,
95+
metadata: Record<string, string | null>
96+
) {
8397
const req = rawReq as MultiPartRequest
8498

8599
if (!req.url) {
86100
throw new Error('no url set')
87101
}
88102

89-
const metadataHeader = req.headers['upload-metadata']
90-
91-
if (typeof metadataHeader !== 'string') {
92-
throw new Error('no metadata')
103+
if (!metadata) {
104+
throw new StorageBackendError('metadata_header_invalid', 400, 'metadata header invalid')
93105
}
94106

95107
try {
96-
const params = Metadata.parse(metadataHeader)
97-
98108
const version = randomUUID()
99109

100110
return new UploadId({
101111
tenant: req.upload.tenantId,
102-
bucket: params.bucketName || '',
103-
objectName: params.objectName || '',
112+
bucket: metadata.bucketName || '',
113+
objectName: metadata.objectName || '',
104114
version,
105115
}).toString()
106116
} catch (e) {
107117
throw e
108118
}
109119
}
110120

121+
/**
122+
* Runs before the upload URL is created
123+
*/
111124
export async function onCreate(
112125
rawReq: http.IncomingMessage,
113126
res: http.ServerResponse,
@@ -137,6 +150,9 @@ export async function onCreate(
137150
return res
138151
}
139152

153+
/**
154+
* Runs when the upload to the underline store is completed
155+
*/
140156
export async function onUploadFinish(
141157
rawReq: http.IncomingMessage,
142158
res: http.ServerResponse,
@@ -178,11 +194,15 @@ export async function onUploadFinish(
178194

179195
type TusError = { status_code: number; body: string }
180196

197+
/**
198+
* Runs when there is an error on the TUS upload
199+
*/
181200
export function onResponseError(
182201
req: http.IncomingMessage,
183202
res: http.ServerResponse,
184203
e: TusError | Error
185204
) {
205+
console.log(e)
186206
if (e instanceof Error) {
187207
;(res as any).executionError = e
188208
}

src/storage/tus/als-memory-kv.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { AsyncLocalStorage } from 'async_hooks'
2+
import { KvStore } from '@tus/server'
3+
import { MetadataValue } from '@tus/s3-store'
4+
5+
export class AlsMemoryKV implements KvStore<MetadataValue> {
6+
static localStorage = new AsyncLocalStorage<Map<string, MetadataValue>>()
7+
8+
async delete(value: string): Promise<void> {
9+
AlsMemoryKV.localStorage.getStore()?.delete(value)
10+
}
11+
12+
async get(value: string): Promise<MetadataValue | undefined> {
13+
return AlsMemoryKV.localStorage.getStore()?.get(value)
14+
}
15+
16+
async set(key: string, value: MetadataValue): Promise<void> {
17+
AlsMemoryKV.localStorage.getStore()?.set(key, value)
18+
}
19+
}

src/http/routes/tus/file-store.ts renamed to src/storage/tus/file-store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { FileStore as TusFileStore } from '@tus/file-store'
22
import { Upload } from '@tus/server'
33
import fsExtra from 'fs-extra'
44
import path from 'path'
5-
import { FileBackend } from '../../../storage/backend'
65
import { Configstore } from '@tus/file-store'
6+
import { FileBackend } from '../backend'
77

88
type FileStoreOptions = {
99
directory: string

src/storage/tus/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './file-store'
2+
export * from './s3-store'
3+
export * from './postgres-locker'
4+
export * from './upload-id'
5+
export * from './als-memory-kv'

src/http/routes/tus/postgres-locker.ts renamed to src/storage/tus/postgres-locker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Lock, Locker, RequestRelease } from '@tus/server'
2-
import { Database, DBError } from '../../../storage/database'
3-
import { PubSubAdapter } from '../../../pubsub'
4-
import { UploadId } from './upload-id'
52
import { clearTimeout } from 'timers'
63
import EventEmitter from 'events'
4+
import { Database, DBError } from '../database'
5+
import { PubSubAdapter } from '../../pubsub'
6+
import { UploadId } from './upload-id'
77

88
const REQUEST_LOCK_RELEASE_MESSAGE = 'REQUEST_LOCK_RELEASE'
99

0 commit comments

Comments
 (0)