Initial commit

This commit is contained in:
CallMeVerity
2026-06-03 00:44:48 +01:00
commit eb56ad5183
36 changed files with 3419 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# RustFS (S3-compatible) configuration
RUSTFS_ENDPOINT=http://localhost:9000
RUSTFS_ACCESS_KEY=minioadmin
RUSTFS_SECRET_KEY=minioadmin
RUSTFS_BUCKET=surf
RUSTFS_PUBLIC=true
# Server
PORT=3001
DB_PATH=./data/surf.db
# Auth - required, protects all upload/delete endpoints
ADMIN_TOKEN=change-me-to-a-real-secret
+162
View File
@@ -0,0 +1,162 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "surfnathanrip-backend",
"dependencies": {
"@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/s3-request-presigner": "^3.700.0",
"@elysiajs/cors": "latest",
"@elysiajs/static": "latest",
"elysia": "latest",
},
"devDependencies": {
"bun-types": "latest",
},
},
},
"packages": {
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
"@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1058.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.48", "@aws-sdk/middleware-bucket-endpoint": "^3.972.17", "@aws-sdk/middleware-expect-continue": "^3.972.14", "@aws-sdk/middleware-flexible-checksums": "^3.974.23", "@aws-sdk/middleware-location-constraint": "^3.972.11", "@aws-sdk/middleware-sdk-s3": "^3.972.44", "@aws-sdk/middleware-ssec": "^3.972.11", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-AfED3hhaBZ121NuiBImgnlF98kQRMk6hGPMGfj/Oo1hSaoMFRzM+N4nlICCasUSM2R8QaIRZRYGpZ3fy0ilGZQ=="],
"@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
"@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-P+QGozmXn2mZZI7sDgk+aUm+RTI61MPSFB+Ir2vjEjEbEsE4e7hYtzrDvAUxZy9ko81h53e11+F/GYlvwDkaOQ=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg=="],
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.43", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA=="],
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA=="],
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA=="],
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.48", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-QIbtJP0olSLZ2ImEu636pP+7JJbPfaL3xSJIFXhu472CWuondCc4bGOa8OeyhOFet8z4H1D/ZFKXc39FboWwYA=="],
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ=="],
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/token-providers": "3.1056.0", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA=="],
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ=="],
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-lbDmWuHenc+kiwCNrxz4MyN6nkxCWyTXPIWuspJN0ibziu+8CXci7vI1bK9MAkwy8cwJOEXNu0gBM5S0uTGRIg=="],
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.14", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-3TNFEVGO4sWZj9TEXOCZLzGEctXHnaO4fk2EQ8KVaboTbwHmEPEQrm17Xb9koImUIXEw0sgi2xtHjg7LuTS3rA=="],
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.23", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/crc64-nvme": "^3.972.9", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-4nPKARo2lfKvQGUt2fPA5NlS/mEohckdxpuC9ecbjVfj7B7NFFYHeTg+Bf5BEQwdn3yRfUIzFiEkPp8Yuaw3wA=="],
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-hkfspNUP4criAH6ton6BGKgnm5dZx+7bUOy1YqlTfejDeUPAM23D81q/IX+hdlS3KUsfwGz5ADTqZWKBEUpf4A=="],
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.44", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-8HQsRg1NpX8vR4vNl1E8pyLnqZroq9VSL2vZQVSgBqp6wv6365LzYD08/c9FFh/9FTg7YRc7aTtEmXF0ir/pqg=="],
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-7PQvGNhtveKlvVqNahqWx5yrwxP7ecwAoB1dYBf8eKwfo2tzzCbNnW+q2nO3N066ktQaB4iBQbDRWtizm+amoQ=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
"@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.1058.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-IRgNfn8U3zfsZ0JkpmwjS59R/XyHMHxpuwW6HVuJhik+FsbClhNkujEO0w1WqJvXrF4FX+7qIAwUrvlwNvaZ7Q=="],
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1056.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA=="],
"@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="],
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="],
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
"@elysiajs/cors": ["@elysiajs/cors@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-FTCcbH35brTLigF1W7BYySRZomgI/dBEMK9BgK9RP9Nez7zmpGh4koL/Yr1BFv8nYz7CfhRvcM8d/c+XnwMaVQ=="],
"@elysiajs/static": ["@elysiajs/static@1.4.10", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-BN/3PZVWjqkL/6TM7RH69Pae89CfU0OqfEYPeBp2hB/GR5Euds39Wy+0ug9Vbvu++GG4tRRANf2QjaLnriJveg=="],
"@nodable/entities": ["@nodable/entities@2.1.1", "", {}, "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
"@smithy/core": ["@smithy/core@3.24.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.7", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ=="],
"@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="],
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="],
"fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
"file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
"strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "surfnathanrip-backend",
"version": "1.0.0",
"scripts": {
"dev": "bun run --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist --target bun",
"start": "bun run dist/index.js"
},
"dependencies": {
"elysia": "latest",
"@elysiajs/cors": "latest",
"@elysiajs/static": "latest",
"@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/s3-request-presigner": "^3.700.0"
},
"devDependencies": {
"bun-types": "latest"
}
}
+38
View File
@@ -0,0 +1,38 @@
import { Elysia, status } from "elysia";
import { cors } from "@elysiajs/cors";
import { videoRoutes } from "./routes/videos";
import { initDb } from "./services/db";
import { join } from "path";
import { existsSync } from "fs";
initDb();
const PORT = parseInt(process.env.PORT || "3001");
const STATIC_DIR = process.env.STATIC_DIR || "./public";
const app = new Elysia()
.use(cors())
.onError(({ error }) => {
console.error("Unhandled error:", error);
return status(500, { error: "Internal server error" });
})
.use(videoRoutes)
.get("/api/health", () => ({ status: "ok" }))
.get("*", ({ request }) => {
const url = new URL(request.url);
const pathname = url.pathname;
if (pathname.startsWith("/api")) return status(404, "Not found");
const filePath = join(STATIC_DIR, pathname);
if (existsSync(filePath)) return Bun.file(filePath);
const indexPath = join(STATIC_DIR, "index.html");
if (existsSync(indexPath)) return Bun.file(indexPath);
return status(404, "Not found");
})
.listen(PORT);
console.log(`surf.nathan.rip running on port ${PORT}`);
console.log(`Serving static files from ${STATIC_DIR}`);
+367
View File
@@ -0,0 +1,367 @@
import { Elysia, status } from "elysia";
import {
getAllVideos,
getVideoById,
deleteVideoById,
insertVideo,
updateThumbnailKey,
updateTier,
getVideoByMapAndPlayer,
updateVideoPB,
} from "../services/db";
import {
uploadObject,
deleteObject,
getObjectUrl,
getPresignedUploadUrl,
} from "../services/rustfs";
import { parseMtvFile } from "../services/mtv-parser";
import { getMapInfo } from "../services/momentum-api";
import type { VideoEntry, PreviousPB } from "../types/video";
const ADMIN_TOKEN = process.env.ADMIN_TOKEN;
if (!ADMIN_TOKEN) {
console.error(
"ADMIN_TOKEN environment variable is required. Set it to secure upload/delete endpoints.",
);
process.exit(1);
}
function sanitizeFileName(name: string): string {
return name
.replace(/\s+/g, "_")
.replace(/[^a-zA-Z0-9_.\-]/g, "")
.toLowerCase();
}
export const videoRoutes = new Elysia({ prefix: "/api/videos" })
.guard({
beforeHandle: ({ request }) => {
const isWrite =
request.method !== "GET" &&
request.method !== "HEAD" &&
request.method !== "OPTIONS";
if (isWrite) {
const auth = request.headers.get("authorization");
const token = auth?.startsWith("Bearer ")
? auth.slice(7)
: null;
if (!token || token !== ADMIN_TOKEN)
return status(401, "Unauthorized");
}
},
})
.get("/", () => {
const videos = getAllVideos();
return videos.map((v) => ({
...v,
thumbnailUrl: v.thumbnailUrl
? getObjectUrl(v.thumbnailUrl)
: undefined,
}));
})
.post("/upload-url", async ({ request }) => {
let formData: FormData;
try {
formData = await request.formData();
} catch {
return status(400, { error: "Failed to parse form data" });
}
const mtv = formData.get("mtv") as File | null;
const videoFileName = formData.get("videoFileName") as string | null;
const videoContentType =
(formData.get("videoContentType") as string | null) || "video/webm";
const runDate = formData.get("runDate") as string | null;
if (!mtv || !videoFileName) {
return status(400, {
error: "mtv file and videoFileName are required",
});
}
const mtvBuffer = await mtv.arrayBuffer();
const metadata = parseMtvFile(mtvBuffer);
if (!metadata) {
return status(400, {
error: "Invalid .mtv file: could not parse header",
});
}
const existing = getVideoByMapAndPlayer(
metadata.mapName,
metadata.steamId,
);
if (existing) {
if (existing.runTime <= metadata.runTime) {
return status(409, {
error: `PB not improved. Your current PB on ${metadata.mapName} is ${existing.runTime}, this run is ${metadata.runTime}`,
});
}
const deletes = [
deleteObject(existing.videoKey),
deleteObject(existing.mtvKey),
];
if (existing.thumbnailKey)
deletes.push(deleteObject(existing.thumbnailKey));
await Promise.all(deletes);
const previousPbs: PreviousPB[] = [
{ runTime: existing.runTime, createdAt: existing.createdAt },
...(existing.previousPbs ?? []),
];
const id = existing.id;
const createdAt =
runDate && !isNaN(Date.parse(runDate))
? new Date(runDate).toISOString()
: new Date().toISOString();
const sanitizedVideoName = sanitizeFileName(videoFileName);
const videoKey = `videos/${id}/${sanitizedVideoName}`;
const mtvKey = `videos/${id}/${sanitizeFileName(mtv.name)}`;
await uploadObject(
mtvKey,
Buffer.from(mtvBuffer),
"application/octet-stream",
);
let tier: number | null | undefined;
let mapId: number | null | undefined;
let thumbnailKey: string | undefined;
const mapInfo = await getMapInfo(metadata.mapName);
mapId = mapInfo.mapId ?? undefined;
tier = mapInfo.tier;
if (mapInfo.thumbnailUrl) {
thumbnailKey = `videos/${id}/thumbnail.jpg`;
try {
const imgRes = await fetch(mapInfo.thumbnailUrl);
if (imgRes.ok) {
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
await uploadObject(thumbnailKey, imgBuf, "image/jpeg");
} else {
thumbnailKey = undefined;
}
} catch {
thumbnailKey = undefined;
}
}
const title = `${metadata.mapName} - ${metadata.playerName}`;
const entry: VideoEntry = {
id,
title,
description: "",
mapName: metadata.mapName,
playerName: metadata.playerName,
steamId: metadata.steamId,
runTime: metadata.runTime,
totalTicks: metadata.totalTicks,
tickInterval: metadata.tickInterval,
videoKey,
mtvKey,
thumbnailKey,
tier,
mapId,
jsonStats: metadata.jsonStats,
createdAt,
previousPbs,
};
updateVideoPB(id, entry, previousPbs);
const presignedUrls = {
video: await getPresignedUploadUrl(videoKey, videoContentType),
};
return {
...entry,
presignedUrls,
videoUrl: getObjectUrl(videoKey),
mtvUrl: getObjectUrl(mtvKey),
thumbnailUrl: thumbnailKey
? getObjectUrl(thumbnailKey)
: undefined,
};
}
const id = crypto.randomUUID();
const createdAt =
runDate && !isNaN(Date.parse(runDate))
? new Date(runDate).toISOString()
: new Date().toISOString();
const sanitizedVideoName = sanitizeFileName(videoFileName);
const videoKey = `videos/${id}/${sanitizedVideoName}`;
const mtvKey = `videos/${id}/${sanitizeFileName(mtv.name)}`;
await uploadObject(
mtvKey,
Buffer.from(mtvBuffer),
"application/octet-stream",
);
let tier: number | null | undefined;
let mapId: number | null | undefined;
let thumbnailKey: string | undefined;
const mapInfo = await getMapInfo(metadata.mapName);
mapId = mapInfo.mapId ?? undefined;
tier = mapInfo.tier;
if (mapInfo.thumbnailUrl) {
thumbnailKey = `videos/${id}/thumbnail.jpg`;
try {
const imgRes = await fetch(mapInfo.thumbnailUrl);
if (imgRes.ok) {
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
await uploadObject(thumbnailKey, imgBuf, "image/jpeg");
} else {
thumbnailKey = undefined;
}
} catch {
thumbnailKey = undefined;
}
}
const title = `${metadata.mapName} - ${metadata.playerName}`;
const entry: VideoEntry = {
id,
title,
description: "",
mapName: metadata.mapName,
playerName: metadata.playerName,
steamId: metadata.steamId,
runTime: metadata.runTime,
totalTicks: metadata.totalTicks,
tickInterval: metadata.tickInterval,
videoKey,
mtvKey,
thumbnailKey,
tier,
mapId,
jsonStats: metadata.jsonStats,
createdAt,
};
insertVideo(entry);
const presignedUrls = {
video: await getPresignedUploadUrl(videoKey, videoContentType),
};
return {
...entry,
presignedUrls,
videoUrl: getObjectUrl(videoKey),
mtvUrl: getObjectUrl(mtvKey),
thumbnailUrl: thumbnailKey ? getObjectUrl(thumbnailKey) : undefined,
};
})
.post("/:id/complete", async ({ params: { id } }) => {
const video = getVideoById(id);
if (!video) return status(404, { error: "Not found" });
return {
...video,
videoUrl: getObjectUrl(video.videoKey),
mtvUrl: getObjectUrl(video.mtvKey),
thumbnailUrl: video.thumbnailKey
? getObjectUrl(video.thumbnailKey)
: undefined,
};
})
.get("/:id", ({ params: { id } }) => {
const video = getVideoById(id);
if (!video) return status(404, { error: "Not found" });
return {
...video,
videoUrl: getObjectUrl(video.videoKey),
mtvUrl: getObjectUrl(video.mtvKey),
thumbnailUrl: video.thumbnailKey
? getObjectUrl(video.thumbnailKey)
: undefined,
};
})
.post("/:id/refresh-info", async ({ params: { id } }) => {
const video = getVideoById(id);
if (!video) return status(404, { error: "Not found" });
const mapInfo = await getMapInfo(video.mapName);
let thumbnailKey = video.thumbnailKey;
if (!thumbnailKey && mapInfo.thumbnailUrl) {
thumbnailKey = `videos/${video.id}/thumbnail.jpg`;
try {
const imgRes = await fetch(mapInfo.thumbnailUrl);
if (imgRes.ok) {
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
await uploadObject(thumbnailKey, imgBuf, "image/jpeg");
updateThumbnailKey(id, thumbnailKey);
} else {
thumbnailKey = undefined;
}
} catch {
thumbnailKey = undefined;
}
}
updateTier(id, mapInfo.tier);
return {
success: true,
tier: mapInfo.tier,
thumbnailUrl: thumbnailKey ? getObjectUrl(thumbnailKey) : undefined,
};
})
.post("/:id/refresh-thumbnail", async ({ params: { id } }) => {
const video = getVideoById(id);
if (!video) return status(404, { error: "Not found" });
const mapInfo = await getMapInfo(video.mapName);
if (!mapInfo.thumbnailUrl) {
return status(404, {
error: `No thumbnail found on Momentum Mod for ${video.mapName}`,
});
}
const thumbnailKey = `videos/${video.id}/thumbnail.jpg`;
try {
const imgRes = await fetch(mapInfo.thumbnailUrl);
if (!imgRes.ok)
return status(502, { error: "Failed to download thumbnail" });
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
await uploadObject(thumbnailKey, imgBuf, "image/jpeg");
} catch {
return status(502, { error: "Failed to download thumbnail" });
}
updateThumbnailKey(id, thumbnailKey);
updateTier(id, mapInfo.tier);
return { success: true, thumbnailUrl: getObjectUrl(thumbnailKey) };
})
.delete("/:id", async ({ params: { id } }) => {
const video = getVideoById(id);
if (!video) return status(404, { error: "Not found" });
const deletes = [
deleteObject(video.videoKey),
deleteObject(video.mtvKey),
];
if (video.thumbnailKey) deletes.push(deleteObject(video.thumbnailKey));
await Promise.all(deletes);
deleteVideoById(id);
return { success: true };
});
+229
View File
@@ -0,0 +1,229 @@
import { Database } from "bun:sqlite";
import { mkdirSync } from "fs";
import { dirname } from "path";
import type { VideoEntry, VideoListItem, PreviousPB } from "../types/video";
const DB_PATH = process.env.DB_PATH || "./data/surf.db";
let db: Database | null = null;
export function initDb(): void {
if (db) return;
mkdirSync(dirname(DB_PATH), { recursive: true });
db = new Database(DB_PATH, { create: true });
db.exec("PRAGMA journal_mode = WAL");
db.exec("PRAGMA strict_mode = ON");
db.exec(`
CREATE TABLE IF NOT EXISTS videos (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
map_name TEXT NOT NULL,
player_name TEXT NOT NULL,
steam_id TEXT NOT NULL,
run_time REAL NOT NULL,
total_ticks INTEGER NOT NULL,
tick_interval REAL NOT NULL,
video_key TEXT NOT NULL,
mtv_key TEXT NOT NULL,
thumbnail_key TEXT,
tier INTEGER,
json_stats TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_videos_map ON videos(map_name);
CREATE INDEX IF NOT EXISTS idx_videos_created ON videos(created_at DESC);
`);
const columns = db.prepare("PRAGMA table_info(videos)").all() as {
name: string;
}[];
if (!columns.some((col) => col.name === "tier")) {
db.exec("ALTER TABLE videos ADD COLUMN tier INTEGER");
}
if (!columns.some((col) => col.name === "map_id")) {
db.exec("ALTER TABLE videos ADD COLUMN map_id INTEGER");
}
if (!columns.some((col) => col.name === "rank")) {
db.exec("ALTER TABLE videos ADD COLUMN rank INTEGER");
}
if (!columns.some((col) => col.name === "rank_updated_at")) {
db.exec("ALTER TABLE videos ADD COLUMN rank_updated_at TEXT");
}
if (!columns.some((col) => col.name === "previous_pbs")) {
db.exec("ALTER TABLE videos ADD COLUMN previous_pbs TEXT");
}
}
function getDb(): Database {
if (!db) initDb();
return db!;
}
export function insertVideo(entry: VideoEntry): void {
const d = getDb();
d.prepare(
`INSERT INTO videos (id, title, description, map_name, player_name, steam_id, run_time, total_ticks, tick_interval, video_key, mtv_key, thumbnail_key, tier, map_id, json_stats, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
entry.id,
entry.title,
entry.description,
entry.mapName,
entry.playerName,
entry.steamId,
entry.runTime,
entry.totalTicks,
entry.tickInterval,
entry.videoKey,
entry.mtvKey,
entry.thumbnailKey ?? null,
entry.tier ?? null,
entry.mapId ?? null,
entry.jsonStats ?? null,
entry.createdAt,
);
}
export function getAllVideos(): VideoListItem[] {
const d = getDb();
return d
.prepare(
`SELECT id, title, map_name, player_name, run_time, tier, thumbnail_key, created_at
FROM videos ORDER BY created_at DESC`,
)
.all()
.map(rowToListItem);
}
export function getVideoById(id: string): VideoEntry | null {
const d = getDb();
const row = d.prepare(`SELECT * FROM videos WHERE id = ?`).get(id);
return row ? rowToEntry(row as any) : null;
}
export function deleteVideoById(id: string): boolean {
const d = getDb();
const result = d.prepare(`DELETE FROM videos WHERE id = ?`).run(id);
return result.changes > 0;
}
function rowToListItem(row: any): VideoListItem {
return {
id: row.id,
title: row.title,
mapName: row.map_name,
playerName: row.player_name,
runTime: row.run_time,
tier: row.tier ?? undefined,
thumbnailUrl: row.thumbnail_key || undefined,
createdAt: row.created_at,
previousPbs: row.previous_pbs
? JSON.parse(row.previous_pbs)
: undefined,
};
}
function rowToEntry(row: any): VideoEntry {
return {
id: row.id,
title: row.title,
description: row.description,
mapName: row.map_name,
playerName: row.player_name,
steamId: row.steam_id,
runTime: row.run_time,
totalTicks: row.total_ticks,
tickInterval: row.tick_interval,
videoKey: row.video_key,
mtvKey: row.mtv_key,
thumbnailKey: row.thumbnail_key ?? undefined,
tier: row.tier ?? undefined,
mapId: row.map_id ?? undefined,
jsonStats: row.json_stats ?? undefined,
createdAt: row.created_at,
previousPbs: row.previous_pbs
? JSON.parse(row.previous_pbs)
: undefined,
};
}
export function updateThumbnailKey(
id: string,
thumbnailKey: string | null,
): boolean {
const d = getDb();
const result = d
.prepare("UPDATE videos SET thumbnail_key = ? WHERE id = ?")
.run(thumbnailKey, id);
return result.changes > 0;
}
export function getVideoByMapAndPlayer(
mapName: string,
steamId: string,
): VideoEntry | null {
const d = getDb();
const row = d
.prepare(`SELECT * FROM videos WHERE map_name = ? AND steam_id = ?`)
.get(mapName, steamId);
return row ? rowToEntry(row as any) : null;
}
export function updateVideoPB(
id: string,
entry: VideoEntry,
previousPbs: PreviousPB[],
): void {
const d = getDb();
d.prepare(
`UPDATE videos SET
title = ?,
description = ?,
map_name = ?,
player_name = ?,
steam_id = ?,
run_time = ?,
total_ticks = ?,
tick_interval = ?,
video_key = ?,
mtv_key = ?,
thumbnail_key = ?,
tier = ?,
map_id = ?,
json_stats = ?,
created_at = ?,
previous_pbs = ?
WHERE id = ?`,
).run(
entry.title,
entry.description,
entry.mapName,
entry.playerName,
entry.steamId,
entry.runTime,
entry.totalTicks,
entry.tickInterval,
entry.videoKey,
entry.mtvKey,
entry.thumbnailKey ?? null,
entry.tier ?? null,
entry.mapId ?? null,
entry.jsonStats ?? null,
entry.createdAt,
JSON.stringify(previousPbs),
id,
);
}
export function updateTier(id: string, tier: number | null): boolean {
const d = getDb();
const result = d
.prepare("UPDATE videos SET tier = ? WHERE id = ?")
.run(tier, id);
return result.changes > 0;
}
+114
View File
@@ -0,0 +1,114 @@
const MOMENTUM_API = "https://api.momentum-mod.org/v1";
interface MomentumMapThumbnail {
id: string;
small: string;
medium: string;
large: string;
xl: string;
}
interface MomentumMapLeaderboard {
gamemode: number;
trackType: number;
trackNum: number;
style: number;
tier: number | null;
tags: number[];
type: number;
linear: boolean | null;
}
interface MomentumMapInfo {
description: string;
youtubeID: string;
creationDate: string;
approvedDate: string;
requiredGames: number[];
}
interface MomentumMap {
id: number;
name: string;
thumbnail: MomentumMapThumbnail | null;
images: MomentumMapThumbnail[];
info: MomentumMapInfo | null;
leaderboards: MomentumMapLeaderboard[];
}
interface MomentumMapsResponse {
totalCount: number;
returnCount: number;
data: MomentumMap[];
}
export interface MapInfo {
mapId: number | null;
thumbnailUrl: string | null;
tier: number | null;
mapDate: string | null;
}
const mapInfoCache = new Map<string, MapInfo>();
function extractSurfTier(
leaderboards: MomentumMapLeaderboard[],
): number | null {
const mainLeaderboard = leaderboards.find(
(lb) => lb.gamemode === 1 && lb.trackType === 0 && lb.style === 0,
);
return mainLeaderboard?.tier ?? null;
}
export async function getMapInfo(mapName: string): Promise<MapInfo> {
const cached = mapInfoCache.get(mapName);
if (cached) return cached;
try {
const url = `${MOMENTUM_API}/maps?search=${encodeURIComponent(mapName)}&take=1&expand=info,leaderboards`;
const res = await fetch(url);
if (!res.ok) {
const info: MapInfo = {
mapId: null,
thumbnailUrl: null,
tier: null,
mapDate: null,
};
mapInfoCache.set(mapName, info);
return info;
}
const json: MomentumMapsResponse = await res.json();
const map = json.data?.[0];
if (!map || map.name !== mapName) {
const info: MapInfo = {
mapId: null,
thumbnailUrl: null,
tier: null,
mapDate: null,
};
mapInfoCache.set(mapName, info);
return info;
}
const thumbnailUrl =
map.thumbnail?.small ?? map.images?.[0]?.small ?? null;
const tier = extractSurfTier(map.leaderboards ?? []);
const mapDate = map.info?.creationDate ?? null;
const info: MapInfo = { mapId: map.id, thumbnailUrl, tier, mapDate };
mapInfoCache.set(mapName, info);
return info;
} catch {
const info: MapInfo = {
mapId: null,
thumbnailUrl: null,
tier: null,
mapDate: null,
};
mapInfoCache.set(mapName, info);
return info;
}
}
export async function getMapThumbnail(mapName: string): Promise<string | null> {
const info = await getMapInfo(mapName);
return info.thumbnailUrl;
}
+97
View File
@@ -0,0 +1,97 @@
import type { MtvHeader, MtvMetadata } from "../types/video";
const MTV_MAGIC = 0x56544d4d;
const HEADER_SIZE = 0xc3;
function readNullTerminatedString(buffer: ArrayBuffer, offset: number, maxLen: number): string {
const view = new Uint8Array(buffer, offset, maxLen);
let end = 0;
while (end < maxLen && view[end] !== 0) end++;
return new TextDecoder().decode(view.slice(0, end));
}
export function parseMtvHeader(buffer: ArrayBuffer): MtvHeader | null {
if (buffer.byteLength < HEADER_SIZE) return null;
const view = new DataView(buffer);
const magic = view.getUint32(0, true);
if (magic !== MTV_MAGIC) return null;
const version = view.getUint32(4, true);
const unknown08 = view.getFloat32(8, true);
const unknown0c = view.getUint32(0x0c, true);
const mapName = readNullTerminatedString(buffer, 0x10, 64);
const mapHash = readNullTerminatedString(buffer, 0x50, 41);
const unknown79 = view.getUint8(0x79);
const compression = view.getUint8(0x7a);
const tickInterval = view.getFloat32(0x7b, true);
const steamId = view.getBigUint64(0x7f, true);
const playerName = readNullTerminatedString(buffer, 0x87, 32);
const unknownA7 = view.getUint8(0xa7);
const unknownA8 = view.getUint8(0xa8);
const runTime = view.getFloat64(0xa9, true);
const totalTicks = view.getUint32(0xb1, true);
const seekEntryCount = view.getUint32(0xb5, true);
const baselineCount = view.getUint32(0xb9, true);
const entityTypeCount = view.getUint8(0xbd);
const tempEntityCount = view.getUint8(0xbe);
const stringTableCount = view.getUint8(0xbf);
return {
magic,
version,
unknown08,
unknown0c,
mapName,
mapHash,
unknown79,
compression,
tickInterval,
steamId,
playerName,
unknownA7,
unknownA8,
runTime,
totalTicks,
seekEntryCount,
baselineCount,
entityTypeCount,
tempEntityCount,
stringTableCount,
};
}
export function parseMtvFile(buffer: ArrayBuffer): MtvMetadata | null {
const header = parseMtvHeader(buffer);
if (!header) return null;
let jsonStats: string | undefined;
if (buffer.byteLength > HEADER_SIZE + 4) {
const view = new DataView(buffer);
const jsonLen = view.getUint32(HEADER_SIZE, true);
if (jsonLen > 0 && buffer.byteLength >= HEADER_SIZE + 4 + jsonLen) {
const jsonBytes = new Uint8Array(buffer, HEADER_SIZE + 4, jsonLen - 1);
jsonStats = new TextDecoder().decode(jsonBytes);
}
}
return {
mapName: header.mapName,
mapHash: header.mapHash,
playerName: header.playerName,
steamId: header.steamId.toString(),
runTime: header.runTime,
totalTicks: header.totalTicks,
tickInterval: header.tickInterval,
version: header.version,
jsonStats,
};
}
export function formatRunTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toFixed(3).padStart(6, "0")}`;
}
+82
View File
@@ -0,0 +1,82 @@
import {
S3Client,
PutObjectCommand,
DeleteObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const RUSTFS_ENDPOINT = process.env.RUSTFS_ENDPOINT || "http://localhost:9000";
const RUSTFS_ACCESS_KEY = process.env.RUSTFS_ACCESS_KEY || "minioadmin";
const RUSTFS_SECRET_KEY = process.env.RUSTFS_SECRET_KEY || "minioadmin";
const RUSTFS_BUCKET = process.env.RUSTFS_BUCKET || "surf";
const RUSTFS_PUBLIC = (process.env.RUSTFS_PUBLIC || "true") === "true";
let client: S3Client | null = null;
function getRustFsClient(): S3Client {
if (!client) {
client = new S3Client({
endpoint: RUSTFS_ENDPOINT,
region: "us-east-1",
credentials: {
accessKeyId: RUSTFS_ACCESS_KEY,
secretAccessKey: RUSTFS_SECRET_KEY,
},
forcePathStyle: true,
});
}
return client;
}
function getPublicUrl(key: string): string {
if (RUSTFS_PUBLIC) {
return `${RUSTFS_ENDPOINT}/${RUSTFS_BUCKET}/${key}`;
}
return "";
}
export function getObjectUrl(key: string): string {
const publicUrl = getPublicUrl(key);
if (publicUrl) return publicUrl;
return `/api/videos/file/${encodeURIComponent(key)}`;
}
export async function uploadObject(
key: string,
body: Buffer,
contentType: string,
): Promise<void> {
const s3 = getRustFsClient();
await s3.send(
new PutObjectCommand({
Bucket: RUSTFS_BUCKET,
Key: key,
Body: body,
ContentType: contentType,
}),
);
}
export async function deleteObject(key: string): Promise<void> {
const s3 = getRustFsClient();
await s3.send(
new DeleteObjectCommand({
Bucket: RUSTFS_BUCKET,
Key: key,
}),
);
}
export async function getPresignedUploadUrl(
key: string,
contentType: string,
expiresInSeconds = 3600,
): Promise<string> {
const s3 = getRustFsClient();
const command = new PutObjectCommand({
Bucket: RUSTFS_BUCKET,
Key: key,
ContentType: contentType,
});
return getSignedUrl(s3, command, { expiresIn: expiresInSeconds });
}
+71
View File
@@ -0,0 +1,71 @@
export interface MtvHeader {
magic: number;
version: number;
unknown08: number;
unknown0c: number;
mapName: string;
mapHash: string;
unknown79: number;
compression: number;
tickInterval: number;
steamId: bigint;
playerName: string;
unknownA7: number;
unknownA8: number;
runTime: number;
totalTicks: number;
seekEntryCount: number;
baselineCount: number;
entityTypeCount: number;
tempEntityCount: number;
stringTableCount: number;
}
export interface MtvMetadata {
mapName: string;
mapHash: string;
playerName: string;
steamId: string;
runTime: number;
totalTicks: number;
tickInterval: number;
version: number;
jsonStats?: string;
}
export interface PreviousPB {
runTime: number;
createdAt: string;
}
export interface VideoEntry {
id: string;
title: string;
description: string;
mapName: string;
playerName: string;
steamId: string;
runTime: number;
totalTicks: number;
tickInterval: number;
videoKey: string;
mtvKey: string;
thumbnailKey?: string;
tier?: number | null;
mapId?: number | null;
jsonStats?: string;
createdAt: string;
previousPbs?: PreviousPB[];
}
export interface VideoListItem {
id: string;
title: string;
mapName: string;
playerName: string;
runTime: number;
tier?: number | null;
thumbnailUrl?: string;
createdAt: string;
previousPbs?: PreviousPB[];
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"types": ["bun-types"],
},
"include": ["src"],
}