Skip to content

Commit

Permalink
rebase: main
Browse files Browse the repository at this point in the history
  • Loading branch information
fenos committed Jan 17, 2024
2 parents dc5f8c2 + 367fdec commit b4ef68b
Show file tree
Hide file tree
Showing 16 changed files with 1,181 additions and 805 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
"test": "turbo run test --concurrency 1"
},
"devDependencies": {
"eslint": "^8.48.0",
"eslint": "^8.56.0",
"eslint-config-custom": "workspace:*",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.8",
"turbo": "^1.10.13"
"turbo": "^1.11.3"
},
"packageManager": "[email protected]+sha256.825003a0f561ad09a3b1ac4a3b3ea6207af2796d54f62a9420520915721f5186"
}
8 changes: 4 additions & 4 deletions packages/eslint-config-custom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
},
"dependencies": {
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^6.13.1",
"eslint": "^8.48.0",
"@typescript-eslint/parser": "^6.19.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0",
"eslint-config-turbo": "^1.10.16",
"eslint-config-turbo": "^1.11.3",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.8"
},
"devDependencies": {
"@types/eslint": "^8.44.2",
"@types/eslint": "^8.56.2",
"@types/prettier": "^2.7.3"
}
}
25 changes: 7 additions & 18 deletions packages/file-store/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// TODO: use /promises versions
import fs from 'node:fs'
import fsProm from 'node:fs/promises'
import path from 'node:path'
import stream from 'node:stream'
import http from 'node:http'
Expand Down Expand Up @@ -57,26 +58,14 @@ export class FileStore extends DataStore {
/**
* Create an empty file.
*/
create(file: Upload): Promise<Upload> {
return new Promise((resolve, reject) => {
fs.open(path.join(this.directory, file.id), 'w', async (err, fd) => {
if (err) {
log('[FileStore] create: Error', err)
return reject(err)
}
async create(file: Upload): Promise<Upload> {
const dirs = file.id.split('/').slice(0, -1)

await this.configstore.set(file.id, file)
await fsProm.mkdir(path.join(this.directory, ...dirs), {recursive: true})
await fsProm.writeFile(path.join(this.directory, file.id), '')
await this.configstore.set(file.id, file)

return fs.close(fd, (exception) => {
if (exception) {
log('[FileStore] create: Error', exception)
return reject(exception)
}

return resolve(file)
})
})
})
return file
}

read(file_id: string) {
Expand Down
12 changes: 6 additions & 6 deletions packages/file-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@
},
"devDependencies": {
"@tus/server": "workspace:^",
"@types/debug": "^4.1.8",
"@types/mocha": "^10.0.1",
"@types/node": "^20.10.4",
"eslint": "^8.48.0",
"@types/debug": "^4.1.12",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.5",
"eslint": "^8.56.0",
"eslint-config-custom": "workspace:*",
"mocha": "^10.2.0",
"should": "^13.2.3",
"typescript": "^5.2.2"
"typescript": "^5.3.3"
},
"peerDependencies": {
"@tus/server": "workspace:^"
},
"optionalDependencies": {
"@redis/client": "^1.5.9"
"@redis/client": "^1.5.13"
},
"engines": {
"node": ">=16"
Expand Down
5 changes: 0 additions & 5 deletions packages/file-store/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ describe('FileStore', function () {
describe('create', () => {
const file = new Upload({id: '1234', size: 1000, offset: 0})

it('should reject when the directory doesnt exist', function () {
this.datastore.directory = 'some_new_path'
return this.datastore.create(file).should.be.rejected()
})

it('should resolve when the directory exists', function () {
return this.datastore.create(file).should.be.fulfilled()
})
Expand Down
10 changes: 5 additions & 5 deletions packages/gcs-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
"devDependencies": {
"@google-cloud/storage": "^6.12.0",
"@tus/server": "workspace:^",
"@types/debug": "^4.1.8",
"@types/mocha": "^10.0.1",
"@types/node": "^20.10.4",
"eslint": "^8.48.0",
"@types/debug": "^4.1.12",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.5",
"eslint": "^8.56.0",
"eslint-config-custom": "workspace:*",
"mocha": "^10.2.0",
"should": "^13.2.3",
"typescript": "^5.2.2"
"typescript": "^5.3.3"
},
"peerDependencies": {
"@google-cloud/storage": "*",
Expand Down
12 changes: 6 additions & 6 deletions packages/s3-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@
"test": "mocha test.ts --timeout 40000 --exit --extension ts --require ts-node/register"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.400.0",
"@aws-sdk/client-s3": "^3.490.0",
"debug": "^4.3.4"
},
"devDependencies": {
"@tus/server": "workspace:^",
"@types/debug": "^4.1.8",
"@types/mocha": "^10.0.1",
"@types/node": "^20.10.4",
"eslint": "^8.48.0",
"@types/debug": "^4.1.12",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.5",
"eslint": "^8.56.0",
"eslint-config-custom": "workspace:*",
"mocha": "^10.2.0",
"should": "^13.2.3",
"typescript": "^5.2.2"
"typescript": "^5.3.3"
},
"peerDependencies": {
"@tus/server": "workspace:^"
Expand Down
76 changes: 69 additions & 7 deletions packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [Example: integrate tus into Fastify](#example-integrate-tus-into-fastify)
- [Example: integrate tus into Next.js](#example-integrate-tus-into-nextjs)
- [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)
- [Compatibility](#compatibility)
- [Contribute](#contribute)
Expand Down Expand Up @@ -78,18 +79,43 @@ Allow `Forwarded`, `X-Forwarded-Proto`, and `X-Forwarded-Host` headers to overri
Additional headers sent in `Access-Control-Allow-Headers` (`string[]`).

#### `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`)

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`)
Expand Down Expand Up @@ -358,30 +384,66 @@ Access control is opinionated and can be done in different ways.
This example is psuedo-code for what it could look like with JSON Web Tokens.
```js
const { Server } = require("@tus/server");
const {Server} = require('@tus/server')
// ...

const server = new Server({
// ..
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'}
}

try {
const decodedToken = await jwt.verify(token, 'your_secret_key')
req.user = decodedToken
} 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')
},
})

```
Expand Down
20 changes: 10 additions & 10 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@
"debug": "^4.3.4"
},
"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": "workspace:*",
"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.9"
Expand Down
8 changes: 2 additions & 6 deletions packages/server/src/handlers/BaseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ export class BaseHandler extends EventEmitter {
}

generateUrl(req: http.IncomingMessage, id: string) {
// @ts-expect-error req.baseUrl does exist
const baseUrl = req.baseUrl ?? ''
const path = this.options.path === '/' ? '' : this.options.path

if (this.options.generateUrl) {
Expand All @@ -50,21 +48,19 @@ export class BaseHandler extends EventEmitter {
return this.options.generateUrl(req, {
proto,
host,
// @ts-expect-error we can pass undefined
baseUrl: req.baseUrl,
path: path,
id,
})
}

// Default implementation
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}`
}

getFileIdFromRequest(req: http.IncomingMessage) {
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type ServerOptions = {
*/
generateUrl?: (
req: http.IncomingMessage,
options: {proto: string; host: string; baseUrl: string; path: string; id: string}
options: {proto: string; host: string; path: string; id: string}
) => string

/**
Expand Down
9 changes: 4 additions & 5 deletions packages/server/test/BaseHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,20 @@ describe('BaseHandler', () => {
const handler = new BaseHandler(store, {
path: '/path',
locker: new MemoryLocker(),
generateUrl: (req: http.IncomingMessage, info) => {
const {proto, host, baseUrl, path, id} = info
return `${proto}://${host}${baseUrl}${path}/${id}?customParam=1`
generateUrl: (_, info) => {
const {proto, host, path, id} = info
return `${proto}://${host}${path}/${id}?customParam=1`
},
})

const req = httpMocks.createRequest({
headers: {
host: 'localhost',
},
url: '/upload',
})
const id = '123'
const url = handler.generateUrl(req, id)
assert.equal(url, `http://localhost/upload/path/123?customParam=1`)
assert.equal(url, `http://localhost/path/123?customParam=1`)
})

it('should allow extracting the request id with a custom function', () => {
Expand Down
Loading

0 comments on commit b4ef68b

Please sign in to comment.