---
phase: 03-images
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/server/db/schema.ts
- src/lib/server/db/repository.ts
- src/lib/server/images/storage.ts
- src/lib/server/images/thumbnails.ts
- src/routes/api/images/[id]/+server.ts
- src/routes/api/images/[id]/thumbnail/+server.ts
autonomous: true
must_haves:
truths:
- "Images table exists in database with entryId foreign key"
- "Image files can be written to filesystem"
- "Thumbnails can be generated from uploaded images"
- "Images can be served via API endpoints"
artifacts:
- path: "src/lib/server/db/schema.ts"
provides: "images table definition"
contains: "images = sqliteTable"
- path: "src/lib/server/db/repository.ts"
provides: "imageRepository with CRUD operations"
contains: "imageRepository"
- path: "src/lib/server/images/storage.ts"
provides: "File write/read operations"
exports: ["saveOriginal", "saveThumbnail", "deleteImage"]
- path: "src/lib/server/images/thumbnails.ts"
provides: "Sharp thumbnail generation"
exports: ["generateThumbnail"]
- path: "src/routes/api/images/[id]/+server.ts"
provides: "GET endpoint for original images"
exports: ["GET"]
- path: "src/routes/api/images/[id]/thumbnail/+server.ts"
provides: "GET endpoint for thumbnails"
exports: ["GET"]
key_links:
- from: "src/lib/server/images/thumbnails.ts"
to: "sharp"
via: "import sharp"
pattern: "import sharp from 'sharp'"
- from: "src/routes/api/images/[id]/+server.ts"
to: "src/lib/server/db/repository.ts"
via: "imageRepository.getById"
pattern: "imageRepository\\.getById"
---
Create the foundation layer for image attachments: database schema, file storage utilities, thumbnail generation, and API endpoints for serving images.
Purpose: Establishes the data model and infrastructure that upload components will use. Without this layer, images cannot be stored, processed, or retrieved.
Output:
- Database table for images with entry relationship
- Repository layer for image CRUD
- Filesystem utilities for storing originals and thumbnails
- API endpoints that serve images to the browser
@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
@/home/tho/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/03-images/03-RESEARCH.md
@src/lib/server/db/schema.ts
@src/lib/server/db/repository.ts
@src/lib/server/db/index.ts
Task 1: Database schema and repository for images
src/lib/server/db/schema.ts
src/lib/server/db/repository.ts
1. Install sharp: `npm install sharp`
2. Add images table to schema.ts:
- id: text primary key (nanoid)
- entryId: text foreign key to entries.id (not null)
- filename: text (original filename for display)
- ext: text (file extension without dot: jpg, png, etc.)
- createdAt: text ISO timestamp
Export Image and NewImage types.
3. Add ImageRepository interface and implementation to repository.ts:
- create(image: Omit): Image
- getById(id: string): Image | undefined
- getByEntryId(entryId: string): Image[]
- delete(id: string): boolean
- deleteByEntryId(entryId: string): number (returns count deleted)
4. Export imageRepository singleton alongside entryRepository.
Use same patterns as existing entryRepository (nanoid for IDs, synchronous Drizzle operations).
Run `npm run check` - TypeScript compiles without errors.
Verify exports: `grep -n "imageRepository" src/lib/server/db/repository.ts`
images table defined with entryId relationship.
imageRepository exported with create, getById, getByEntryId, delete, deleteByEntryId methods.
Task 2: File storage and thumbnail generation utilities
src/lib/server/images/storage.ts
src/lib/server/images/thumbnails.ts
1. Create src/lib/server/images/storage.ts:
- UPLOAD_DIR constant: 'data/uploads'
- ORIGINALS_DIR: 'data/uploads/originals'
- THUMBNAILS_DIR: 'data/uploads/thumbnails'
Functions:
- ensureDirectories(): Creates directories if they don't exist (use mkdir recursive)
- saveOriginal(id: string, ext: string, buffer: Buffer): Promise
Writes to `${ORIGINALS_DIR}/${id}.${ext}`
- saveThumbnail(id: string, buffer: Buffer): Promise
Writes to `${THUMBNAILS_DIR}/${id}.jpg` (thumbnails always jpg)
- getOriginalPath(id: string, ext: string): string
- getThumbnailPath(id: string): string
- deleteImage(id: string, ext: string): Promise
Removes both original and thumbnail files (use try/catch, don't throw if missing)
2. Create src/lib/server/images/thumbnails.ts:
- import sharp from 'sharp'
Function:
- generateThumbnail(buffer: Buffer, size?: number): Promise
Default size: 150
CRITICAL: Call sharp.rotate() FIRST to handle EXIF orientation from mobile photos
Then resize with fit: 'cover', position: 'center', withoutEnlargement: true
Output as jpeg with quality 80
Return the thumbnail buffer (caller saves it)
Run `npm run check` - TypeScript compiles.
Run quick test: `node -e "import('sharp').then(s => console.log('sharp version:', s.default.versions?.sharp || 'loaded'))"` or just verify sharp installed in package.json.
storage.ts exports directory management and file I/O functions.
thumbnails.ts exports generateThumbnail that handles EXIF rotation.
Task 3: API endpoints for serving images
src/routes/api/images/[id]/+server.ts
src/routes/api/images/[id]/thumbnail/+server.ts
1. Create src/routes/api/images/[id]/+server.ts:
- GET handler that:
a. Gets id from params
b. Looks up image in imageRepository.getById(id)
c. Returns 404 error if not found
d. Reads file from getOriginalPath(id, image.ext)
e. Returns Response with:
- Content-Type based on ext (jpg -> image/jpeg, png -> image/png, etc.)
- Content-Length header
- Cache-Control: public, max-age=31536000, immutable (images are immutable by ID)
f. Catches file read errors, returns 404
2. Create src/routes/api/images/[id]/thumbnail/+server.ts:
- Same pattern but reads from getThumbnailPath(id)
- Content-Type always image/jpeg (thumbnails are always jpg)
Import imageRepository and storage functions. Use readFile from 'node:fs/promises'.
Run `npm run check` - TypeScript compiles.
Verify endpoints exist: `ls -la src/routes/api/images/`
GET /api/images/[id] serves original images with proper Content-Type.
GET /api/images/[id]/thumbnail serves thumbnails.
Both return 404 for missing images.
1. `npm run check` passes with no TypeScript errors
2. `npm run dev` starts without errors
3. Sharp is in dependencies: `grep sharp package.json`
4. All new files exist:
- src/lib/server/db/schema.ts contains `images = sqliteTable`
- src/lib/server/db/repository.ts exports `imageRepository`
- src/lib/server/images/storage.ts exists
- src/lib/server/images/thumbnails.ts exists
- src/routes/api/images/[id]/+server.ts exists
- src/routes/api/images/[id]/thumbnail/+server.ts exists
- images table can store image metadata with entry relationship
- imageRepository provides typed CRUD for images
- Thumbnail generation uses Sharp with EXIF auto-rotation
- API endpoints can serve images from filesystem
- All code compiles without TypeScript errors