Files
summercms-initial-research/.planning/phases/09-content-management/09-03-PLAN.md
Jakub Zych edbee885ac fix(09): revise plans based on checker feedback
- Plan 02: Added key_link clarifying CmsPageService.render populates
  PageRenderContext.components before template rendering. Updated
  Task 2/3 actions to emphasize component initialization flow.

- Plan 03: Split into 03 (storage + library core) and 03b (image
  processing + routes) to reduce scope from 9 files to 7+4 files.
  Estimated context reduced from ~65% to ~45% each.

- Plan 03b: New plan for ImageProcessor and MediaRoutes. Added
  specific curl command with -F flags and expected JSON response.

- Plan 05: Added plugin integration test (MenuPluginIntegrationSpec)
  demonstrating custom menu item type registration, resolution, and
  template rendering per CONT-09 requirement.

- Plan 06: Reframed must_haves truths from implementation details
  to user-observable outcomes (e.g., 'Developer edits template file,
  browser refresh shows changes without server restart')

- Roadmap: Updated Phase 9 from 6 to 7 plans.
2026-02-05 15:41:50 +01:00

245 lines
7.8 KiB
Markdown

---
phase: 09-content-management
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- build.mill
- summercms/src/content/media/StorageBackend.scala
- summercms/src/content/media/LocalStorage.scala
- summercms/src/content/media/S3Storage.scala
- summercms/src/content/media/MediaItem.scala
- summercms/src/content/media/MediaLibrary.scala
- summercms/resources/db/migration/V010__media_library.sql
autonomous: true
must_haves:
truths:
- "Admin can upload files to media library"
- "Files stored via configurable storage backend (local or S3)"
- "Media items organized in folder hierarchy"
- "File metadata tracked in database for listing and search"
artifacts:
- path: "summercms/src/content/media/StorageBackend.scala"
provides: "Storage abstraction trait"
contains: "trait StorageBackend"
- path: "summercms/src/content/media/MediaLibrary.scala"
provides: "Media library service"
contains: "trait MediaLibrary"
- path: "summercms/resources/db/migration/V010__media_library.sql"
provides: "Media metadata tables"
contains: "CREATE TABLE media_files"
key_links:
- from: "MediaLibrary.upload"
to: "StorageBackend.put"
via: "stream to storage"
pattern: "storage\\.put"
---
<objective>
Implement media library core with storage abstraction
Purpose: Enable admin to upload and manage media files. Storage backend is pluggable (local filesystem for dev, S3 for production). This plan focuses on the core storage and library service.
Output: StorageBackend trait with Local/S3 implementations, MediaLibrary service, database migration for metadata.
</objective>
<execution_context>
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/09-content-management/09-RESEARCH.md
@summercms/src/db/QuillContext.scala
@build.mill
</context>
<tasks>
<task type="auto">
<name>Task 1: Add dependencies and create storage backend abstraction</name>
<files>
build.mill
summercms/src/content/media/StorageBackend.scala
summercms/src/content/media/LocalStorage.scala
summercms/src/content/media/S3Storage.scala
summercms/src/content/media/MediaItem.scala
</files>
<action>
**build.mill - Add dependencies:**
```scala
// S3 storage backend
mvn"dev.zio::zio-s3:0.4.2.1"
```
**MediaItem.scala:**
```scala
case class MediaItem(
id: Long,
path: String, // Storage path: "uploads/2024/01/image.jpg"
publicUrl: String, // Public access URL
fileName: String, // Original file name
size: Long,
mimeType: String,
folder: String, // Folder path: "uploads/2024/01"
itemType: MediaItemType,
width: Option[Int], // For images
height: Option[Int], // For images
createdAt: Instant,
updatedAt: Instant
)
enum MediaItemType:
case File, Image, Video, Audio, Document
object MediaItemType:
def fromMimeType(mime: String): MediaItemType = mime match
case m if m.startsWith("image/") => Image
case m if m.startsWith("video/") => Video
case m if m.startsWith("audio/") => Audio
case m if m.startsWith("application/pdf") => Document
// ... more mappings
case _ => File
```
**StorageBackend.scala:**
```scala
trait StorageBackend:
def put(path: String, content: ZStream[Any, Throwable, Byte], contentType: String): IO[StorageError, Unit]
def get(path: String): IO[StorageError, ZStream[Any, Throwable, Byte]]
def delete(path: String): IO[StorageError, Unit]
def exists(path: String): IO[StorageError, Boolean]
def url(path: String): IO[StorageError, String]
def list(folder: String): IO[StorageError, List[String]]
enum StorageError:
case NotFound(path: String)
case AccessDenied(path: String)
case WriteError(path: String, cause: Throwable)
case ReadError(path: String, cause: Throwable)
```
**LocalStorage.scala:**
Implementation using java.nio.file:
- basePath: Path (e.g., /var/www/media)
- publicUrlPrefix: String (e.g., /media)
- put: Create directories, stream to file
- get: Stream from file
- delete: Files.delete with existence check
- url: Combine publicUrlPrefix with path
- Validate paths don't escape basePath (security: prevent path traversal)
**S3Storage.scala:**
Implementation using zio-s3:
- bucket: String
- region: String
- Provide ZLayer for S3 client
- put: Use putObject with streaming
- get: Use getObject
- delete: Use deleteObject
- url: Generate public URL or presigned URL
- List uses listObjectsV2
Note: S3 implementation should be optional - compile without AWS credentials. Use config to select backend.
</action>
<verify>
`./mill summercms.compile` succeeds with new dependencies
LocalStorage can write/read files (unit test or manual verification)
</verify>
<done>
StorageBackend trait with LocalStorage and S3Storage implementations. MediaItem model for file metadata.
</done>
</task>
<task type="auto">
<name>Task 2: Implement MediaLibrary service and database migration</name>
<files>
summercms/src/content/media/MediaLibrary.scala
summercms/resources/db/migration/V010__media_library.sql
</files>
<action>
**V010__media_library.sql:**
```sql
CREATE TABLE media_files (
id BIGSERIAL PRIMARY KEY,
path VARCHAR(500) UNIQUE NOT NULL,
file_name VARCHAR(255) NOT NULL,
size BIGINT NOT NULL,
mime_type VARCHAR(100) NOT NULL,
folder VARCHAR(500) NOT NULL,
item_type VARCHAR(20) NOT NULL,
width INT,
height INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE media_folders (
id BIGSERIAL PRIMARY KEY,
path VARCHAR(500) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
parent_path VARCHAR(500),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_media_files_folder ON media_files(folder);
CREATE INDEX idx_media_files_type ON media_files(item_type);
CREATE INDEX idx_media_folders_parent ON media_folders(parent_path);
```
**MediaLibrary.scala:**
```scala
trait MediaLibrary:
def upload(folder: String, fileName: String, content: ZStream[Any, Throwable, Byte], contentType: String): IO[MediaError, MediaItem]
def list(folder: String, filter: Option[MediaItemType] = None, page: Int = 1, perPage: Int = 50): IO[MediaError, MediaPage]
def get(id: Long): IO[MediaError, Option[MediaItem]]
def delete(ids: List[Long]): IO[MediaError, Unit]
def move(id: Long, newFolder: String): IO[MediaError, MediaItem]
def createFolder(path: String): IO[MediaError, MediaFolder]
def listFolders(parent: Option[String]): IO[MediaError, List[MediaFolder]]
case class MediaPage(items: List[MediaItem], total: Long, page: Int, perPage: Int)
case class MediaFolder(id: Long, path: String, name: String, parentPath: Option[String])
```
Live implementation:
- Generate unique path: folder/YYYY/MM/uuid-filename
- Upload to storage backend
- Insert metadata into database
- Return MediaItem with public URL
</action>
<verify>
`./mill summercms.compile` succeeds
Migration file exists and is syntactically valid
</verify>
<done>
MediaLibrary service handles uploads and metadata tracking. Database migration creates tables for files and folders.
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. `./mill summercms.compile` - all code compiles
2. Migration V010 ready for Flyway
3. Test local storage: upload file, verify on disk, retrieve via URL
4. Test folder operations: create folder, list contents
</verification>
<success_criteria>
- Files upload via MediaLibrary to configurable storage backend
- LocalStorage writes to local filesystem with path traversal protection
- S3Storage integrates with AWS S3 (compile-time only, runtime optional)
- Media metadata tracked in database
- Folder hierarchy supported
</success_criteria>
<output>
After completion, create `.planning/phases/09-content-management/09-03-SUMMARY.md`
</output>