ыввы
This commit is contained in:
@@ -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) })
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export async function registerAuthRoutes(fastify) {
|
||||
passwordHash,
|
||||
displayName: displayName || null,
|
||||
avatar: avatarUri,
|
||||
avatarStyle: 'avataaars',
|
||||
avatarStyle: 'initials',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user