diff --git a/backend/package-lock.json b/backend/package-lock.json index e59804653..5501666a7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.12.0", "dependencies": { "@aws-sdk/client-s3": "^3.787.0", + "@keyv/redis": "^4.4.0", "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.17", "@nestjs/config": "^4.0.2", @@ -25,6 +26,7 @@ "argon2": "^0.41.1", "body-parser": "^2.2.0", "cache-manager": "^6.4.2", + "cacheable": "^1.9.0", "clamscan": "^2.4.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -2573,6 +2575,22 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@keyv/redis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-4.4.0.tgz", + "integrity": "sha512-n/KEj3S7crVkoykggqsMUtcjNGvjagGPlJYgO/r6m9hhGZfhp1txJElHxcdJ1ANi/LJoBuOSILj15g6HD2ucqQ==", + "license": "MIT", + "dependencies": { + "@redis/client": "^1.6.0", + "cluster-key-slot": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.3.3" + } + }, "node_modules/@keyv/serialize": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", @@ -3259,6 +3277,20 @@ "@prisma/debug": "6.6.0" } }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -5233,6 +5265,16 @@ "keyv": "^5.3.2" } }, + "node_modules/cacheable": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.9.0.tgz", + "integrity": "sha512-8D5htMCxPDUULux9gFzv30f04Xo3wCnik0oOxKoRTPIBoqA7HtOcJ87uBhQTs3jCfZZTrUBGsYIZOgE0ZRgMAg==", + "license": "MIT", + "dependencies": { + "hookified": "^1.8.2", + "keyv": "^5.3.3" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", @@ -5444,6 +5486,15 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -6897,6 +6948,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/get-intrinsic": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", @@ -7134,6 +7194,12 @@ "node": ">= 0.4" } }, + "node_modules/hookified": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.9.0.tgz", + "integrity": "sha512-2yEEGqphImtKIe1NXWEhu6yD3hlFR4Mxk4Mtp3XEyScpSt4pQ4ymmXA1zzxZpj99QkFK+nN0nzjeb2+RUi/6CQ==", + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -7634,9 +7700,10 @@ } }, "node_modules/keyv": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.2.tgz", - "integrity": "sha512-Lji2XRxqqa5Wg+CHLVfFKBImfJZ4pCSccu9eVWK6w4c2SDFLd8JAn1zqTuSFnsxb7ope6rMsnIHfp+eBbRBRZQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz", + "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==", + "license": "MIT", "dependencies": { "@keyv/serialize": "^1.0.3" } @@ -10855,8 +10922,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.7.1", diff --git a/backend/package.json b/backend/package.json index 7ef8ecf85..ba15fbeef 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.787.0", + "@keyv/redis": "^4.4.0", "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.17", "@nestjs/config": "^4.0.2", @@ -30,6 +31,7 @@ "argon2": "^0.41.1", "body-parser": "^2.2.0", "cache-manager": "^6.4.2", + "cacheable": "^1.9.0", "clamscan": "^2.4.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts index e5a1c4226..8f0d1612c 100644 --- a/backend/prisma/seed/config.seed.ts +++ b/backend/prisma/seed/config.seed.ts @@ -76,6 +76,25 @@ export const configVariables = { secret: false, }, }, + cache: { + "redis-enabled": { + type: "boolean", + defaultValue: "false", + }, + "redis-url": { + type: "string", + defaultValue: "redis://pingvin-redis:6379", + secret: true, + }, + ttl: { + type: "number", + defaultValue: "60", + }, + maxItems: { + type: "number", + defaultValue: "1000", + }, + }, email: { enableShareEmailRecipients: { type: "boolean", @@ -419,11 +438,11 @@ const prisma = new PrismaClient({ async function seedConfigVariables() { for (const [category, configVariablesOfCategory] of Object.entries( - configVariables + configVariables, )) { let order = 0; for (const [name, properties] of Object.entries( - configVariablesOfCategory + configVariablesOfCategory, )) { const existingConfigVariable = await prisma.config.findUnique({ where: { name_category: { name, category } }, @@ -469,7 +488,7 @@ async function migrateConfigVariables() { // Update the config variable if it exists in the seed } else { const variableOrder = Object.keys( - configVariables[existingConfigVariable.category] + configVariables[existingConfigVariable.category], ).indexOf(existingConfigVariable.name); await prisma.config.update({ where: { diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 23227438f..8e581d324 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -3,9 +3,9 @@ import { Module } from "@nestjs/common"; import { ScheduleModule } from "@nestjs/schedule"; import { AuthModule } from "./auth/auth.module"; -import { CacheModule } from "@nestjs/cache-manager"; import { APP_GUARD } from "@nestjs/core"; import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; +import { AppCacheModule } from "./cache/cache.module"; import { AppController } from "./app.controller"; import { ClamScanModule } from "./clamscan/clamscan.module"; import { ConfigModule } from "./config/config.module"; @@ -38,9 +38,7 @@ import { UserModule } from "./user/user.module"; ClamScanModule, ReverseShareModule, OAuthModule, - CacheModule.register({ - isGlobal: true, - }), + AppCacheModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/cache/cache.module.ts b/backend/src/cache/cache.module.ts new file mode 100644 index 000000000..a59c05a1f --- /dev/null +++ b/backend/src/cache/cache.module.ts @@ -0,0 +1,41 @@ +import { Module } from "@nestjs/common"; +import { CacheModule } from "@nestjs/cache-manager"; +import { CacheableMemory } from "cacheable"; +import { createKeyv } from "@keyv/redis"; +import { Keyv } from "keyv"; +import { ConfigModule } from "src/config/config.module"; +import { ConfigService } from "src/config/config.service"; + +@Module({ + imports: [ + ConfigModule, + CacheModule.registerAsync({ + isGlobal: true, + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => { + const useRedis = configService.get("cache.redis-enabled"); + const ttl = configService.get("cache.ttl"); + const max = configService.get("cache.maxItems"); + + let config = { + ttl, + max, + stores: [], + }; + + if (useRedis) { + const redisUrl = configService.get("cache.redis-url"); + config.stores = [ + new Keyv({ store: new CacheableMemory({ ttl, lruSize: 5000 }) }), + createKeyv(redisUrl), + ]; + } + + return config; + }, + }), + ], + exports: [CacheModule], +}) +export class AppCacheModule {} diff --git a/config.example.yaml b/config.example.yaml index 51d6a9be0..2d98cb6ff 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -23,12 +23,21 @@ share: shareIdLength: "8" #Maximum share size maxSize: "1000000000" - #Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. + #Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. zipCompressionLevel: "9" #Adjust the chunk size for your uploads to balance efficiency and reliability according to your internet connection. Smaller chunks can enhance success rates for unstable connections, while larger chunks make uploads faster for stable connections. chunkSize: "10000000" #The share creation modal automatically appears when a user selects files, eliminating the need to manually click the button. autoOpenShareModal: "false" +cache: + #Normally Pingvin Share caches information in memory. If you run multiple instances of Pingvin Share, you need to enable Redis caching to share the cache between the instances. + redis-enabled: "false" + #Url to connect to the Redis instance used for caching. + redis-url: redis://pingvin-redis:6379 + #Time in second to keep information inside the cache. + ttl: "60" + #Maximum number of items inside the cache. + maxItems: "1000" email: #Whether to allow email sharing with recipients. Only enable this if SMTP is activated. enableShareEmailRecipients: "false" diff --git a/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx b/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx index 369d72609..71a8b9017 100644 --- a/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx +++ b/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx @@ -13,13 +13,14 @@ import Link from "next/link"; import { Dispatch, SetStateAction } from "react"; import { TbAt, + TbBinaryTree, + TbBucket, TbMail, + TbScale, + TbServerBolt, + TbSettings, TbShare, TbSocial, - TbBucket, - TbBinaryTree, - TbSettings, - TbScale, } from "react-icons/tb"; import { FormattedMessage } from "react-intl"; @@ -32,6 +33,7 @@ const categories = [ { name: "LDAP", icon: }, { name: "S3", icon: }, { name: "Legal", icon: }, + { name: "Cache", icon: }, ]; const useStyles = createStyles((theme) => ({ diff --git a/frontend/src/i18n/translations/en-US.ts b/frontend/src/i18n/translations/en-US.ts index 266f560aa..98e1d15b7 100644 --- a/frontend/src/i18n/translations/en-US.ts +++ b/frontend/src/i18n/translations/en-US.ts @@ -423,6 +423,7 @@ export default { "admin.config.title": "Configuration", "admin.config.category.general": "General", "admin.config.category.share": "Share", + "admin.config.category.cache": "Cache", "admin.config.category.email": "Email", "admin.config.category.smtp": "SMTP", "admin.config.category.oauth": "Social Login", @@ -446,6 +447,19 @@ export default { "Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.", "admin.config.general.logo.placeholder": "Pick image", + "admin.config.cache.ttl": "TTL", + "admin.config.cache.ttl.description": + "Time in second to keep information inside the cache.", + "admin.config.cache.max-items": "Maximum items", + "admin.config.cache.max-items.description": + "Maximum number of items inside the cache.", + "admin.config.cache.redis-enabled": "Redis enabled", + "admin.config.cache.redis-enabled.description": + "Normally Pingvin Share caches information in memory. If you run multiple instances of Pingvin Share, you need to enable Redis caching to share the cache between the instances.", + "admin.config.cache.redis-url": "Redis URL", + "admin.config.cache.redis-url.description": + "Url to connect to the Redis instance used for caching.", + "admin.config.email.enable-share-email-recipients": "Enable email recipient sharing", "admin.config.email.enable-share-email-recipients.description":