Spaces:
Sleeping
Sleeping
Download src/lib/server/photos.ts from assafvayner/splitwise: direct link, hf CLI and curl.
- Browser
- Download file 4.34 kB
-
https://huggingface.co/spaces/assafvayner/splitwise/resolve/main/src/lib/server/photos.ts
- Command line
-
hf download hf://spaces/assafvayner/splitwise/src/lib/server/photos.ts
-
curl -L -o photos.ts https://huggingface.co/spaces/assafvayner/splitwise/resolve/main/src/lib/server/photos.ts
4.34 kB
| import { ulid } from '../domain/ulid'; | |
| import type { ExpensePhoto } from '../domain/types'; | |
| import type { BucketStore } from './storage/store'; | |
| export const MAX_PHOTO_BYTES = 5 * 1024 * 1024; | |
| export type ImageType = 'image/jpeg' | 'image/png' | 'image/webp'; | |
| const EXT: Record<ImageType, string> = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' }; | |
| const TYPE_BY_EXT: Record<string, ImageType> = { jpg: 'image/jpeg', png: 'image/png', webp: 'image/webp' }; | |
| export const PHOTO_KEY_RE = /^photos\/(e_[0-9A-HJKMNP-TV-Z]{26})\/([0-9A-HJKMNP-TV-Z]{26})\.(jpg|png|webp)$/; | |
| export function photoKey(expenseId: string, photoId: string, type: ImageType): string { | |
| return `photos/${expenseId}/${photoId}.${EXT[type]}`; | |
| } | |
| export function contentTypeForKey(key: string): string { | |
| const ext = key.slice(key.lastIndexOf('.') + 1); | |
| return Object.hasOwn(TYPE_BY_EXT, ext) ? TYPE_BY_EXT[ext] : 'application/octet-stream'; | |
| } | |
| export function sniffImageType(b: Uint8Array): ImageType | null { | |
| if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'image/jpeg'; | |
| if (b.length >= 8 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47 && b[4] === 0x0d && b[5] === 0x0a && b[6] === 0x1a && b[7] === 0x0a) return 'image/png'; | |
| if (b.length >= 12 && ascii(b, 0, 4) === 'RIFF' && ascii(b, 8, 12) === 'WEBP') return 'image/webp'; | |
| return null; | |
| } | |
| export const MAX_IMAGE_DIMENSION = 20_000; | |
| export function imageDimensions(b: Uint8Array, type: ImageType): { width: number; height: number } | null { | |
| const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); | |
| try { | |
| if (type === 'image/png') { | |
| if (b.length < 24 || ascii(b, 12, 16) !== 'IHDR') return null; | |
| return check(dv.getUint32(16), dv.getUint32(20)); | |
| } | |
| if (type === 'image/jpeg') { | |
| let i = 2; | |
| while (i + 9 < b.length) { | |
| if (b[i] !== 0xff) { | |
| i++; | |
| continue; | |
| } | |
| const marker = b[i + 1]; | |
| if (marker === 0xff) { | |
| i++; | |
| continue; | |
| } | |
| const isSof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc; | |
| if (isSof) return check(dv.getUint16(i + 7), dv.getUint16(i + 5)); | |
| if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) { | |
| i += 2; | |
| continue; | |
| } | |
| i += 2 + dv.getUint16(i + 2); | |
| } | |
| return null; | |
| } | |
| // WebP | |
| const chunk = ascii(b, 12, 16); | |
| if (chunk === 'VP8X' && b.length >= 30) return check(1 + u24le(b, 24), 1 + u24le(b, 27)); | |
| if (chunk === 'VP8 ' && b.length >= 30) return check(dv.getUint16(26, true) & 0x3fff, dv.getUint16(28, true) & 0x3fff); | |
| if (chunk === 'VP8L' && b.length >= 25) { | |
| if (b[20] !== 0x2f) return null; | |
| const w = 1 + (((b[22] & 0x3f) << 8) | b[21]); | |
| const h = 1 + (((b[24] & 0x0f) << 10) | (b[23] << 2) | ((b[22] & 0xc0) >> 6)); | |
| return check(w, h); | |
| } | |
| return null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function ascii(b: Uint8Array, start: number, end: number): string { | |
| return String.fromCharCode(...b.subarray(start, end)); | |
| } | |
| function u24le(b: Uint8Array, i: number): number { | |
| return b[i] | (b[i + 1] << 8) | (b[i + 2] << 16); | |
| } | |
| function check(width: number, height: number): { width: number; height: number } | null { | |
| return width > 0 && height > 0 && width <= MAX_IMAGE_DIMENSION && height <= MAX_IMAGE_DIMENSION ? { width, height } : null; | |
| } | |
| /** Returns the uploaded file for `name`, or null when the field is absent/empty. */ | |
| export function uploadedFile(form: FormData, name: string): File | null { | |
| const f = form.get(name); | |
| return f instanceof File && f.size > 0 ? f : null; | |
| } | |
| export type StoredPhoto = { ok: true; photo: ExpensePhoto } | { ok: false; error: string }; | |
| /** Validates and stores an uploaded image under photos/<expenseId>/<ulid>.<ext>. */ | |
| export async function storeUploadedPhoto(store: BucketStore, expenseId: string, file: File): Promise<StoredPhoto> { | |
| if (file.size > MAX_PHOTO_BYTES) return { ok: false, error: 'Photo must be 5 MB or smaller' }; | |
| const bytes = new Uint8Array(await file.arrayBuffer()); | |
| const type = sniffImageType(bytes); | |
| if (!type) return { ok: false, error: 'Photo must be a JPEG, PNG or WebP image' }; | |
| const key = photoKey(expenseId, ulid(), type); | |
| await store.put(key, bytes, type); | |
| const dims = imageDimensions(bytes, type); | |
| return { ok: true, photo: { key, contentType: type, ...(dims ?? {}) } }; | |
| } | |