diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cc8ec71..f3428753 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ name: CI on: - pull_request_target: + pull_request: push: branches: - master diff --git a/package-lock.json b/package-lock.json index b23d531c..65de57f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,9 +18,9 @@ "@fastify/swagger": "^8.3.1", "@fastify/swagger-ui": "^1.7.0", "@isaacs/ttlcache": "^1.4.1", - "@tus/file-store": "https://gitpkg.now.sh/supabase/tus-node-server/packages/file-store/dist?supabase/build-v2", - "@tus/s3-store": "https://gitpkg.now.sh/supabase/tus-node-server/packages/s3-store/dist?supabase/build-v2", - "@tus/server": "https://gitpkg.now.sh/supabase/tus-node-server/packages/server/dist?supabase/build-v2", + "@tus/file-store": "1.1.0", + "@tus/s3-store": "1.2.0", + "@tus/server": "1.2.0", "agentkeepalive": "^4.2.1", "async-retry": "^1.3.3", "axios": "^0.27.2", @@ -64,15 +64,15 @@ "@typescript-eslint/parser": "^5.12.1", "babel-jest": "^29.2.2", "eslint": "^8.9.0", - "eslint-config-prettier": "^8.4.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint-config-prettier": "8.10.0", + "eslint-plugin-prettier": "4.2.1", "form-data": "^4.0.0", "jest": "^29.2.2", "js-yaml": "^4.1.0", "json-schema-to-ts": "^2.5.4", "mustache": "^4.2.0", "pino-pretty": "^8.1.0", - "prettier": "^2.5.1", + "prettier": "2.8.8", "ts-jest": "^29.0.3", "ts-node-dev": "^1.1.8", "tsx": "^3.13.0", @@ -3861,13 +3861,11 @@ "peer": true }, "node_modules/@tus/file-store": { - "version": "1.0.1", - "resolved": "https://gitpkg.now.sh/supabase/tus-node-server/packages/file-store/dist?supabase/build-v2", - "integrity": "sha512-5NV2grkv7pGWTn3PRDMZ6NRMazYNr3MlyqqbdPW/EtriD4AbN5dQWYuGdg6BHDg//h/rW0OPYN70Q6g2Vfj+Yg==", - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tus/file-store/-/file-store-1.1.0.tgz", + "integrity": "sha512-DsHDm/r6tDNDti9wk09Cc3vVwVBTTG7lbemEUAXkiOiW47e+A0/YA+S3grxjZah2Gc4Ok9V2ii9fHo7ZbPlm0w==", "dependencies": { - "debug": "^4.3.4", - "p-queue": "^6.6.2" + "debug": "^4.3.4" }, "engines": { "node": ">=16" @@ -3876,14 +3874,13 @@ "@redis/client": "^1.5.9" }, "peerDependencies": { - "@tus/server": "https://gitpkg.now.sh/supabase/tus-node-server/packages/server/dist?supabase/build-v2" + "@tus/server": "^1.0.1" } }, "node_modules/@tus/s3-store": { - "version": "1.1.1", - "resolved": "https://gitpkg.now.sh/supabase/tus-node-server/packages/s3-store/dist?supabase/build-v2", - "integrity": "sha512-UdTFJKCQ3RWjNXDX4YDQhbODLPyfDDwCgSHVmz/GhKeW5erzt4I/qRBCPp5tKVvTGDPhk8uX+bF1jd52K4Op6g==", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tus/s3-store/-/s3-store-1.2.0.tgz", + "integrity": "sha512-oUBTqbA2fTcJiyZXfOPfC/K6NMc60nE/1DDXrdrsFpeRDX6GG28qszxHqiK+fuDAbnKY2YX9hwykU5cMmBCN+g==", "dependencies": { "@aws-sdk/client-s3": "^3.400.0", "debug": "^4.3.4" @@ -3892,7 +3889,7 @@ "node": ">=16" }, "peerDependencies": { - "@tus/server": "https://gitpkg.now.sh/supabase/tus-node-server/packages/server/dist?supabase/build-v2" + "@tus/server": "^1.0.1" } }, "node_modules/@tus/s3-store/node_modules/@aws-sdk/client-s3": { @@ -4484,10 +4481,9 @@ } }, "node_modules/@tus/server": { - "version": "1.0.1", - "resolved": "https://gitpkg.now.sh/supabase/tus-node-server/packages/server/dist?supabase/build-v2", - "integrity": "sha512-33rmdQc1Oavae+l2idZXFU0k+3nDQ7gtvs3kNVyqYjeRageLKSx7nOCoLhteL1tQ3DiVnEPk+EPcvwG/uoTcRg==", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tus/server/-/server-1.2.0.tgz", + "integrity": "sha512-fJ0Dtej1O86byvZuOcr6Eg486m17eDYi8Bxw4ABa3ZCaEgvUMpyL/2gjmOb4/j4IzErUjrFlJpxNKqfbP/n7IA==", "dependencies": { "debug": "^4.3.4" }, @@ -6057,9 +6053,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.4.0.tgz", - "integrity": "sha512-CFotdUcMY18nGRo5KGsnNxpznzhkopOcOo0InID+sgQssPrzjvsyKZPvOgymTFeHrFuC3Tzdf2YndhXtULK9Iw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -6069,15 +6065,15 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", - "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=12.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", @@ -6287,11 +6283,6 @@ "node": ">=6" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -8444,14 +8435,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8507,32 +8490,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -9071,15 +9028,18 @@ } }, "node_modules/prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "bin": { "prettier": "bin-prettier.js" }, "engines": { "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/prettier-linter-helpers": { @@ -13496,17 +13456,18 @@ "peer": true }, "@tus/file-store": { - "version": "https://gitpkg.now.sh/supabase/tus-node-server/packages/file-store/dist?supabase/build-v2", - "integrity": "sha512-5NV2grkv7pGWTn3PRDMZ6NRMazYNr3MlyqqbdPW/EtriD4AbN5dQWYuGdg6BHDg//h/rW0OPYN70Q6g2Vfj+Yg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tus/file-store/-/file-store-1.1.0.tgz", + "integrity": "sha512-DsHDm/r6tDNDti9wk09Cc3vVwVBTTG7lbemEUAXkiOiW47e+A0/YA+S3grxjZah2Gc4Ok9V2ii9fHo7ZbPlm0w==", "requires": { "@redis/client": "^1.5.9", - "debug": "^4.3.4", - "p-queue": "^6.6.2" + "debug": "^4.3.4" } }, "@tus/s3-store": { - "version": "https://gitpkg.now.sh/supabase/tus-node-server/packages/s3-store/dist?supabase/build-v2", - "integrity": "sha512-UdTFJKCQ3RWjNXDX4YDQhbODLPyfDDwCgSHVmz/GhKeW5erzt4I/qRBCPp5tKVvTGDPhk8uX+bF1jd52K4Op6g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tus/s3-store/-/s3-store-1.2.0.tgz", + "integrity": "sha512-oUBTqbA2fTcJiyZXfOPfC/K6NMc60nE/1DDXrdrsFpeRDX6GG28qszxHqiK+fuDAbnKY2YX9hwykU5cMmBCN+g==", "requires": { "@aws-sdk/client-s3": "^3.400.0", "debug": "^4.3.4" @@ -14011,8 +13972,9 @@ } }, "@tus/server": { - "version": "https://gitpkg.now.sh/supabase/tus-node-server/packages/server/dist?supabase/build-v2", - "integrity": "sha512-33rmdQc1Oavae+l2idZXFU0k+3nDQ7gtvs3kNVyqYjeRageLKSx7nOCoLhteL1tQ3DiVnEPk+EPcvwG/uoTcRg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tus/server/-/server-1.2.0.tgz", + "integrity": "sha512-fJ0Dtej1O86byvZuOcr6Eg486m17eDYi8Bxw4ABa3ZCaEgvUMpyL/2gjmOb4/j4IzErUjrFlJpxNKqfbP/n7IA==", "requires": { "debug": "^4.3.4" } @@ -15236,16 +15198,16 @@ } }, "eslint-config-prettier": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.4.0.tgz", - "integrity": "sha512-CFotdUcMY18nGRo5KGsnNxpznzhkopOcOo0InID+sgQssPrzjvsyKZPvOgymTFeHrFuC3Tzdf2YndhXtULK9Iw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, "requires": {} }, "eslint-plugin-prettier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", - "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0" @@ -15357,11 +15319,6 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -17003,11 +16960,6 @@ "word-wrap": "^1.2.3" } }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -17044,23 +16996,6 @@ "aggregate-error": "^3.0.0" } }, - "p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "requires": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - } - }, - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "requires": { - "p-finally": "^1.0.0" - } - }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -17484,9 +17419,9 @@ "dev": true }, "prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true }, "prettier-linter-helpers": { diff --git a/package.json b/package.json index 3110bc1f..2f8f4dd2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test:dummy-data": "tsx -r dotenv/config ./src/test/db/import-dummy-data.ts", "test": "npm run infra:restart && npm run test:dummy-data && jest --runInBand --forceExit", "test:coverage": "npm run infra:restart && npm run test:dummy-data && jest --runInBand --coverage --forceExit", - "prettier:check": "prettier -c src/**", + "prettier:check": "prettier -v && prettier -c src/**", "format": "prettier -c --write src/**", "eslint:check": "eslint 'src/**'", "infra:stop": "docker-compose --project-directory . -f src/test/db/docker-compose.yml down --remove-orphans", @@ -34,9 +34,9 @@ "@fastify/swagger": "^8.3.1", "@fastify/swagger-ui": "^1.7.0", "@isaacs/ttlcache": "^1.4.1", - "@tus/file-store": "https://gitpkg.now.sh/supabase/tus-node-server/packages/file-store/dist?supabase/build-v2", - "@tus/s3-store": "https://gitpkg.now.sh/supabase/tus-node-server/packages/s3-store/dist?supabase/build-v2", - "@tus/server": "https://gitpkg.now.sh/supabase/tus-node-server/packages/server/dist?supabase/build-v2", + "@tus/file-store": "1.1.0", + "@tus/s3-store": "1.2.0", + "@tus/server": "1.2.0", "agentkeepalive": "^4.2.1", "async-retry": "^1.3.3", "axios": "^0.27.2", @@ -77,15 +77,15 @@ "@typescript-eslint/parser": "^5.12.1", "babel-jest": "^29.2.2", "eslint": "^8.9.0", - "eslint-config-prettier": "^8.4.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint-config-prettier": "^8.10.0", + "eslint-plugin-prettier": "^4.2.1", "form-data": "^4.0.0", "jest": "^29.2.2", "js-yaml": "^4.1.0", "json-schema-to-ts": "^2.5.4", "mustache": "^4.2.0", "pino-pretty": "^8.1.0", - "prettier": "^2.5.1", + "prettier": "^2.8.8", "ts-jest": "^29.0.3", "ts-node-dev": "^1.1.8", "tsx": "^3.13.0", diff --git a/src/http/routes/tus/handlers.ts b/src/http/routes/tus/handlers.ts new file mode 100644 index 00000000..986345f9 --- /dev/null +++ b/src/http/routes/tus/handlers.ts @@ -0,0 +1,33 @@ +import { CancellationContext, ERRORS, EVENTS } from '@tus/server' +import { DeleteHandler as BaseDeleteHandler } from '@tus/server/dist/handlers/DeleteHandler' +import http from 'node:http' + +export class DeleteHandler extends BaseDeleteHandler { + async send(req: http.IncomingMessage, res: http.ServerResponse, context: CancellationContext) { + const id = this.getFileIdFromRequest(req) + if (!id) { + throw ERRORS.FILE_NOT_FOUND + } + + if (this.options.onIncomingRequest) { + await this.options.onIncomingRequest(req, res, id) + } + + const lock = await this.acquireLock(req, id, context) + try { + const upload = await this.store.getUpload(id) + if (upload.offset === upload.size) { + throw { + status_code: 400, + body: 'Cannot terminate an already completed upload', + } + } + await this.store.remove(id) + } finally { + await lock.unlock() + } + const writtenRes = this.write(res, 204, {}) + this.emit(EVENTS.POST_TERMINATE, req, writtenRes, id) + return writtenRes + } +} diff --git a/src/http/routes/tus/index.ts b/src/http/routes/tus/index.ts index 3c75f63d..0cfe6702 100644 --- a/src/http/routes/tus/index.ts +++ b/src/http/routes/tus/index.ts @@ -13,15 +13,15 @@ import { generateUrl, getFileIdFromRequest, } from './lifecycle' -import { ServerOptions } from '@tus/server/types' -import { DataStore } from '@tus/server/models' +import { ServerOptions, DataStore } from '@tus/server' import { getFileSizeLimit } from '../../../storage/limits' import { UploadId } from './upload-id' import { FileStore } from './file-store' import { TenantConnection } from '../../../database/connection' -import { LockManager, PostgresLocker } from './postgres-locker' +import { PgLocker, LockNotifier } from './postgres-locker' import { PubSub } from '../../../database/pubsub' import { S3Store } from './s3-store' +import { DeleteHandler } from './handlers' const { globalS3Bucket, @@ -63,7 +63,7 @@ function createTusStore() { }) } -function createTusServer(lockManager: LockManager) { +function createTusServer(lockNotifier: LockNotifier) { const datastore = createTusStore() const serverOptions: ServerOptions & { datastore: DataStore @@ -72,7 +72,7 @@ function createTusServer(lockManager: LockManager) { datastore: datastore, locker: (rawReq: http.IncomingMessage) => { const req = rawReq as MultiPartRequest - return new PostgresLocker(lockManager, req.upload.storage.db) + return new PgLocker(req.upload.storage.db, lockNotifier) }, namingFunction: namingFunction, onUploadCreate: onCreate, @@ -106,12 +106,16 @@ function createTusServer(lockManager: LockManager) { return fileSizeLimit }, } - return new Server(serverOptions) + const server = new Server(serverOptions) + server.handlers.DELETE = new DeleteHandler(datastore, serverOptions) + return server } export default async function routes(fastify: FastifyInstance) { - const lockManager = new LockManager(PubSub) - const tusServer = createTusServer(lockManager) + const lockNotifier = new LockNotifier(PubSub) + await lockNotifier.subscribe() + + const tusServer = createTusServer(lockNotifier) fastify.register(async function authorizationContext(fastify) { fastify.addContentTypeParser('application/offset+octet-stream', (request, payload, done) => @@ -169,6 +173,13 @@ export default async function routes(fastify: FastifyInstance) { tusServer.handle(req.raw, res.raw) } ) + fastify.delete( + '/*', + { schema: { summary: 'Handle DELETE request for TUS Resumable uploads', tags: ['object'] } }, + (req, res) => { + tusServer.handle(req.raw, res.raw) + } + ) }) fastify.register(async function authorizationContext(fastify) { diff --git a/src/http/routes/tus/lifecycle.ts b/src/http/routes/tus/lifecycle.ts index bc4a1bd1..fe4d0a20 100644 --- a/src/http/routes/tus/lifecycle.ts +++ b/src/http/routes/tus/lifecycle.ts @@ -183,7 +183,6 @@ export function onResponseError( res: http.ServerResponse, e: TusError | Error ) { - console.error(e) if (e instanceof Error) { ;(res as any).executionError = e } diff --git a/src/http/routes/tus/postgres-locker.ts b/src/http/routes/tus/postgres-locker.ts index 0cdb5c54..29c440ee 100644 --- a/src/http/routes/tus/postgres-locker.ts +++ b/src/http/routes/tus/postgres-locker.ts @@ -1,66 +1,60 @@ -import { Locker } from '@tus/server' +import { Lock, Locker, RequestRelease } from '@tus/server' import { Database, DBError } from '../../../storage/database' import { PubSubAdapter } from '../../../pubsub' import { UploadId } from './upload-id' -import { RequestRelease } from '@tus/server/models/Locker' import { clearTimeout } from 'timers' - -class Lock { - tnxResolver?: () => void - requestRelease?: RequestRelease -} +import EventEmitter from 'events' const REQUEST_LOCK_RELEASE_MESSAGE = 'REQUEST_LOCK_RELEASE' -export class LockManager { - readonly listener: Promise - private locks: Map = new Map() - - constructor(private readonly pubSub: PubSubAdapter) { - this.listener = this.listenForMessages() - } +export class LockNotifier { + protected events = new EventEmitter() + constructor(private readonly pubSub: PubSubAdapter) {} - releaseExistingLock(id: string) { + release(id: string) { return this.pubSub.publish(REQUEST_LOCK_RELEASE_MESSAGE, { id }) } - addLock(id: string, lock: Lock) { - this.locks.set(id, lock) + onRelease(id: string, callback: () => void) { + this.events.once(`release:${id}`, callback) } - deleteLock(id: string) { - const lock = this.locks.get(id) - if (!lock) { - throw new Error('unlocking not existing lock') - } - - lock.tnxResolver && lock.tnxResolver() - this.locks.delete(id) + removeListeners(id: string) { + this.events.removeAllListeners(`release:${id}`) } - protected async listenForMessages() { - await this.pubSub.subscribe(REQUEST_LOCK_RELEASE_MESSAGE, async ({ id }: { id: string }) => { - const lock = this.locks.get(id) - if (lock) { - await lock.requestRelease?.() - } + async subscribe() { + await this.pubSub.subscribe(REQUEST_LOCK_RELEASE_MESSAGE, ({ id }) => { + this.events.emit(`release:${id}`) }) } } -export class PostgresLocker implements Locker { - constructor(private readonly manager: LockManager, private readonly db: Database) {} +export class PgLocker implements Locker { + constructor(private readonly db: Database, private readonly notifier: LockNotifier) {} - async lock(id: string, cancel: RequestRelease): Promise { - await this.manager.listener + newLock(id: string): Lock { + return new PgLock(id, this.db, this.notifier) + } +} + +export class PgLock implements Lock { + tnxResolver?: () => void + constructor( + private readonly id: string, + private readonly db: Database, + private readonly notifier: LockNotifier + ) {} + + async lock(cancelReq: RequestRelease): Promise { await new Promise((resolve, reject) => { this.db .withTransaction(async (db) => { const abortController = new AbortController() const acquired = await Promise.race([ this.waitTimeout(15000, abortController.signal), - this.acquireLock(db, id, abortController.signal), + this.acquireLock(db, this.id, abortController.signal), ]) abortController.abort() @@ -70,19 +64,22 @@ export class PostgresLocker implements Locker { } await new Promise((innerResolve) => { - const lock = new Lock() - lock.tnxResolver = innerResolve - lock.requestRelease = cancel - this.manager.addLock(id, lock) + this.tnxResolver = innerResolve resolve() }) }) .catch(reject) }) + + this.notifier.onRelease(this.id, () => { + cancelReq() + }) } - async unlock(id: string): Promise { - this.manager.deleteLock(id) + async unlock(): Promise { + this.notifier.removeListeners(this.id) + this.tnxResolver?.() + this.tnxResolver = undefined } protected async acquireLock(db: Database, id: string, signal: AbortSignal) { @@ -94,7 +91,7 @@ export class PostgresLocker implements Locker { return true } catch (e) { if (e instanceof DBError && e.message === 'resource_locked') { - await this.manager.releaseExistingLock(id) + await this.notifier.release(id) await new Promise((resolve) => { setTimeout(resolve, 100) }) diff --git a/src/http/routes/tus/s3-store.ts b/src/http/routes/tus/s3-store.ts index f69b5456..d8b4bd1d 100644 --- a/src/http/routes/tus/s3-store.ts +++ b/src/http/routes/tus/s3-store.ts @@ -32,8 +32,6 @@ export class S3Store extends BaseS3Store { return cached } - console.log('getMetadata', id, bucket, cache) - const { Metadata, Body } = await client.getObject({ Bucket: bucket, Key: id + '.info', diff --git a/src/monitoring/logger.ts b/src/monitoring/logger.ts index bbcabee6..b4fefe05 100644 --- a/src/monitoring/logger.ts +++ b/src/monitoring/logger.ts @@ -121,6 +121,7 @@ const whitelistHeaders = (headers: Record) => { 'if-modified-since', 'upload-metadata', 'upload-length', + 'upload-offset', 'tus-resumable', ] const allowlistedResponseHeaders = [ diff --git a/src/pubsub/postgres.ts b/src/pubsub/postgres.ts index 49918c3e..3d9050fb 100644 --- a/src/pubsub/postgres.ts +++ b/src/pubsub/postgres.ts @@ -32,9 +32,10 @@ export class PostgresPubSub implements PubSubAdapter { } async subscribe(channel: string, cb: (payload: any) => void): Promise { + const listenerCount = this.subscriber.notifications.listenerCount(channel) this.subscriber.notifications.on(channel, cb) - if (this.isConnected && this.subscriber.notifications.listenerCount(channel) === 0) { + if (this.isConnected && listenerCount === 0) { await this.subscriber.listenTo(channel) } } diff --git a/src/queue/queue.ts b/src/queue/queue.ts index 513fc2e2..0c0a7596 100644 --- a/src/queue/queue.ts +++ b/src/queue/queue.ts @@ -6,8 +6,9 @@ import { QueueJobRetryFailed, QueueJobCompleted, QueueJobError } from '../monito import { logger } from '../monitoring' import { normalizeRawError } from '../storage' +//eslint-disable-next-line @typescript-eslint/no-explicit-any type SubclassOfBaseClass = (new (payload: any) => BaseEvent) & { - [K in keyof typeof BaseEvent]: typeof BaseEvent[K] + [K in keyof typeof BaseEvent]: (typeof BaseEvent)[K] } export abstract class Queue { diff --git a/src/storage/database/knex.ts b/src/storage/database/knex.ts index 3d4da899..242ecd86 100644 --- a/src/storage/database/knex.ts +++ b/src/storage/database/knex.ts @@ -35,6 +35,7 @@ export class StorageKnexDB implements Database { this.role = connection?.role } + //eslint-disable-next-line @typescript-eslint/no-explicit-any async withTransaction Promise>( fn: T, transactionOptions?: TransactionOptions @@ -400,7 +401,7 @@ export class StorageKnexDB implements Database { } return object as typeof filters extends FindObjectFilters - ? typeof filters['dontErrorOnEmpty'] extends true + ? (typeof filters)['dontErrorOnEmpty'] extends true ? Obj | undefined : Obj : Obj diff --git a/src/storage/uploader.ts b/src/storage/uploader.ts index 30c95b3f..f78895b8 100644 --- a/src/storage/uploader.ts +++ b/src/storage/uploader.ts @@ -159,7 +159,7 @@ export class Uploader { owner, }) - const events: Promise[] = [] + const events: Promise[] = [] // schedule the deletion of the previous file if (currentObj && currentObj.version !== version) { @@ -272,7 +272,7 @@ export class Uploader { throw new StorageBackendError('empty_file', 400, 'Unexpected empty file received', e) } } else { - // just assume its a binary file + // just assume it's a binary file body = request.raw mimeType = request.headers['content-type'] || 'application/octet-stream' cacheControl = request.headers['cache-control'] ?? 'no-cache'