Added sharp and blurhash dependencies, implemented thumbnail generation and retrieval functionality in the file management system. Updated database schema to include thumbnail metadata and enhanced route handlers for thumbnail access.
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit

This commit is contained in:
Tom Butcher 2026-07-26 03:07:23 +01:00
parent 4db7ffff6a
commit 4231d548cc
7 changed files with 784 additions and 42 deletions

View File

@ -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": {

566
pnpm-lock.yaml generated
View File

@ -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

View File

@ -3,4 +3,5 @@ allowBuilds:
chromedriver: true
core-js: true
puppeteer: true
unrs-resolver: true
sharp: true
unrs-resolver: true

View File

@ -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: {

View File

@ -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 }
);

View File

@ -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);
});

View File

@ -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;