@tus/server
Advanced tools
Comparing version 1.2.0 to 1.3.0
@@ -16,2 +16,6 @@ export declare const REQUEST_METHODS: readonly ["POST", "HEAD", "PATCH", "OPTIONS", "DELETE"]; | ||
}; | ||
readonly INVALID_TERMINATION: { | ||
readonly status_code: 400; | ||
readonly body: "Cannot terminate an already completed upload"; | ||
}; | ||
readonly ERR_LOCK_TIMEOUT: { | ||
@@ -18,0 +22,0 @@ readonly status_code: 500; |
@@ -39,2 +39,6 @@ "use strict"; | ||
}, | ||
INVALID_TERMINATION: { | ||
status_code: 400, | ||
body: 'Cannot terminate an already completed upload', | ||
}, | ||
ERR_LOCK_TIMEOUT: { | ||
@@ -41,0 +45,0 @@ status_code: 500, |
@@ -34,4 +34,2 @@ "use strict"; | ||
generateUrl(req, id) { | ||
// @ts-expect-error req.baseUrl does exist | ||
const baseUrl = req.baseUrl ?? ''; | ||
const path = this.options.path === '/' ? '' : this.options.path; | ||
@@ -44,4 +42,2 @@ if (this.options.generateUrl) { | ||
host, | ||
// @ts-expect-error we can pass undefined | ||
baseUrl: req.baseUrl, | ||
path: path, | ||
@@ -53,6 +49,6 @@ id, | ||
if (this.options.relativeLocation) { | ||
return `${baseUrl}${path}/${id}`; | ||
return `${path}/${id}`; | ||
} | ||
const { proto, host } = this.extractHostAndProto(req); | ||
return `${proto}://${host}${baseUrl}${path}/${id}`; | ||
return `${proto}://${host}${path}/${id}`; | ||
} | ||
@@ -59,0 +55,0 @@ getFileIdFromRequest(req) { |
@@ -17,2 +17,8 @@ "use strict"; | ||
try { | ||
if (this.options.disableTerminationForFinishedUploads) { | ||
const upload = await this.store.getUpload(id); | ||
if (upload.offset === upload.size) { | ||
throw constants_1.ERRORS.INVALID_TERMINATION; | ||
} | ||
} | ||
await this.store.remove(id); | ||
@@ -19,0 +25,0 @@ } |
@@ -40,9 +40,18 @@ "use strict"; | ||
} | ||
let metadata; | ||
if ('upload-metadata' in req.headers) { | ||
try { | ||
metadata = models_1.Metadata.parse(upload_metadata); | ||
} | ||
catch { | ||
throw constants_1.ERRORS.INVALID_METADATA; | ||
} | ||
} | ||
let id; | ||
try { | ||
id = this.options.namingFunction(req); | ||
id = this.options.namingFunction(req, metadata); | ||
} | ||
catch (error) { | ||
log('create: check your `namingFunction`. Error', error); | ||
throw constants_1.ERRORS.FILE_WRITE_ERROR; | ||
throw error; | ||
} | ||
@@ -55,11 +64,2 @@ const maxFileSize = await this.getConfiguredMaxSize(req, id); | ||
} | ||
let metadata; | ||
if ('upload-metadata' in req.headers) { | ||
try { | ||
metadata = models_1.Metadata.parse(upload_metadata); | ||
} | ||
catch { | ||
throw constants_1.ERRORS.INVALID_METADATA; | ||
} | ||
} | ||
if (this.options.onIncomingRequest) { | ||
@@ -66,0 +66,0 @@ await this.options.onIncomingRequest(req, res, id); |
@@ -6,1 +6,2 @@ export { Server } from './server'; | ||
export * from './constants'; | ||
export * from './kvstores'; |
@@ -24,1 +24,2 @@ "use strict"; | ||
__exportStar(require("./constants"), exports); | ||
__exportStar(require("./kvstores"), exports); |
@@ -37,3 +37,2 @@ /// <reference types="node" /> | ||
host: string; | ||
baseUrl: string; | ||
path: string; | ||
@@ -54,3 +53,3 @@ id: string; | ||
*/ | ||
namingFunction?: (req: http.IncomingMessage) => string; | ||
namingFunction?: (req: http.IncomingMessage, metadata?: Record<string, string | null>) => string; | ||
/** | ||
@@ -62,2 +61,6 @@ * The Lock interface defines methods for implementing a locking mechanism. | ||
/** | ||
* Disallow termination for finished uploads. | ||
*/ | ||
disableTerminationForFinishedUploads?: boolean; | ||
/** | ||
* `onUploadCreate` will be invoked before a new upload is created. | ||
@@ -64,0 +67,0 @@ * If the function returns the (modified) response, the upload will be created. |
{ | ||
"$schema": "https://json.schemastore.org/package.json", | ||
"name": "@tus/server", | ||
"version": "1.2.0", | ||
"version": "1.3.0", | ||
"description": "Tus resumable upload protocol in Node.js", | ||
@@ -27,18 +27,21 @@ "main": "dist/index.js", | ||
"devDependencies": { | ||
"@types/debug": "^4.1.8", | ||
"@types/mocha": "^10.0.1", | ||
"@types/node": "^20.10.4", | ||
"@types/sinon": "^10.0.16", | ||
"@types/supertest": "^2.0.12", | ||
"eslint": "^8.48.0", | ||
"@types/debug": "^4.1.12", | ||
"@types/mocha": "^10.0.6", | ||
"@types/node": "^20.11.5", | ||
"@types/sinon": "^10.0.20", | ||
"@types/supertest": "^2.0.16", | ||
"eslint": "^8.56.0", | ||
"eslint-config-custom": "0.0.0", | ||
"mocha": "^10.2.0", | ||
"node-mocks-http": "^1.13.0", | ||
"node-mocks-http": "^1.14.1", | ||
"should": "^13.2.3", | ||
"sinon": "^15.2.0", | ||
"supertest": "^6.3.3", | ||
"ts-node": "^10.9.1", | ||
"supertest": "^6.3.4", | ||
"ts-node": "^10.9.2", | ||
"tsconfig": "*", | ||
"typescript": "^5.2.2" | ||
"typescript": "^5.3.3" | ||
}, | ||
"optionalDependencies": { | ||
"@redis/client": "^1.5.13" | ||
}, | ||
"engines": { | ||
@@ -45,0 +48,0 @@ "node": ">=16" |
138
README.md
@@ -20,2 +20,3 @@ # `@tus/server` | ||
- [Example: validate metadata when an upload is created](#example-validate-metadata-when-an-upload-is-created) | ||
- [Example: store files in custom nested directories](#example-store-files-in-custom-nested-directories) | ||
- [Types](#types) | ||
@@ -53,4 +54,4 @@ - [Compatibility](#compatibility) | ||
This package exports `Server` and all [`constants`][], [`types`][], and [`models`][]. There is no default export. | ||
You should only need the `Server` and `EVENTS` exports. | ||
This package exports `Server` and all [`constants`][], [`types`][], [`models`][], and [`kvstores`][]. There is no default export. | ||
You should only need the `Server`, `EVENTS`, and KV store exports. | ||
@@ -83,14 +84,43 @@ ### `new Server(options)` | ||
#### `options.generateUrl` | ||
Control how the upload url is generated (`(req, { proto, host, baseUrl, path, id }) => string)`) | ||
Control how the upload URL is generated (`(req, { proto, host, path, id }) => string)`) | ||
This only changes the upload URL (`Location` header). | ||
If you also want to change the file name in storage use `namingFunction`. | ||
Returning `prefix-1234` in `namingFunction` means the `id` argument in `generateUrl` is `prefix-1234`. | ||
`@tus/server` expects everything in the path after the last `/` to be the upload id. | ||
If you change that you have to use `getFileIdFromRequest` as well. | ||
A common use case of this function and `getFileIdFromRequest` is to base65 encode a complex id into the URL. | ||
> [!TIP] | ||
> Checkout the example how to [store files in custom nested directories](#example-store-files-in-custom-nested-directories). | ||
#### `options.getFileIdFromRequest` | ||
Control how the Upload-ID is extracted from the request (`(req) => string | void`) | ||
By default, it expects everything in the path after the last `/` to be the upload id. | ||
> [!TIP] | ||
> Checkout the example how to [store files in custom nested directories](#example-store-files-in-custom-nested-directories). | ||
#### `options.namingFunction` | ||
Control how you want to name files (`(req) => string`) | ||
Control how you want to name files (`(req, metadata) => string`) | ||
In `@tus/server`, the upload ID in the URL is the same as the file name. | ||
This means using a custom `namingFunction` will return a different `Location` header for uploading | ||
and result in a different file name in storage. | ||
It is important to make these unique to prevent data loss. Only use it if you need to. | ||
Default uses `crypto.randomBytes(16).toString('hex')`. | ||
> [!TIP] | ||
> Checkout the example how to [store files in custom nested directories](#example-store-files-in-custom-nested-directories). | ||
#### `disableTerminationForFinishedUploads` | ||
Disallow the [termination extension](https://tus.io/protocols/resumable-upload#termination) for finished uploads. (`boolean`) | ||
#### `options.onUploadCreate` | ||
@@ -205,2 +235,53 @@ | ||
### Key-Value Stores | ||
All stores (as in the `datastore` option) save two files, | ||
the uploaded file and an info file with metadata, usually adjacent to each other. | ||
In `@tus/file-store` the `FileKvStore` is used to persist upload info but the KV stores | ||
can also be used as a cache in other stores, such as `@tus/s3-store`. | ||
#### `MemoryKvStore` | ||
```ts | ||
import {MemoryKvStore} from '@tus/server' | ||
import S3Store, {type MetadataValue} from '@tus/s3-store' | ||
new S3Store({ | ||
// ... | ||
cache: new MemoryKvStore<MetadataValue>(), | ||
}) | ||
``` | ||
#### `FileKvStore` | ||
```ts | ||
import {FileKvStore} from '@tus/server' | ||
import S3Store, {type MetadataValue} from '@tus/s3-store' | ||
const path = './uploads' | ||
new S3Store({ | ||
// ... | ||
cache: new FileKvStore<MetadataValue>(path), | ||
}) | ||
``` | ||
#### `RedisKvStore` | ||
```ts | ||
import {RedisKvStore} from '@tus/server' | ||
import S3Store, {type MetadataValue} from '@tus/s3-store' | ||
import {createClient} from '@redis/client' | ||
const client = await createClient().connect() | ||
const path = './uploads' | ||
const prefix = 'foo' // prefix for the key (foo${id}) | ||
new S3Store({ | ||
// ... | ||
cache: new RedisKvStore<MetadataValue>(client, prefix), | ||
}) | ||
``` | ||
## Examples | ||
@@ -361,3 +442,3 @@ | ||
```js | ||
const { Server } = require("@tus/server"); | ||
const {Server} = require('@tus/server') | ||
// ... | ||
@@ -368,6 +449,6 @@ | ||
async onIncomingRequest(req, res) { | ||
const token = req.headers.authorization; | ||
const token = req.headers.authorization | ||
if (!token) { | ||
throw { status_code: 401, body: 'Unauthorized' } | ||
throw {status_code: 401, body: 'Unauthorized'} | ||
} | ||
@@ -379,11 +460,47 @@ | ||
} catch (error) { | ||
throw { status_code: 401, body: 'Invalid token' } | ||
throw {status_code: 401, body: 'Invalid token'} | ||
} | ||
if (req.user.role !== 'admin') { | ||
throw { status_code: 403, body: 'Access denied' } | ||
throw {status_code: 403, body: 'Access denied'} | ||
} | ||
}, | ||
}); | ||
}) | ||
``` | ||
### Example: store files in custom nested directories | ||
You can use `namingFunction` to change the name of the stored file. | ||
If you’re only adding a prefix or suffix without a slash (`/`), | ||
you don’t need to implement `generateUrl` and `getFileIdFromRequest`. | ||
Adding a slash means you create a new directory, for which you need | ||
to implement all three functions as we need encode the id with base64 into the URL. | ||
```js | ||
const path = '/files' | ||
const server = new Server({ | ||
path, | ||
datastore: new FileStore({directory: './test/output'}), | ||
namingFunction(req) { | ||
const id = crypto.randomBytes(16).toString('hex') | ||
const folder = getFolderForUser(req) // your custom logic | ||
return `users/${folder}/${id}` | ||
}, | ||
generateUrl(req, {proto, host, path, id}) { | ||
id = Buffer.from(id, 'utf-8').toString('base64url') | ||
return `${proto}://${host}${path}/${id}` | ||
}, | ||
getFileIdFromRequest(req) { | ||
const reExtractFileID = /([^/]+)\/?$/ | ||
const match = reExtractFileID.exec(req.url as string) | ||
if (!match || path.includes(match[1])) { | ||
return | ||
} | ||
return Buffer.from(match[1], 'base64url').toString('utf-8') | ||
}, | ||
}) | ||
``` | ||
@@ -413,2 +530,3 @@ | ||
[`models`]: https://github.com/tus/tus-node-server/blob/main/packages/server/src/models/index.ts | ||
[`kvstores`]: https://github.com/tus/tus-node-server/blob/main/packages/server/src/kvstores/index.ts | ||
[expiration]: https://tus.io/protocols/resumable-upload.html#expiration |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
112024
61
2275
525
2