mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-25 08:27:59 +03:00
* fix: resize profile images on upload and add cache headers Profile pictures were stored at original upload size (up to 1 MB) but only ever displayed at 32–128 px, so every page load downloaded the full original image. Additionally, no Cache-Control headers were set anywhere in the pipeline, forcing a fresh round-trip through the API redirect and CDN on every page view. - Resize JPEG/PNG pfps to max 256×256 and banners to max 1920×1080 via sharp before storing; re-encode to WebP at 85% quality (GIFs skipped to preserve the animated-GIF easter egg) - Hash the resized buffer so the CDN path reflects what's on disk - Set ContentType and CacheControl: immutable on S3 PutObject so CDN edges and browsers cache the objects indefinitely (safe because paths are content-addressed via SHA-256) - Add Cache-Control: public, max-age=3600, stale-while-revalidate=86400 to the GET /pfp and GET /banner redirect responses so browsers cache the userId→CDN-URL mapping and skip the API hop on repeat visits Fixes #1553 * chore: apply biome formatting * chore: generate ImportTimestop kanel type * feat: add backfill-media script to migrate existing profile images One-off script that reads every user's existing pfp and banner from S3, runs them through the same sharp resize/WebP pipeline as new uploads, stores the result under the new content-addressed key with immutable cache headers, and updates custom_pfp_location/custom_banner_location in the DB. Features: - --dry-run flag to preview changes without writing anything - Skips objects already at optimal size (idempotent) - Gracefully handles missing S3 objects (logs + skips) - Summary stats at the end - Reads S3 config from CLI args or TACHI_CDN_SAVE_LOCATION_* env vars * chore: apply biome formatting to backfill-media * fix: resize animated GIFs to animated WebP via sharp animated:true * test: rewrite change-pfp/banner tests as real S3 + sharp integration Remove the CDN mock entirely. Tests now pass real image buffers through sharp and verify the stored S3 object via CDNRetrieve: - PNG/JPEG input → assert stored object is WebP, dimensions ≤256×256, smaller than the original - GIF input → assert stored object is WebP - Hash returned from the action == SHA256 of the stored WebP bytes (not the original upload) - Delete tests upload a real pfp then delete it and verify CDNRetrieve throws afterwards Also move change-pfp.test.ts from the isolated vitest project back to the default pool now that it no longer uses vi.mock. * chore: apply biome formatting to test file * fix: use POSTGRES_TEST_HOST env var in bot vitest setup Both vitest.globalSetup.ts and vitest.setup.ts hardcoded 'tachi-postgres' (the dev server), but just bot-db-test-template-reset creates the template on 'tachi-postgres-test' (the tmpfs test server). Workers then failed to clone the template because they were looking on the wrong host. Mirrors the pattern already used in the server package: process.env.POSTGRES_TEST_HOST ?? 'tachi-postgres-test' * fix: update pfp router tests for WebP resize pipeline The PUT tests were comparing the stored S3 bytes directly to the original upload buffer. Since we now resize and re-encode to WebP, this always fails. Also, following the mockApi redirect to fetch the bytes added a slow supertest round-trip. Replace with: read custom_pfp_location from the DB after PUT, fetch the stored object directly via CDNRetrieve, and assert it is smaller than the original (non-deterministic WebP compression makes exact byte comparison inappropriate). * fix: update banner router tests for WebP resize pipeline * fix: tests * fix: ci
202 lines
5.4 KiB
JSON
202 lines
5.4 KiB
JSON
{
|
|
"name": "tachi",
|
|
"version": "3.0.0",
|
|
"private": true,
|
|
"description": "The score tracker.",
|
|
"workspaces": [
|
|
"typescript/*",
|
|
"typescript-vendor/*"
|
|
],
|
|
"tsMachete": {
|
|
"excludeGlobal": [
|
|
"@types/bun",
|
|
"@typescript/native-preview",
|
|
"@vitest/coverage-v8",
|
|
"typescript"
|
|
],
|
|
"exclude": {
|
|
"tachi-client": [
|
|
"@types/history",
|
|
"bootstrap-scss",
|
|
"sass"
|
|
],
|
|
"tachi-db": [
|
|
"@types/express",
|
|
"@types/node",
|
|
"@types/node-fetch"
|
|
],
|
|
"eslint-config-tachi": [
|
|
"@typescript-eslint/eslint-plugin",
|
|
"@typescript-eslint/parser",
|
|
"eslint-import-resolver-typescript",
|
|
"@types/eslint__js"
|
|
]
|
|
}
|
|
},
|
|
"author": "zk",
|
|
"devDependencies": {
|
|
"@biomejs/biome": "^2.0.6",
|
|
"@bufbuild/buf": "^1.66.1",
|
|
"@bufbuild/protoc-gen-es": "^2.11.0",
|
|
"@connectrpc/protoc-gen-connect-es": "^1.7.0",
|
|
"@types/node": "20.19.37",
|
|
"eslint": "^9.20.1",
|
|
"kanel": "^3.16.1",
|
|
"kanel-kysely": "^0.8.0",
|
|
"typescript": "5.9.3"
|
|
},
|
|
"catalog": {
|
|
"@aws-sdk/client-s3": "3.49.0",
|
|
"@discordjs/builders": "0.5.0",
|
|
"@discordjs/rest": "0.1.0-canary.0",
|
|
"@eslint-community/eslint-utils": "^4.9.1",
|
|
"@eslint-community/regexpp": "^4.12.2",
|
|
"@eslint/js": "^9.20.0",
|
|
"@fortawesome/fontawesome-free": "5.13.0",
|
|
"@fullcalendar/core": "^6.0.0",
|
|
"@fullcalendar/daygrid": "^6.0.0",
|
|
"@fullcalendar/react": "^6.0.0",
|
|
"@nivo/bar": "0.79.1",
|
|
"@nivo/core": "0.79.0",
|
|
"@nivo/line": "0.79.1",
|
|
"@octokit/app": "^16.1.2",
|
|
"@octokit/webhooks-types": "7.5.1",
|
|
"@types/bcryptjs": "^2.4.2",
|
|
"@types/bun": "1.3.14",
|
|
"@types/eslint__js": "8.42.3",
|
|
"@types/express": "4.17.13",
|
|
"@types/express-serve-static-core": "4.17.29",
|
|
"@types/express-session": "1.17.4",
|
|
"@types/history": "4.7.11",
|
|
"@types/humanize-duration": "^3.27.1",
|
|
"@types/lodash": "4.14.172",
|
|
"@types/lodash.get": "^4.4.7",
|
|
"@types/luxon": "^2.3.1",
|
|
"@types/multer": "1.4.7",
|
|
"@types/natural-compare": "^1.4.1",
|
|
"@types/node": "*",
|
|
"@types/node-fetch": "2.5.12",
|
|
"@types/nodemailer": "^6.4.4",
|
|
"@types/pg": "^8.11.11",
|
|
"@types/react": "17.0.13",
|
|
"@types/react-dom": "17.0.8",
|
|
"@types/react-google-recaptcha": "2.1.2",
|
|
"@types/react-router-bootstrap": "0.26.1",
|
|
"@types/react-router-dom": "5.1.7",
|
|
"@types/supertest": "2.0.11",
|
|
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
|
"@typescript-eslint/parser": "^4.27.0",
|
|
"@typescript-eslint/utils": "^8.24.1",
|
|
"@typescript/native-preview": "7.0.0-dev.20260309.1",
|
|
"@vitejs/plugin-react": "^6.0.1",
|
|
"@vitest/coverage-v8": "~3.2.0",
|
|
"bcryptjs": "2.4.3",
|
|
"binary-parser": "^2.3.0",
|
|
"bms-table-loader": "2.0.0",
|
|
"bootstrap-scss": "^5.3.0",
|
|
"chalk": "4",
|
|
"cheerio": "^1.0.0-rc.12",
|
|
"commander": "^8.2.0",
|
|
"csv-parse": "^5.0.3",
|
|
"deepmerge": "^4.2.2",
|
|
"discord-api-types": "0.22.0",
|
|
"discord.js": "13.6.0",
|
|
"dotenv": "16.0.0",
|
|
"encoding-japanese": "^1.0.30",
|
|
"eslint-config-prettier": "^9.1.0",
|
|
"eslint-import-resolver-typescript": "^3.7.0",
|
|
"eslint-plugin-import": "^2.31.0",
|
|
"eslint-plugin-unused-imports": "^4.1.4",
|
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
"eslint-plugin-perfectionist": "^3.9.1",
|
|
"eslint-plugin-react": "^7.37.4",
|
|
"eslint-plugin-react-hooks": "^5.1.0",
|
|
"eslint-plugin-react-refresh": "^0.4.14",
|
|
"express": "4.20.0",
|
|
"express-async-errors": "3.1.1",
|
|
"express-prom-bundle": "^7.0.0",
|
|
"express-session": "1.17.2",
|
|
"fast-json-stable-hash": "^1.0.3",
|
|
"fast-xml-parser": "^4.2.5",
|
|
"formik": "2.2.9",
|
|
"globals": "^15.15.0",
|
|
"helmet": "5.0.2",
|
|
"html-entities": "^2.3.2",
|
|
"humanize-duration": "^3.27.1",
|
|
"iconv-lite": "^0.6.3",
|
|
"kysely": "^0.27.5",
|
|
"lodash": "^4.17.23",
|
|
"lodash.get": "^4.4.2",
|
|
"luxon": "^2.3.1",
|
|
"multer": "2.0.1",
|
|
"nanoid": "3.1.23",
|
|
"natural-compare": "^1.4.0",
|
|
"node-cache": "5.1.2",
|
|
"node-fetch": "2.6.7",
|
|
"node-html-parser": "^5.3.3",
|
|
"node-mocks-http": "1.11.0",
|
|
"node-sass": "9.0.0",
|
|
"nodemailer": "7.0.11",
|
|
"pg": "^8.13.3",
|
|
"pino": "^10.3.1",
|
|
"pino-pretty": "^13.1.3",
|
|
"prettier": "3.2.5",
|
|
"pretty-bytes": "5.6.0",
|
|
"prom-client": "15.1.3",
|
|
"prudence": "0.10.0",
|
|
"react": "17.0.2",
|
|
"react-async": "10.0.1",
|
|
"react-bootstrap": "2.9.0-beta.0",
|
|
"react-dom": "17.0.2",
|
|
"react-google-recaptcha": "2.1.0",
|
|
"react-hot-toast": "2.0.0",
|
|
"react-markdown": "7.0.1",
|
|
"react-query": "3.18.1",
|
|
"react-router-bootstrap": "^0.25.0",
|
|
"react-router-dom": "5.1.2",
|
|
"react-select": "^5.6.1",
|
|
"rg-stats": "workspace:*",
|
|
"sharp": "^0.34.5",
|
|
"sass": "^1.77.0",
|
|
"supertest": "6.2.2",
|
|
"sync-fetch": "^0.3.1",
|
|
"typescript": "^4.3.4",
|
|
"typescript-eslint": "^8.24.1",
|
|
"vite": "^8.0.0",
|
|
"vite-plugin-html": "^3.2.0",
|
|
"vitest": "~3.2.0",
|
|
"zod": "^4.3.6",
|
|
"@types/tap": "^15.0.5",
|
|
"tap": "^15.1.6",
|
|
"@types/istanbul-lib-coverage": "^2.0.6",
|
|
"istanbul-lib-coverage": "^3.2.2",
|
|
"@codemirror/lang-sql": "^6.10.0",
|
|
"@codemirror/language": "^6.12.3",
|
|
"@lezer/highlight": "^1.2.3",
|
|
"@sqlite.org/sqlite-wasm": "^3.51.2-build9",
|
|
"codemirror": "^6.0.2",
|
|
"comlink": "^4.4.2",
|
|
"fast-json-patch": "^3.1.1",
|
|
"cron-parser": "5.0.2",
|
|
"@types/sql.js": "^1.4.11",
|
|
"sql.js": "^1.14.1"
|
|
},
|
|
"overrides": {
|
|
"@types/node": "20.19.37",
|
|
"@types/express-serve-static-core": "4.17.29",
|
|
"@types/express": "4.17.13",
|
|
"typescript": "5.9.3"
|
|
},
|
|
"patchedDependencies": {
|
|
"browserslist@4.21.4": "patches/browserslist@4.21.4.patch"
|
|
},
|
|
"engines": {
|
|
"bun": ">=1.2.0"
|
|
},
|
|
"dependencies": {
|
|
"@bufbuild/protobuf": "^2.11.0",
|
|
"@connectrpc/connect": "^2.1.1",
|
|
"@connectrpc/connect-node": "^2.1.1"
|
|
}
|
|
}
|