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

7.8 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
09-content-management 03 execute 1
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
true
truths artifacts key_links
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
path provides contains
summercms/src/content/media/StorageBackend.scala Storage abstraction trait trait StorageBackend
path provides contains
summercms/src/content/media/MediaLibrary.scala Media library service trait MediaLibrary
path provides contains
summercms/resources/db/migration/V010__media_library.sql Media metadata tables CREATE TABLE media_files
from to via pattern
MediaLibrary.upload StorageBackend.put stream to storage storage.put
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.

<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/09-content-management/09-RESEARCH.md @summercms/src/db/QuillContext.scala @build.mill Task 1: Add dependencies and create storage backend abstraction 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 **build.mill - Add dependencies:** ```scala // S3 storage backend mvn"dev.zio::zio-s3:0.4.2.1" ```

MediaItem.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:

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. ./mill summercms.compile succeeds with new dependencies LocalStorage can write/read files (unit test or manual verification) StorageBackend trait with LocalStorage and S3Storage implementations. MediaItem model for file metadata.

Task 2: Implement MediaLibrary service and database migration summercms/src/content/media/MediaLibrary.scala summercms/resources/db/migration/V010__media_library.sql **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 ./mill summercms.compile succeeds Migration file exists and is syntactically valid MediaLibrary service handles uploads and metadata tracking. Database migration creates tables for files and folders.
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

<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>
After completion, create `.planning/phases/09-content-management/09-03-SUMMARY.md`