Compress and resize photos on upload via sharp
Buffer upload → auto-rotate (EXIF) → resize to 1800px max → JPEG 82% progressive. Typical phone photo drops from 4-8 MB to ~200-400 KB. sharp prebuilt binaries include libvips so no Alpine package changes needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ca006f9aea
commit
4ab9eb4035
2 changed files with 21 additions and 14 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { requireAuth, requireCap, hasCap } from '../auth.js'
|
||||
import { pool } from '../db.js'
|
||||
import { logEvent } from '../lib/task-core.js'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { mkdir, unlink } from 'fs/promises'
|
||||
import { mkdir, unlink, writeFile } from 'fs/promises'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { extname, join } from 'path'
|
||||
import { join } from 'path'
|
||||
import sharp from 'sharp'
|
||||
|
||||
const ALLOWED_IMAGES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||
const STAGES = ['report', 'progress', 'resolution']
|
||||
|
|
@ -22,22 +22,28 @@ export async function photoRoutes(app, opts) {
|
|||
let fileData = null, stage = 'report'
|
||||
for await (const part of req.parts()) {
|
||||
if (part.type === 'file') {
|
||||
fileData = part
|
||||
// must consume the file stream inside the loop — save it now
|
||||
if (!ALLOWED_IMAGES.includes(part.mimetype)) {
|
||||
return reply.status(400).send({ error: 'Only JPEG, PNG and WebP images are allowed' })
|
||||
}
|
||||
const ext = extname(part.filename) || '.jpg'
|
||||
const filename = randomUUID() + ext
|
||||
|
||||
// Buffer the upload then process with sharp:
|
||||
// auto-rotate (fixes phone EXIF orientation), resize to 1800px max, re-encode as JPEG
|
||||
const chunks = []
|
||||
for await (const chunk of part.file) chunks.push(chunk)
|
||||
const raw = Buffer.concat(chunks)
|
||||
|
||||
const processed = await sharp(raw)
|
||||
.rotate()
|
||||
.resize(1800, 1800, { fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: 82, progressive: true })
|
||||
.toBuffer()
|
||||
|
||||
const filename = randomUUID() + '.jpg'
|
||||
const dir = join(UPLOADS_DIR, 'tasks', String(taskId))
|
||||
await mkdir(dir, { recursive: true })
|
||||
await writeFile(join(dir, filename), processed)
|
||||
|
||||
let size = 0
|
||||
const dest = createWriteStream(join(dir, filename))
|
||||
for await (const chunk of part.file) { dest.write(chunk); size += chunk.length }
|
||||
await new Promise(r => dest.end(r))
|
||||
|
||||
fileData = { filename: part.filename, mimetype: part.mimetype, savedAs: filename, size }
|
||||
fileData = { originalName: part.filename, savedAs: filename, size: processed.length }
|
||||
} else {
|
||||
const val = await part.value
|
||||
if (part.fieldname === 'stage' && STAGES.includes(String(val))) stage = String(val)
|
||||
|
|
@ -49,7 +55,7 @@ export async function photoRoutes(app, opts) {
|
|||
const { rows: ins } = await pool.query(
|
||||
`INSERT INTO task_photos (task_id, file_name, file_path, mime_type, file_size, stage, uploaded_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
|
||||
[taskId, fileData.filename, filePath, fileData.mimetype, fileData.size, stage, req.user.email]
|
||||
[taskId, fileData.originalName, filePath, 'image/jpeg', fileData.size, stage, req.user.email]
|
||||
)
|
||||
await logEvent(taskId, 'photo', { note: `Photo added (${stage})`, userName: req.user.name })
|
||||
return ins[0]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue