From 09c5e0cd5074c2c6e91fbabe8984010c08805747 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 25 May 2026 21:14:19 +0500 Subject: [PATCH] =?UTF-8?q?=D1=8B=D0=B2=D0=B2=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/package-lock.json | 102 +++--------------------- server/package.json | 2 +- server/prisma/prisma/dev.db | Bin 352256 -> 348160 bytes server/src/lib/generate-avatar.js | 4 +- server/src/lib/image-resize.js | 49 +++++++++--- server/src/routes/auth.js | 2 +- server/src/routes/oauth-social.js | 2 +- server/src/routes/uploads-resized.js | 111 +++++++++++++++------------ 8 files changed, 112 insertions(+), 160 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 9c04391..1e6e3e7 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -191,6 +191,7 @@ "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.2.tgz", "integrity": "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -462,29 +463,6 @@ "@dicebear/core": "^9.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -1637,9 +1615,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1654,9 +1629,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1671,9 +1643,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1688,9 +1657,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1705,9 +1671,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1722,9 +1685,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1739,9 +1699,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1756,9 +1713,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1773,9 +1727,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1790,9 +1741,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1807,9 +1755,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1824,9 +1769,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1841,9 +1783,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2103,9 +2042,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2120,9 +2056,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2137,9 +2070,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2154,9 +2084,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2171,9 +2098,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2188,9 +2112,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2205,9 +2126,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2222,9 +2140,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2239,9 +2154,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2256,9 +2168,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2467,6 +2376,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3117,6 +3027,7 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -3173,6 +3084,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3720,6 +3632,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4373,6 +4286,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4517,6 +4431,7 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -4547,6 +4462,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -5259,6 +5175,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.4" }, @@ -5312,6 +5229,7 @@ "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/server/package.json b/server/package.json index 0e552ad..8c8d921 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "node --env-file=.env --watch src/index.js", + "dev": "node --env-file=.env --watch-path=./src src/index.js", "dev:classic": "node --watch src/index.js", "start": "node src/index.js", "db:migrate": "prisma migrate dev", diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db index 76de4c851c6f8eeb04d006951b872438c2fdcd0c..cab328bd028380398f6a24278d027f0dd5e157f9 100644 GIT binary patch delta 8694 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 diff --git a/server/src/lib/generate-avatar.js b/server/src/lib/generate-avatar.js index cbc2680..002344f 100644 --- a/server/src/lib/generate-avatar.js +++ b/server/src/lib/generate-avatar.js @@ -1,7 +1,7 @@ -import { avataaars } from '@dicebear/collection' +import { initials } from '@dicebear/collection' import { createAvatar } from '@dicebear/core' -const DEFAULT_STYLE = avataaars +const DEFAULT_STYLE = initials export async function generateAvatar(seed) { const avatar = createAvatar(DEFAULT_STYLE, { seed: String(seed) }) diff --git a/server/src/lib/image-resize.js b/server/src/lib/image-resize.js index f7fc4ed..5283ed2 100644 --- a/server/src/lib/image-resize.js +++ b/server/src/lib/image-resize.js @@ -49,15 +49,28 @@ export async function getOrCreateResized(uuid, width, format, subdir = '') { await fs.promises.mkdir(path.dirname(cachePath), { recursive: true }) - const sharp = (await import('sharp')).default - let pipeline = sharp(originalPath) - - if (width) { - pipeline = pipeline.resize(width, null, { withoutEnlargement: true }) + let sharpModule + try { + sharpModule = (await import('sharp')).default + } catch (err) { + const msg = `Failed to load sharp image processing library: ${err.message}` + throw Object.assign(new Error(msg), { cause: err, code: 'SHARP_LOAD_ERROR' }) } - const options = format === 'avif' ? { quality: 75, effort: 4 } : { quality: 80 } - await pipeline[format](options).toFile(cachePath) + let pipeline + try { + pipeline = sharpModule(originalPath) + + if (width) { + pipeline = pipeline.resize(width, null, { withoutEnlargement: true }) + } + + const options = format === 'avif' ? { quality: 75, effort: 4 } : { quality: 80 } + await pipeline[format](options).toFile(cachePath) + } catch (err) { + const msg = `Failed to resize image ${originalPath} to ${width}w ${format}: ${err.message}` + throw Object.assign(new Error(msg), { cause: err, code: 'SHARP_RESIZE_ERROR' }) + } return { path: cachePath, isNew: true } } @@ -75,17 +88,29 @@ export async function generateAllSizes(uuid, subdir, originalPath) { const cacheDir = path.join(CACHE_DIR, cacheSubdir) await fs.promises.mkdir(cacheDir, { recursive: true }) - const sharp = (await import('sharp')).default - const source = sharp(originalPath) + let sharpModule + try { + sharpModule = (await import('sharp')).default + } catch (err) { + const msg = `Failed to load sharp image processing library: ${err.message}` + throw Object.assign(new Error(msg), { cause: err, code: 'SHARP_LOAD_ERROR' }) + } + + const source = sharpModule(originalPath) for (const width of VALID_WIDTHS) { for (const format of SUPPORTED_FORMATS) { const cacheFileName = `${uuid}_w${width}.${format}` const cachePath = path.join(CACHE_DIR, cacheSubdir, cacheFileName) - const pipeline = source.clone().resize(width, null, { withoutEnlargement: true }) - const options = format === 'avif' ? { quality: 75, effort: 4 } : { quality: 80 } - await pipeline[format](options).toFile(cachePath) + try { + const pipeline = source.clone().resize(width, null, { withoutEnlargement: true }) + const options = format === 'avif' ? { quality: 75, effort: 4 } : { quality: 80 } + await pipeline[format](options).toFile(cachePath) + } catch (err) { + const msg = `Failed to generate ${width}w ${format} for ${originalPath}: ${err.message}` + throw Object.assign(new Error(msg), { cause: err, code: 'SHARP_RESIZE_ERROR' }) + } } } } diff --git a/server/src/routes/auth.js b/server/src/routes/auth.js index cf7185b..2e5ee6f 100644 --- a/server/src/routes/auth.js +++ b/server/src/routes/auth.js @@ -112,7 +112,7 @@ export async function registerAuthRoutes(fastify) { passwordHash, displayName: displayName || null, avatar: avatarUri, - avatarStyle: 'avataaars', + avatarStyle: 'initials', }, }) diff --git a/server/src/routes/oauth-social.js b/server/src/routes/oauth-social.js index faf74b3..7bab24c 100644 --- a/server/src/routes/oauth-social.js +++ b/server/src/routes/oauth-social.js @@ -90,7 +90,7 @@ async function findOrCreateUserFromOAuth({ provider, providerUserId, accessToken email, displayName: norm ? norm.split('@')[0] : 'Пользователь', avatar: await generateAvatar(email), - avatarStyle: 'avataaars', + avatarStyle: 'initials', }, }) await prisma.oAuthAccount.create({ diff --git a/server/src/routes/uploads-resized.js b/server/src/routes/uploads-resized.js index 04a7d56..add5606 100644 --- a/server/src/routes/uploads-resized.js +++ b/server/src/routes/uploads-resized.js @@ -11,62 +11,71 @@ const CACHE_CONTROL_SHORT = 'public, max-age=86400' */ export function registerUploadsResized(fastify) { fastify.get('/uploads-resized/*', async (request, reply) => { - const rawPath = request.params['*'] - const url = new URL(request.url, 'http://localhost') - const widthParam = url.searchParams.get('w') - - // Parse: [subdir/]filename.format - const parts = rawPath.split('/') - let filename, - subdir = '' - - if (parts.length > 1) { - subdir = parts.slice(0, -1).join('/') + '/' - filename = parts[parts.length - 1] - } else { - filename = parts[0] - } - - const dotIdx = filename.lastIndexOf('.') - if (dotIdx === -1) { - return reply.code(400).send({ error: 'Invalid request: no format specified' }) - } - - const uuid = filename.slice(0, dotIdx) - const format = filename.slice(dotIdx + 1).toLowerCase() - - if (!SUPPORTED_FORMATS.has(format)) { - return reply.code(400).send({ error: `Unsupported format: ${format}. Use avif or webp.` }) - } - - // Validate width - let width = null - if (widthParam) { - const w = parseInt(widthParam, 10) - if (!VALID_WIDTHS.includes(w)) { - return reply.code(400).send({ error: `Invalid width: ${widthParam}. Use: ${VALID_WIDTHS.join(', ')}` }) + try { + const rawPath = request.params['*'] + if (typeof rawPath !== 'string') { + return reply.code(400).send({ error: 'Invalid request: missing file path' }) } - width = w - } - // If no width requested, serve original with short cache - if (!width) { - const originalPath = await findOriginalFile(uuid, subdir || undefined) - if (!originalPath) { + const url = new URL(request.url, 'http://localhost') + const widthParam = url.searchParams.get('w') + + // Parse: [subdir/]filename.format + const parts = rawPath.split('/') + let filename, + subdir = '' + + if (parts.length > 1) { + subdir = parts.slice(0, -1).join('/') + '/' + filename = parts[parts.length - 1] + } else { + filename = parts[0] + } + + const dotIdx = filename.lastIndexOf('.') + if (dotIdx === -1) { + return reply.code(400).send({ error: 'Invalid request: no format specified' }) + } + + const uuid = filename.slice(0, dotIdx) + const format = filename.slice(dotIdx + 1).toLowerCase() + + if (!SUPPORTED_FORMATS.has(format)) { + return reply.code(400).send({ error: `Unsupported format: ${format}. Use avif or webp.` }) + } + + // Validate width + let width = null + if (widthParam) { + const w = parseInt(widthParam, 10) + if (!VALID_WIDTHS.includes(w)) { + return reply.code(400).send({ error: `Invalid width: ${widthParam}. Use: ${VALID_WIDTHS.join(', ')}` }) + } + width = w + } + + // If no width requested, serve original with short cache + if (!width) { + const originalPath = await findOriginalFile(uuid, subdir || undefined) + if (!originalPath) { + return reply.code(404).send({ error: 'Image not found' }) + } + reply.header('Cache-Control', CACHE_CONTROL_SHORT) + reply.header('Content-Type', format === 'avif' ? 'image/avif' : 'image/webp') + return reply.send(fs.createReadStream(originalPath)) + } + + const result = await getOrCreateResized(uuid, width, format, subdir || undefined) + if (!result) { return reply.code(404).send({ error: 'Image not found' }) } - reply.header('Cache-Control', CACHE_CONTROL_SHORT) + + reply.header('Cache-Control', CACHE_CONTROL_IMMUTABLE) reply.header('Content-Type', format === 'avif' ? 'image/avif' : 'image/webp') - return reply.send(fs.createReadStream(originalPath)) + return reply.send(fs.createReadStream(result.path)) + } catch (error) { + request.log.error({ err: error, url: request.url }, 'uploads-resized route error') + return reply.code(500).send({ error: error.message || 'Image resize failed' }) } - - const result = await getOrCreateResized(uuid, width, format, subdir || undefined) - if (!result) { - return reply.code(404).send({ error: 'Image not found' }) - } - - reply.header('Cache-Control', CACHE_CONTROL_IMMUTABLE) - reply.header('Content-Type', format === 'avif' ? 'image/avif' : 'image/webp') - return reply.send(fs.createReadStream(result.path)) }) }