diff --git a/package.json b/package.json index 51324ac..ea91171 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "pg": "^8.16.3", "puppeteer": "^24.37.5", "redis": "^5.10.0", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "sharp": "^0.34.3", + "blurhash": "^2.0.5" }, "type": "module", "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa47af5..e55e27c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: bcrypt: specifier: ^6.0.0 version: 6.0.0 + blurhash: + specifier: ^2.0.5 + version: 2.0.5 body-parser: specifier: ^2.2.0 version: 2.2.2 @@ -101,6 +104,9 @@ importers: sequelize: specifier: ^6.37.7 version: 6.37.7(pg@8.18.0) + sharp: + specifier: ^0.34.3 + version: 0.34.5 devDependencies: '@babel/cli': specifier: ^7.28.3 @@ -1035,6 +1041,143 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1648,10 +1791,22 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.18: + resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==} + engines: {node: '>=12.0'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1771,8 +1926,8 @@ packages: axios@1.13.4: resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} - axios@1.13.6: - resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} b4a@1.8.0: resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} @@ -1878,7 +2033,11 @@ packages: basic-ftp@5.1.0: resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} engines: {node: '>=10.0.0'} - deprecated: Security vulnerability fixed in 5.2.0, please upgrade + deprecated: Security vulnerability fixed in 5.2.1, please upgrade + + basic-ftp@5.3.1: + resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + engines: {node: '>=10.0.0'} bcrypt@6.0.0: resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} @@ -1904,6 +2063,9 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + blurhash@2.0.5: + resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==} + bn.js@4.12.2: resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} @@ -2016,9 +2178,9 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - chromedriver@146.0.0: - resolution: {integrity: sha512-fDAbuEy+Dn9F/h8fphiQIUEyUDOTGlfjZHfI9dJZz75+ui/LIHqWzStQt87vpwA9oV3ut4C2W3flfvbn3KELFQ==} - engines: {node: '>=20'} + chromedriver@151.0.1: + resolution: {integrity: sha512-pjpIi8+CuyqI594OwtBsL63h3rW5RQU24+Bi4eDXKQ11VQzN+8J5Dmy52MQkm39UtC0wrDnEkZQwYjWVQ99CYQ==} + engines: {node: '>=22'} hasBin: true chromium-bidi@14.0.0: @@ -2170,6 +2332,10 @@ packages: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} + data-uri-to-buffer@8.0.0: + resolution: {integrity: sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==} + engines: {node: '>= 20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2250,6 +2416,12 @@ packages: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} + degenerator@7.0.1: + resolution: {integrity: sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==} + engines: {node: '>= 20'} + peerDependencies: + quickjs-wasi: ^2.2.0 + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2258,6 +2430,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -2691,6 +2867,15 @@ packages: debug: optional: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2796,6 +2981,10 @@ packages: resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} engines: {node: '>= 14'} + get-uri@8.0.1: + resolution: {integrity: sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==} + engines: {node: '>= 20'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2886,10 +3075,22 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http-proxy-agent@9.1.0: + resolution: {integrity: sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==} + engines: {node: '>= 20'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + https-proxy-agent@9.1.0: + resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==} + engines: {node: '>= 20'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2950,9 +3151,9 @@ packages: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} - ip-regex@4.3.0: - resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} - engines: {node: '>=8'} + ip-regex@2.1.0: + resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==} + engines: {node: '>=4'} ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} @@ -3091,8 +3292,8 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - is2@2.0.9: - resolution: {integrity: sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==} + is2@2.0.1: + resolution: {integrity: sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==} engines: {node: '>=v0.10.0'} isarray@1.0.0: @@ -3825,10 +4026,20 @@ packages: resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} engines: {node: '>= 14'} + pac-proxy-agent@9.1.0: + resolution: {integrity: sha512-1aU+1mpj3DrQPfo3gh+3Gap3G5x+axnMx1P/y0ZF2ch7kb2meyOCAH8K2k9d27ROsTE7TnAerzxqF9aon2jqnA==} + engines: {node: '>= 20'} + pac-resolver@7.0.1: resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} engines: {node: '>= 14'} + pac-resolver@9.0.1: + resolution: {integrity: sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==} + engines: {node: '>= 20'} + peerDependencies: + quickjs-wasi: ^2.2.0 + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -4003,15 +4214,29 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-agent-negotiate@1.1.0: + resolution: {integrity: sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==} + engines: {node: '>= 20'} + peerDependencies: + kerberos: ^2.0.0 + peerDependenciesMeta: + kerberos: + optional: true + proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} + proxy-agent@8.0.2: + resolution: {integrity: sha512-idLLRewuemWd7GH/BDJzGiB0dWGfT2SQs3jy6NtZtGWU9uPTTSdeC1/cdbqLwgzhfv027daGFuXX426e2Eg20A==} + engines: {node: '>= 20'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - proxy-from-env@2.0.0: - resolution: {integrity: sha512-h2lD3OfRraP3R51rNFKIE8nX+qoLr1mE74X91YhVxtDbt+OD6ntoNZv56+JgI4RCdtwQ5eexsOk1KdOQDfvPCQ==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -4042,6 +4267,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quickjs-wasi@2.2.0: + resolution: {integrity: sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==} + random-bytes@1.0.0: resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} engines: {node: '>= 0.8'} @@ -4281,6 +4509,10 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -4335,6 +4567,10 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + socks-proxy-agent@10.1.0: + resolution: {integrity: sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==} + engines: {node: '>= 20'} + socks-proxy-agent@8.0.5: resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} @@ -4497,8 +4733,8 @@ packages: tar-stream@3.1.8: resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} - tcp-port-used@1.0.2: - resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==} + tcp-port-used@1.0.3: + resolution: {integrity: sha512-4CEQ3qRJYo+mtEbJ+OoQu3dF4TDkwaO3RDVC4UzP5cpAOIUWwuwPjD7sdxDFFqsMUjsXVVYBMlg/boAaloThMA==} teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} @@ -6225,6 +6461,102 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -7037,8 +7369,21 @@ snapshots: acorn@8.15.0: {} + adm-zip@0.5.18: + optional: true + + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + optional: true + agent-base@7.1.4: {} + agent-base@9.0.0: + optional: true + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -7222,13 +7567,15 @@ snapshots: transitivePeerDependencies: - debug - axios@1.13.6: + axios@1.18.1: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 form-data: 4.0.5 - proxy-from-env: 1.1.0 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 transitivePeerDependencies: - debug + - supports-color optional: true b4a@1.8.0: {} @@ -7356,6 +7703,9 @@ snapshots: basic-ftp@5.1.0: {} + basic-ftp@5.3.1: + optional: true + bcrypt@6.0.0: dependencies: node-addon-api: 8.5.0 @@ -7380,6 +7730,8 @@ snapshots: bluebird@3.7.2: {} + blurhash@2.0.5: {} + bn.js@4.12.2: {} body-parser@2.2.2: @@ -7444,7 +7796,7 @@ snapshots: builtins@5.1.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 busboy@1.6.0: dependencies: @@ -7502,17 +7854,18 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chromedriver@146.0.0: + chromedriver@151.0.1: dependencies: '@testim/chrome-version': 1.1.4 - axios: 1.13.6 + adm-zip: 0.5.18 + axios: 1.18.1 compare-versions: 6.1.1 - extract-zip: 2.0.1 - proxy-agent: 6.5.0 - proxy-from-env: 2.0.0 - tcp-port-used: 1.0.2 + proxy-agent: 8.0.2 + proxy-from-env: 2.1.0 + tcp-port-used: 1.0.3 transitivePeerDependencies: - debug + - kerberos - supports-color optional: true @@ -7652,6 +8005,9 @@ snapshots: data-uri-to-buffer@6.0.2: {} + data-uri-to-buffer@8.0.0: + optional: true + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -7717,10 +8073,20 @@ snapshots: escodegen: 2.1.0 esprima: 4.0.1 + degenerator@7.0.1(quickjs-wasi@2.2.0): + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + quickjs-wasi: 2.2.0 + optional: true + delayed-stream@1.0.0: {} depd@2.0.0: {} + detect-libc@2.1.2: {} + detect-newline@3.1.0: {} devtools-protocol@0.0.1566079: {} @@ -7765,7 +8131,7 @@ snapshots: '@one-ini/wasm': 0.1.1 commander: 10.0.1 minimatch: 9.0.1 - semver: 7.7.3 + semver: 7.7.4 ee-first@1.1.1: {} @@ -7994,7 +8360,7 @@ snapshots: is-core-module: 2.16.1 minimatch: 3.1.2 resolve: 1.22.11 - semver: 7.7.3 + semver: 7.7.4 eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.2))(eslint@9.39.2)(prettier@3.8.1): dependencies: @@ -8358,6 +8724,9 @@ snapshots: follow-redirects@1.15.11: {} + follow-redirects@1.16.0: + optional: true + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -8475,6 +8844,15 @@ snapshots: transitivePeerDependencies: - supports-color + get-uri@8.0.1: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 8.0.0 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + optional: true + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -8574,6 +8952,24 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-agent@9.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + proxy-agent-negotiate: 1.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + optional: true + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -8581,6 +8977,16 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@9.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + proxy-agent-negotiate: 1.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + optional: true + human-signals@2.1.0: {} i@0.3.7: {} @@ -8628,7 +9034,7 @@ snapshots: ip-address@10.1.0: {} - ip-regex@4.3.0: + ip-regex@2.1.0: optional: true ipaddr.js@1.9.1: {} @@ -8764,10 +9170,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is2@2.0.9: + is2@2.0.1: dependencies: deep-is: 0.1.4 - ip-regex: 4.3.0 + ip-regex: 2.1.0 is-url: 1.2.4 optional: true @@ -8787,7 +9193,7 @@ snapshots: '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.3 + semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -9088,7 +9494,7 @@ snapshots: jest-message-util: 30.2.0 jest-util: 30.2.0 pretty-format: 30.2.0 - semver: 7.7.3 + semver: 7.7.4 synckit: 0.11.12 transitivePeerDependencies: - supports-color @@ -9242,9 +9648,10 @@ snapshots: dependencies: jwk-to-pem: 2.0.7 optionalDependencies: - chromedriver: 146.0.0 + chromedriver: 151.0.1 transitivePeerDependencies: - debug + - kerberos - supports-color keyv@4.5.4: @@ -9364,7 +9771,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 makeerror@1.0.12: dependencies: @@ -9678,11 +10085,33 @@ snapshots: transitivePeerDependencies: - supports-color + pac-proxy-agent@9.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + get-uri: 8.0.1 + http-proxy-agent: 9.1.0 + https-proxy-agent: 9.1.0 + pac-resolver: 9.0.1(quickjs-wasi@2.2.0) + quickjs-wasi: 2.2.0 + socks-proxy-agent: 10.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + optional: true + pac-resolver@7.0.1: dependencies: degenerator: 5.0.1 netmask: 2.0.2 + pac-resolver@9.0.1(quickjs-wasi@2.2.0): + dependencies: + degenerator: 7.0.1(quickjs-wasi@2.2.0) + netmask: 2.0.2 + quickjs-wasi: 2.2.0 + optional: true + package-json-from-dist@1.0.1: {} pako@1.0.11: {} @@ -9827,6 +10256,9 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-agent-negotiate@1.1.0: + optional: true + proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 @@ -9840,9 +10272,24 @@ snapshots: transitivePeerDependencies: - supports-color + proxy-agent@8.0.2: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + http-proxy-agent: 9.1.0 + https-proxy-agent: 9.1.0 + lru-cache: 7.18.3 + pac-proxy-agent: 9.1.0 + proxy-from-env: 2.1.0 + socks-proxy-agent: 10.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + optional: true + proxy-from-env@1.1.0: {} - proxy-from-env@2.0.0: + proxy-from-env@2.1.0: optional: true pstree.remy@1.1.8: {} @@ -9896,6 +10343,9 @@ snapshots: queue-microtask@1.2.3: {} + quickjs-wasi@2.2.0: + optional: true + random-bytes@1.0.0: {} range-parser@1.2.1: {} @@ -10169,6 +10619,37 @@ snapshots: dependencies: kind-of: 6.0.3 + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -10213,7 +10694,7 @@ snapshots: simple-update-notifier@2.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 slash@2.0.0: {} @@ -10221,6 +10702,15 @@ snapshots: smart-buffer@4.2.0: {} + socks-proxy-agent@10.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + optional: true + socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 @@ -10466,10 +10956,10 @@ snapshots: - bare-buffer - react-native-b4a - tcp-port-used@1.0.2: + tcp-port-used@1.0.3: dependencies: debug: 4.3.1 - is2: 2.0.9 + is2: 2.0.1 transitivePeerDependencies: - supports-color optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9a0089f..cf899ff 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,5 @@ allowBuilds: chromedriver: true core-js: true puppeteer: true - unrs-resolver: true \ No newline at end of file + sharp: true + unrs-resolver: true diff --git a/src/database/database.js b/src/database/database.js index e41f602..98d2343 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -2,6 +2,9 @@ import config from '../config.js'; import { fileModel } from './schemas/management/file.schema.js'; import _ from 'lodash'; import log4js from 'log4js'; +import sharp from 'sharp'; +import { encode as encodeBlurHash } from 'blurhash'; +import { uploadFile, deleteFile, downloadFile, BUCKETS } from './ceph.js'; import { deleteAuditLog, distributeDelete, @@ -966,6 +969,200 @@ export const newObject = async ({ model, newData, user = null }, distributeChang } }; +const FILE_THUMBNAIL_SIZES = [64, 128, 265]; +const FILE_THUMBNAIL_CACHE_TTL_SECONDS = 5 * 60; + +function getFileThumbnailCacheKey(fileId, size) { + return `thumbnails:${size}:${fileId}`; +} + +function getFileThumbnailS3Key(fileId, size) { + return `thumbnails/${size}/${fileId}.jpg`; +} + +async function streamToBuffer(body) { + if (Buffer.isBuffer(body)) { + return body; + } + + if (body && typeof body.transformToByteArray === 'function') { + return Buffer.from(await body.transformToByteArray()); + } + + const chunks = []; + for await (const chunk of body) { + chunks.push(chunk); + } + + return Buffer.concat(chunks); +} + +export async function getThumbnail({ fileId, size, file }) { + const parsedSize = Number(size); + + if (!FILE_THUMBNAIL_SIZES.includes(parsedSize)) { + return { error: 'Invalid thumbnail size.', code: 400 }; + } + + const normalizedFileId = fileId.toString(); + const cacheKey = getFileThumbnailCacheKey(normalizedFileId, parsedSize); + + try { + const cached = await redisServer.getKey(cacheKey); + if (cached) { + return { + buffer: Buffer.from(cached, 'base64'), + contentType: 'image/jpeg', + }; + } + + const s3Key = + file?.metaData?.thumbnails?.[parsedSize] || + file?.metaData?.thumbnails?.[String(parsedSize)] || + getFileThumbnailS3Key(normalizedFileId, parsedSize); + + const body = await downloadFile(BUCKETS.FILES, s3Key); + const buffer = await streamToBuffer(body); + + await redisServer.setKey( + cacheKey, + buffer.toString('base64'), + FILE_THUMBNAIL_CACHE_TTL_SECONDS + ); + + return { + buffer, + contentType: 'image/jpeg', + }; + } catch (error) { + if (error.message === 'File not found') { + return { error: 'Thumbnail not found.', code: 404 }; + } + + logger.error( + `Error getting thumbnail for file ${normalizedFileId} (size ${parsedSize}):`, + error + ); + return { error: error.message, code: 500 }; + } +} + +async function deleteFileThumbnails(fileObject) { + if (!fileObject?._id) { + return; + } + + const hasStoredThumbnails = + fileObject.hasThumbnails || + fileObject.metaData?.thumbnails || + fileObject.type?.startsWith('image/'); + + if (!hasStoredThumbnails) { + return; + } + + const fileId = fileObject._id.toString(); + const s3Keys = new Set(FILE_THUMBNAIL_SIZES.map((size) => getFileThumbnailS3Key(fileId, size))); + + if (fileObject.metaData?.thumbnails) { + for (const key of Object.values(fileObject.metaData.thumbnails)) { + if (typeof key === 'string') { + s3Keys.add(key); + } + } + } + + for (const s3Key of s3Keys) { + try { + await deleteFile(BUCKETS.FILES, s3Key); + logger.debug(`Deleted thumbnail from S3: ${s3Key}`); + } catch (error) { + logger.warn(`Failed to delete thumbnail from S3 (${s3Key}):`, error.message); + } + } + + for (const size of FILE_THUMBNAIL_SIZES) { + try { + await redisServer.deleteKey(getFileThumbnailCacheKey(fileId, size)); + logger.debug(`Deleted thumbnail from Redis: ${getFileThumbnailCacheKey(fileId, size)}`); + } catch (error) { + logger.warn( + `Failed to delete thumbnail from Redis (${getFileThumbnailCacheKey(fileId, size)}):`, + error.message + ); + } + } +} + +export async function createFileThumbnails(fileData, fileObject) { + try { + if (!fileObject?.type?.startsWith('image/') || !fileData || !fileObject?._id) { + return; + } + + const fileId = fileObject._id.toString(); + const image = sharp(fileData, { failOn: 'none' }); + const imageMetadata = await image.metadata(); + + if (!imageMetadata.width || !imageMetadata.height) { + return; + } + + const blurhashSource = await sharp(fileData) + .rotate() + .resize(32, 32, { fit: 'inside' }) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + + const blurHash = encodeBlurHash( + new Uint8ClampedArray(blurhashSource.data), + blurhashSource.info.width, + blurhashSource.info.height, + 4, + 4 + ); + + const thumbnails = {}; + + for (const size of FILE_THUMBNAIL_SIZES) { + const thumbnailBuffer = await sharp(fileData) + .rotate() + .resize(size, size, { fit: 'inside', withoutEnlargement: true }) + .jpeg({ quality: 85 }) + .toBuffer(); + + const s3Key = getFileThumbnailS3Key(fileId, size); + await uploadFile(BUCKETS.FILES, s3Key, thumbnailBuffer, 'image/jpeg', { + fileId, + thumbnailSize: String(size), + }); + + await redisServer.setKey( + getFileThumbnailCacheKey(fileId, size), + thumbnailBuffer.toString('base64'), + FILE_THUMBNAIL_CACHE_TTL_SECONDS + ); + + thumbnails[size] = s3Key; + } + + await editObject({ + model: fileModel, + id: fileObject._id, + updateData: { + hasThumbnails: true, + metaData: { + ...(fileObject.metaData || {}), + blurHash, + }, + }, + }); + } catch (error) { + logger.error('Error creating file thumbnails:', error); + } +} + // Reusable function to delete an object by ID, with audit logging and distribution export const deleteObject = async ( { model, id, user = null, checkUnused = false }, @@ -997,6 +1194,7 @@ export const deleteObject = async ( } const deleted = expandObjectIds(result.toObject()); + // Audit log the deletion await deleteAuditLog(deleted, id.toString(), parentType, user); @@ -1089,8 +1287,6 @@ export const flushFile = async ({ id, user }) => { // Try to delete from Ceph storage if it exists if (file.extension) { try { - const { deleteFile } = await import('./ceph.js'); - const { BUCKETS } = await import('./ceph.js'); const cephKey = `files/${file._id}${file.extension}`; await deleteFile(BUCKETS.FILES, cephKey); @@ -1101,6 +1297,8 @@ export const flushFile = async ({ id, user }) => { } } + await deleteFileThumbnails(file); + const result = { success: true, deletedFile: { diff --git a/src/database/schemas/management/file.schema.js b/src/database/schemas/management/file.schema.js index 058b607..eab8e34 100644 --- a/src/database/schemas/management/file.schema.js +++ b/src/database/schemas/management/file.schema.js @@ -9,6 +9,7 @@ const fileSchema = new mongoose.Schema( extension: { required: true, type: String }, size: { required: false, type: Number }, metaData: { required: false, type: Object }, + hasThumbnails: { required: false, type: Boolean, default: false }, }, { timestamps: true } ); diff --git a/src/routes/management/files.js b/src/routes/management/files.js index af9182a..64f71fc 100644 --- a/src/routes/management/files.js +++ b/src/routes/management/files.js @@ -7,6 +7,7 @@ import { listFilesRouteHandler, getFileRouteHandler, getFileContentRouteHandler, + getFileThumbnailRouteHandler, editFileRouteHandler, newFileRouteHandler, flushFileRouteHandler, @@ -73,6 +74,10 @@ router.get('/:id/content', isAuthenticated, (req, res) => { getFileContentRouteHandler(req, res); }); +router.get('/:id/thumbnail', isAuthenticated, (req, res) => { + getFileThumbnailRouteHandler(req, res); +}); + router.put('/:id', isAuthenticated, async (req, res) => { editFileRouteHandler(req, res); }); diff --git a/src/services/management/files.js b/src/services/management/files.js index 53d6876..fa91def 100644 --- a/src/services/management/files.js +++ b/src/services/management/files.js @@ -17,6 +17,8 @@ import { getModelHistory, searchObjects, getPropertyValues, + createFileThumbnails, + getThumbnail, } from '../../database/database.js'; import { uploadFile, @@ -225,6 +227,8 @@ export const newFileRouteHandler = async (req, res) => { uploadedBy: req.user?.username || 'unknown', }); + createFileThumbnails(req.file.buffer, created); + // Do not update DB with Ceph location. Return created DB record. logger.debug(`New file with ID: ${created._id} created and uploaded to Ceph`); @@ -307,6 +311,47 @@ function checkFileType(file, cb) { } } +export const getFileThumbnailRouteHandler = async (req, res) => { + const id = req.params.id; + const { size } = req.query; + + logger.info(`File thumbnail requested: ${id} (size: ${size ?? 'unspecified'})`); + + if (!size) { + return res.status(400).send({ error: 'Thumbnail size is required.' }); + } + + try { + const file = await getObject({ + model: fileModel, + id, + }); + + if (!file) { + logger.warn(`File not found with supplied id.`); + return res.status(404).send({ error: 'File not found.' }); + } + + const result = await getThumbnail({ + fileId: id, + size, + file, + }); + + if (result.error) { + return res.status(result.code).send({ error: result.error }); + } + + res.set('Content-Type', result.contentType); + res.set('Cache-Control', 'private, max-age=300'); + res.set('Content-Length', String(result.buffer.length)); + return res.send(result.buffer); + } catch (error) { + logger.error('Error fetching file thumbnail:', error); + return res.status(500).send({ error: error.message }); + } +}; + export const getFileContentRouteHandler = async (req, res) => { const id = req.params.id; let bytesSent = 0;