Optimize image uploads to 2k webp

This commit is contained in:
google-labs-jules[bot] committed 2026-02-15 14:10:07 +00:00
1 parent 16b3ab2bc0
commit a433927de7
4 files changed
+130 -40

No files matched your search

+28 -10
View File
@@ -7,21 +7,29 @@ const mockPrisma = {
},
}
mock.module('ulidx', () => ({
ulid: () => 'mocked-ulid',
}))
mock.module('../db', () => ({
prisma: mockPrisma,
}))
const mockBunWrite = mock((_path: string, content: unknown) =>
Promise.resolve((content as Blob | string).toString().length),
)
// @ts-ignore
Bun.write = mockBunWrite
const mockFileService = {
saveFile: mock(() => Promise.resolve({ path: 'data/files/mock.webp', filename: 'mock.webp', mimeType: 'image/webp' })),
saveBuffer: mock(() => Promise.resolve({ path: 'data/files/mock.webp', filename: 'mock.webp', mimeType: 'image/webp' })),
}
mock.module('./file', () => ({
fileService: mockFileService,
}))
describe('AssetService', () => {
afterEach(() => {
mockPrisma.asset.create.mockClear()
mockPrisma.asset.findUnique.mockClear()
mockBunWrite.mockClear()
mockFileService.saveFile.mockClear()
mockFileService.saveBuffer.mockClear()
})
it('createAsset should save file and create db record', async () => {
@@ -31,18 +39,28 @@ describe('AssetService', () => {
const meta = { name: 'Test Asset', type: 'image/png' }
mockPrisma.asset.create.mockResolvedValue({
id: '01HRKV...',
id: 'mocked-ulid',
name: meta.name,
type: meta.type,
path: 'data/files/01HRKV....png',
type: 'image/webp',
path: 'mock.webp',
createdAt: new Date(),
updatedAt: new Date(),
})
const result = await assetService.createAsset(file, meta)
expect(mockBunWrite).toHaveBeenCalled()
expect(mockFileService.saveFile).toHaveBeenCalled()
expect(mockPrisma.asset.create).toHaveBeenCalled()
// Check call arguments
// Use expect.objectContaining for partial match
const createCall = mockPrisma.asset.create.mock.calls[0][0]
expect(createCall.data).toEqual(expect.objectContaining({
type: 'image/webp',
path: 'mock.webp',
id: 'mocked-ulid'
}))
expect(result.name).toBe(meta.name)
})
})
+7 -17
View File
@@ -1,24 +1,18 @@
import { ulid } from 'ulidx'
import { prisma } from '../db'
import { join, extname } from 'path'
// import { writeFile } from '../lib/fileSystem' // Using Bun.write
import { fileService } from './file'
export class AssetService {
async createAsset(file: File, meta: { name: string; type: string }) {
const saved = await fileService.saveFile(file, meta.name)
const id = ulid()
const ext = extname(file.name) || '.bin'
const filename = `${id}${ext}`
const path = join('data/files', filename)
// await writeFile(path, file)
await Bun.write(path, await file.arrayBuffer())
const asset = await prisma.asset.create({
data: {
id,
name: meta.name,
type: meta.type,
path: filename, // Save only filename or relative path
type: saved.mimeType,
path: saved.filename,
},
})
@@ -29,19 +23,15 @@ export class AssetService {
buffer: Buffer | ArrayBuffer,
meta: { name: string; type: string; ext?: string },
) {
const saved = await fileService.saveBuffer(buffer, meta.name, meta.type)
const id = ulid()
const ext = meta.ext || '.bin'
const filename = `${id}${ext}`
const path = join('data/files', filename)
await Bun.write(path, buffer)
const asset = await prisma.asset.create({
data: {
id,
name: meta.name,
type: meta.type,
path: filename,
type: saved.mimeType,
path: saved.filename,
},
})
+41 -8
View File
@@ -1,14 +1,20 @@
import { describe, expect, test, mock, beforeAll } from 'bun:test'
// Mock ulidx
mock.module('ulidx', () => ({
ulid: () => 'test-ulid',
}))
// Mock sharp
const sharpChain: any = {
resize: mock(() => sharpChain),
webp: mock(() => sharpChain),
toBuffer: mock(() => Promise.resolve(Buffer.from([1, 2, 3]))),
}
mock.module('sharp', () => {
const sharpInstance = {
webp: () => ({
toBuffer: () => Promise.resolve(Buffer.from([1, 2, 3])),
}),
}
return {
default: () => sharpInstance,
default: () => sharpChain,
}
})
@@ -38,7 +44,7 @@ describe('FileService', () => {
const file = new File([VALID_PNG_BUFFER], 'test.png', { type: 'image/png' })
const result = await fileService.saveFile(file)
expect(result.filename.endsWith('.webp')).toBe(true)
expect(result.filename).toBe('test-ulid.webp')
expect(result.mimeType).toBe('image/webp')
// Check if write was called
expect(mockWrite).toHaveBeenCalled()
@@ -46,13 +52,40 @@ describe('FileService', () => {
const writeCalls = mockWrite.mock.calls
const lastCall = writeCalls[writeCalls.length - 1]
expect(lastCall[0]).toContain('data/files')
// Check if sharp resize was called
expect(sharpChain.resize).toHaveBeenCalled()
expect(sharpChain.resize).toHaveBeenCalledWith({
width: 2048,
height: 2048,
fit: 'outside',
withoutEnlargement: true,
})
})
test('saveBase64Image saves a base64 string as webp', async () => {
// Pass valid base64
const result = await fileService.saveBase64Image(VALID_PNG_BASE64)
expect(result.filename.endsWith('.webp')).toBe(true)
expect(result.filename).toBe('test-ulid.webp')
expect(mockWrite).toHaveBeenCalled()
expect(sharpChain.resize).toHaveBeenCalled()
})
test('saveBuffer saves a buffer and converts to webp if mimeType is image', async () => {
const result = await fileService.saveBuffer(VALID_PNG_BUFFER, 'test.png', 'image/png')
expect(result.filename).toBe('test-ulid.webp')
expect(result.mimeType).toBe('image/webp')
expect(mockWrite).toHaveBeenCalled()
expect(sharpChain.resize).toHaveBeenCalled()
})
test('saveBuffer saves original if not image', async () => {
const result = await fileService.saveBuffer(VALID_PNG_BUFFER, 'test.bin', 'application/octet-stream')
expect(result.filename).toBe('test-ulid.bin')
expect(result.mimeType).toBe('application/octet-stream')
expect(mockWrite).toHaveBeenCalled()
})
})
+54 -5
View File
@@ -37,10 +37,16 @@ export class FileService {
} else {
// Blob case
buffer = await file.arrayBuffer()
const result = await this.optimizeImage(buffer)
buffer = result.buffer
filename = `${id}.webp`
mimeType = 'image/webp'
if (file.type.startsWith('image/')) {
const result = await this.optimizeImage(buffer)
buffer = result.buffer
filename = `${id}.webp`
mimeType = 'image/webp'
} else {
const ext = name ? name.split('.').pop() : 'bin'
filename = `${id}.${ext}`
mimeType = file.type || 'application/octet-stream'
}
}
const path = join('data/files', filename)
@@ -50,12 +56,55 @@ export class FileService {
return { path, filename, mimeType }
}
/**
* Saves a buffer directly as a file. Optimizes if it's an image.
* @param buffer The buffer to save.
* @param name Optional name for the file (used for extension detection).
* @param mimeType Optional mimeType.
*/
async saveBuffer(
buffer: Buffer | ArrayBuffer,
name?: string,
mimeType?: string,
): Promise<{ path: string; filename: string; mimeType: string }> {
const id = ulid()
let finalBuffer: Buffer | ArrayBuffer = buffer
let finalFilename: string
let finalMimeType: string = mimeType || 'application/octet-stream'
if (finalMimeType.startsWith('image/')) {
try {
const result = await this.optimizeImage(finalBuffer)
finalBuffer = result.buffer
finalFilename = `${id}.webp`
finalMimeType = 'image/webp'
} catch (error) {
console.warn('Failed to optimize image buffer, saving as is.', error)
const ext = name ? name.split('.').pop() : 'bin'
finalFilename = `${id}.${ext}`
}
} else {
const ext = name ? name.split('.').pop() : 'bin'
finalFilename = `${id}.${ext}`
}
const path = join('data/files', finalFilename)
await Bun.write(path, finalBuffer)
return { path, filename: finalFilename, mimeType: finalMimeType }
}
/**
* Optimizes an image buffer by converting it to WebP using sharp.
*/
private async optimizeImage(buffer: ArrayBuffer | Buffer): Promise<{ buffer: Buffer }> {
const optimizedBuffer = await sharp(buffer)
.webp({ quality: 80 }) // Good balance between size and quality
.resize({
width: 2048,
height: 2048,
fit: 'outside',
withoutEnlargement: true,
})
.webp({ quality: 85 })
.toBuffer()
return { buffer: optimizedBuffer }