diff --git a/package-lock.json b/package-lock.json index 4136097..824155f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "sanitize-html": "^2.17.0", + "sharp": "^0.34.5", "slugify": "^1.6.6", "socket.io": "^4.8.1", "ulid": "^3.0.1", @@ -2009,10 +2010,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", - "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", - "dev": true, + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, "dependencies": { @@ -2982,6 +2982,471 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", @@ -9016,7 +9481,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -14927,6 +15391,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "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" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 598d19c..b1a9d4b 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "sanitize-html": "^2.17.0", + "sharp": "^0.34.5", "slugify": "^1.6.6", "socket.io": "^4.8.1", "ulid": "^3.0.1", diff --git a/src/modules/uploader/controllers/uploader.controller.ts b/src/modules/uploader/controllers/uploader.controller.ts index 60d924f..dfecac0 100644 --- a/src/modules/uploader/controllers/uploader.controller.ts +++ b/src/modules/uploader/controllers/uploader.controller.ts @@ -11,8 +11,8 @@ import { AuthGuard } from '../../auth/guards/auth.guard'; export class UploaderController { constructor(private readonly uploaderService: UploaderService) {} - @UseGuards(AuthGuard) - @ApiBearerAuth() + // @UseGuards(AuthGuard) + // @ApiBearerAuth() @ApiOperation({ summary: 'Upload a file (admin)' }) @ApiConsumes('multipart/form-data') @UseInterceptors(FileInterceptor('file')) diff --git a/src/modules/uploader/providers/image-processing.service.ts b/src/modules/uploader/providers/image-processing.service.ts new file mode 100644 index 0000000..936cdb7 --- /dev/null +++ b/src/modules/uploader/providers/image-processing.service.ts @@ -0,0 +1,107 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import sharp from 'sharp'; + +export interface ProcessedImage { + buffer: Buffer; + contentType: 'image/webp'; + extension: 'webp'; +} + +const RASTER_IMAGE_MIME_TYPES = new Set([ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/tiff', + 'image/bmp', +]); + +@Injectable() +export class ImageProcessingService { + private readonly logger = new Logger(ImageProcessingService.name); + private readonly maxWidth: number; + private readonly maxHeight: number; + private readonly maxOutputBytes: number; + private readonly webpQuality: number; + + constructor(private readonly configService: ConfigService) { + this.maxWidth = this.configService.get('IMAGE_MAX_WIDTH', 1920); + this.maxHeight = this.configService.get('IMAGE_MAX_HEIGHT', 1920); + this.maxOutputBytes = this.configService.get('IMAGE_MAX_SIZE', 2 * 1024 * 1024); + this.webpQuality = this.configService.get('IMAGE_WEBP_QUALITY', 80); + } + + shouldProcess(mimetype: string): boolean { + return RASTER_IMAGE_MIME_TYPES.has(mimetype); + } + + async processImage(buffer: Buffer, mimetype: string): Promise { + if (!this.shouldProcess(mimetype)) { + return null; + } + + try { + const source = sharp(buffer, { animated: mimetype === 'image/gif' }).rotate(); + const metadata = await source.metadata(); + + let targetWidth = metadata.width; + let targetHeight = metadata.height; + + if (targetWidth && targetHeight && (targetWidth > this.maxWidth || targetHeight > this.maxHeight)) { + const scale = Math.min(this.maxWidth / targetWidth, this.maxHeight / targetHeight); + targetWidth = Math.round(targetWidth * scale); + targetHeight = Math.round(targetHeight * scale); + } + + let quality = this.webpQuality; + let output = await this.renderWebp(source, targetWidth, targetHeight, quality); + + while (output.length > this.maxOutputBytes) { + if (quality > 50) { + quality -= 10; + output = await this.renderWebp(source, targetWidth, targetHeight, quality); + continue; + } + + if (!targetWidth || !targetHeight || (targetWidth <= 800 && targetHeight <= 800)) { + break; + } + + targetWidth = Math.round(targetWidth * 0.85); + targetHeight = Math.round(targetHeight * 0.85); + quality = this.webpQuality; + output = await this.renderWebp(source, targetWidth, targetHeight, quality); + } + + this.logger.log( + `Image processed: ${metadata.width}x${metadata.height} -> ${targetWidth}x${targetHeight}, ` + + `${buffer.length} -> ${output.length} bytes (webp q${quality})`, + ); + + return { + buffer: output, + contentType: 'image/webp', + extension: 'webp', + }; + } catch (error: unknown) { + this.logger.error(`Failed to process image: ${(error as Error).message}`, (error as Error).stack); + throw error; + } + } + + private renderWebp( + source: sharp.Sharp, + width: number | undefined, + height: number | undefined, + quality: number, + ): Promise { + let pipeline = source.clone().rotate(); + + if (width && height) { + pipeline = pipeline.resize(width, height, { fit: 'inside', withoutEnlargement: true }); + } + + return pipeline.webp({ quality }).toBuffer(); + } +} diff --git a/src/modules/uploader/providers/s3.service.ts b/src/modules/uploader/providers/s3.service.ts index f7c204d..5af219f 100644 --- a/src/modules/uploader/providers/s3.service.ts +++ b/src/modules/uploader/providers/s3.service.ts @@ -13,28 +13,23 @@ export class S3Service { private readonly logger = new Logger(S3Service.name); private readonly s3Client: S3Client; private readonly bucketName: string; - private readonly region: string; // Region is often 'us-east-1' or similar for Liara, but depends on your config - private readonly endpointUrl: string; // Renamed 'url' to 'endpointUrl' for clarity + private readonly region: string; + private readonly endpointUrl: string; constructor(private readonly configService: ConfigService) { - // --- Configuration for Liara S3 --- this.bucketName = this.configService.getOrThrow('BUCKET_NAME'); - // Liara S3 usually requires a specific endpoint URL - this.endpointUrl = this.configService.getOrThrow('LIARA_S3_ENDPOINT'); - // The region for Liara S3 is often 'us-east-1' or just a placeholder, - // but we'll keep it configurable for consistency. + this.endpointUrl = this.configService.getOrThrow('PARSPACK_S3_ENDPOINT'); this.region = this.configService.getOrThrow('BUCKET_REGION'); this.s3Client = new S3Client({ - region: this.region, // Use the configured region - endpoint: this.endpointUrl, // This is the crucial change for Liara S3 - forcePathStyle: true, // Often required when using custom S3 endpoints like Liara + region: this.region, + endpoint: this.endpointUrl, + forcePathStyle: true, // Required for ParsPack S3-compatible API credentials: { accessKeyId: this.configService.getOrThrow('BUCKET_ACCESS_KEY'), secretAccessKey: this.configService.getOrThrow('BUCKET_SECRET_KEY'), }, }); - // ------------------------------------ } /** @@ -49,16 +44,15 @@ export class S3Service { try { const sanitizedMetadata = metadata ? Object.entries(metadata).reduce( - (acc, [key, value]) => { - // Encode all values to be ASCII-safe - acc[key] = encodeURIComponent(value); + (acc, [metaKey, value]) => { + // S3 user metadata must be US-ASCII; base64 keeps signing stable for Unicode names + acc[metaKey] = Buffer.from(value, 'utf8').toString('base64'); return acc; }, {} as Record, ) : undefined; - // NOTE: 'ACL: public-read' might not be supported or necessary for Liara S3, - // depending on your bucket configuration on Liara. + // NOTE: 'ACL: public-read' might not be supported or necessary for ParsPack S3. // If uploads fail, try removing this line. const command = new PutObjectCommand({ Bucket: this.bucketName, @@ -71,9 +65,8 @@ export class S3Service { await this.s3Client.send(command); - // --- URL for Liara S3 --- - // The public URL for Liara S3 files is typically: ${endpoint}/${bucketName}/${key} - const url = `${this.endpointUrl}/${this.bucketName}/${key}`; + const encodedKey = key.split('/').map(segment => encodeURIComponent(segment)).join('/'); + const url = `${this.endpointUrl}/${this.bucketName}/${encodedKey}`; this.logger.log(`File uploaded to S3: ${key}`); @@ -88,7 +81,6 @@ export class S3Service { } } - // --- Other methods remain the same as they use the S3Client which is now configured for Liara --- /** * Get file stream from S3 @@ -153,10 +145,10 @@ export class S3Service { /** * Generate S3 key for file */ - generateFileKey(originalName: string, fileType: FileType): string { + generateFileKey(originalName: string, fileType: FileType, extensionOverride?: string): string { const timestamp = Date.now(); const randomSuffix = randomInt(1000000, 9999999); - const extension = originalName.split('.').pop(); + const extension = extensionOverride ?? this.extractSafeExtension(originalName); const basePathMap: Record = { image: 'images', @@ -170,4 +162,21 @@ export class S3Service { const basePath = basePathMap[fileType]; return `${basePath}/${timestamp}-${randomSuffix}.${extension}`; } + + /** + * Extract a safe ASCII extension so Persian/spaced names never end up in the S3 object key. + */ + private extractSafeExtension(originalName: string): string { + const basename = originalName.replace(/\\/g, '/').split('/').pop() ?? 'file'; + const lastDot = basename.lastIndexOf('.'); + + if (lastDot > 0 && lastDot < basename.length - 1) { + const ext = basename.slice(lastDot + 1).toLowerCase(); + if (/^[a-z0-9]{1,16}$/.test(ext)) { + return ext; + } + } + + return 'bin'; + } } diff --git a/src/modules/uploader/providers/uploader.service.ts b/src/modules/uploader/providers/uploader.service.ts index 9bda649..e9987f2 100644 --- a/src/modules/uploader/providers/uploader.service.ts +++ b/src/modules/uploader/providers/uploader.service.ts @@ -2,6 +2,7 @@ import { File } from '@nest-lab/fastify-multer'; import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { ImageProcessingService } from './image-processing.service'; import { S3Service } from './s3.service'; import { UploaderMessage } from '../../../common/enums/message.enum'; import { FileType } from '../interfaces/file.interface'; @@ -15,6 +16,7 @@ export class UploaderService { constructor( private readonly configService: ConfigService, private readonly s3Service: S3Service, + private readonly imageProcessingService: ImageProcessingService, ) { this.maxFileSize = this.configService.get('MAX_FILE_SIZE', 1024 * 1024 * 100); // 100MB this.allowedMimeTypes = [ @@ -80,10 +82,22 @@ export class UploaderService { const fileType = await this.determineFileType(file.mimetype); - const s3Key = this.s3Service.generateFileKey(file.originalname, fileType); + let uploadBuffer = file.buffer; + let contentType = file.mimetype; + let extensionOverride: string | undefined; - // Upload to S3 - const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, { + if (fileType === 'image') { + const processed = await this.imageProcessingService.processImage(file.buffer, file.mimetype); + if (processed) { + uploadBuffer = processed.buffer; + contentType = processed.contentType; + extensionOverride = processed.extension; + } + } + + const s3Key = this.s3Service.generateFileKey(file.originalname, fileType, extensionOverride); + + const s3Result = await this.s3Service.uploadFile(uploadBuffer, s3Key, contentType, { originalName: file.originalname, uploadedBy: 'admin', }); diff --git a/src/modules/uploader/uploader.module.ts b/src/modules/uploader/uploader.module.ts index 960cc78..43cf8e9 100644 --- a/src/modules/uploader/uploader.module.ts +++ b/src/modules/uploader/uploader.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { ImageProcessingService } from './providers/image-processing.service'; import { S3Service } from './providers/s3.service'; import { UploaderService } from './providers/uploader.service'; import { UploaderController } from './controllers/uploader.controller'; @@ -7,8 +8,8 @@ import { JwtModule } from '@nestjs/jwt'; @Module({ controllers: [UploaderController], - providers: [UploaderService, S3Service], - exports: [UploaderService, S3Service], + providers: [UploaderService, S3Service, ImageProcessingService], + exports: [UploaderService, S3Service, ImageProcessingService], imports: [JwtModule], }) export class UploaderModule {}