Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/@uppy/tus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ uppy.use(Tus, {
endpoint: 'https://tusd.tusdemo.net/files/', // use your tus endpoint here
resume: true,
retryDelays: [0, 1000, 3000, 5000],
fingerprintExtra: [
'key1',
'key2'
]
})
```

Expand Down
11 changes: 10 additions & 1 deletion packages/@uppy/tus/src/getFingerprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,22 @@ function isReactNative() {
// fingerprint handling take charge.
export default function getFingerprint<M extends Meta, B extends Body>(
uppyFile: UppyFile<M, B>,
extraKeys?: string[]
): tus.UploadOptions['fingerprint'] {
return (file, options) => {
if (isCordova() || isReactNative()) {
return tus.defaultOptions.fingerprint(file, options)
}

const uppyFingerprint = ['tus', uppyFile.id, options.endpoint].join('-')
let uppyFingerprintParts = ['tus', uppyFile.id, options.endpoint]

if (extraKeys && Array.isArray(extraKeys)) {
const extras = extraKeys.map((key) => String(uppyFile.meta?.[key] ?? '')).join(':')
uppyFingerprintParts.push(extras)
}

const uppyFingerprint = uppyFingerprintParts.join('-')


return Promise.resolve(uppyFingerprint)
}
Expand Down
104 changes: 102 additions & 2 deletions packages/@uppy/tus/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Core from '@uppy/core'
import { describe, expect, expectTypeOf, it } from 'vitest'
import Tus, { type TusBody } from './index.js'
import {describe, expect, expectTypeOf, it} from 'vitest'
import Tus, {type TusBody} from './index.js'
import getFingerprint from './getFingerprint.js'

describe('Tus', () => {
it('Throws errors if autoRetry option is true', () => {
Expand Down Expand Up @@ -45,3 +46,102 @@ describe('Tus', () => {
>()
})
})

describe('getFingerprint', () => {
it('includes extra metadata in the fingerprint', async () => {
const file = {
id: 'file1',
meta: {
customField: 'value1',
},
}
const extraKeys = ['customField']
const fingerprintFn = getFingerprint(file, extraKeys)
const options = { endpoint: 'http://example.com/upload' }
const fingerprint = await fingerprintFn({}, options)
expect(fingerprint).toBe('tus-file1-http://example.com/upload-value1')
})

it('does not include extra metadata when extraKeys is not provided', async () => {
const file = {
id: 'file2',
meta: {
customField: 'value1',
},
}
const fingerprintFn = getFingerprint(file)
const options = { endpoint: 'http://example.com/upload' }
const fingerprint = await fingerprintFn({}, options)
expect(fingerprint).toBe('tus-file2-http://example.com/upload')
})

it('handles multiple extra keys', async () => {
const file = {
id: 'file3',
meta: {
customField: 'value1',
anotherField: 'value2',
},
}
const extraKeys = ['customField', 'anotherField']
const fingerprintFn = getFingerprint(file, extraKeys)
const options = { endpoint: 'http://example.com/upload' }
const fingerprint = await fingerprintFn({}, options)
expect(fingerprint).toBe('tus-file3-http://example.com/upload-value1:value2')
})

it('handles missing metadata keys', async () => {
const file = {
id: 'file4',
meta: {},
}
const extraKeys = ['customField']
const fingerprintFn = getFingerprint(file, extraKeys)
const options = { endpoint: 'http://example.com/upload' }
const fingerprint = await fingerprintFn({}, options)
expect(fingerprint).toBe('tus-file4-http://example.com/upload-')
})

it('handles multiple extra keys with some missing', async () => {
const file = {
id: 'file5',
meta: {
customField: 'value1',
},
}
const extraKeys = ['customField', 'missingField']
const fingerprintFn = getFingerprint(file, extraKeys)
const options = { endpoint: 'http://example.com/upload' }
const fingerprint = await fingerprintFn({}, options)
expect(fingerprint).toBe('tus-file5-http://example.com/upload-value1:')
})

it('converts metadata values to strings', async () => {
const file = {
id: 'file6',
meta: {
numberField: 123,
objectField: { key: 'value' },
},
}
const extraKeys = ['numberField', 'objectField']
const fingerprintFn = getFingerprint(file, extraKeys)
const options = { endpoint: 'http://example.com/upload' }
const fingerprint = await fingerprintFn({}, options)
expect(fingerprint).toBe('tus-file6-http://example.com/upload-123:[object Object]')
})

it('handles special characters in metadata', async () => {
const file = {
id: 'file7',
meta: {
customField: 'value:with:colon',
},
}
const extraKeys = ['customField']
const fingerprintFn = getFingerprint(file, extraKeys)
const options = { endpoint: 'http://example.com/upload' }
const fingerprint = await fingerprintFn({}, options)
expect(fingerprint).toBe('tus-file7-http://example.com/upload-value:with:colon')
})
})
5 changes: 4 additions & 1 deletion packages/@uppy/tus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface TusOpts<M extends Meta, B extends Body>
withCredentials?: boolean
allowedMetaFields?: boolean | string[]
rateLimitedQueue?: RateLimitedQueue
fingerprintExtra?: string[]
}
export type { TusOpts as TusOptions }

Expand Down Expand Up @@ -237,7 +238,9 @@ export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
// now also includes `relativePath` for files added from folders.
// This means you can add 2 identical files, if one is in folder a,
// the other in folder b.
uploadOptions.fingerprint = getFingerprint(file)

// Allow adding extra data to the fingerprint, for users to manually regenerate fingerprint
uploadOptions.fingerprint = getFingerprint(file, opts.fingerprintExtra)

uploadOptions.onBeforeRequest = async (req) => {
const xhr = req.getUnderlyingObject()
Expand Down