Skip to main content

File Service

This is the reference page for lumie-backend/modules/file, the tenant-scoped module that owns file metadata, object-storage upload and download flows, reusable file links, download history, and textbook folders.

Source Paths

PathRole
lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/{FileController,TextbookFolderController}.javaPublic HTTP surface
lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/{FileCommandService,FileQueryService,FileMetadataLookupService}.javaUpload, download, authorization, and metadata logic
lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/{TextbookFolderCommandService,TextbookFolderQueryService}.javaFolder tree commands and reads
lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/internal/FileServiceAdapter.javaPublished in-process file API used by other modules
lumie-backend/modules/file/src/main/java/com/lumie/file/domain/entity/{FileMetadata,FileLink,FileDownload,TextbookFolder}.javaMain file aggregates
lumie-backend/modules/file/src/main/java/com/lumie/file/domain/vo/FilePath.javaObject key builder for MinIO-backed file storage
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sqlBase creation of file_metadata and textbook_folders with RLS
lumie-backend/app/src/main/resources/db/migration/public/{V54__create_file_links,V63__create_file_download_table,V64__add_download_count_to_file_download}.sqlReusable link and download-history follow-up tables

Public Surface

EndpointPurpose
POST /v1/files/uploadDirect proxy upload through the backend
POST /v1/files/presigned-uploadCreate metadata plus a presigned object-storage upload URL
POST /v1/files/presigned-downloadCreate a presigned object-storage download URL for an authorized file
POST /v1/filesMark a presigned upload as completed after the object exists in storage
GET /v1/files/{fileId}, GET /v1/files, DELETE /v1/files/{fileId}Metadata read, list, and delete
GET /v1/files/{fileId}/downloadStream an authorized download through the backend
GET /v1/files/{fileId}/downloadersReturn download history with student names for staff users
GET /v1/files/public/{fileId}Anonymous public file delivery for explicitly allowlisted entity types
POST /v1/textbook-folders, GET /v1/textbook-folders, GET /v1/textbook-folders/{id}, GET /v1/textbook-folders/{id}/path, PATCH /v1/textbook-folders/{id}, DELETE /v1/textbook-folders/{id}Textbook folder tree management

Internal Surface And Dependencies

SurfaceRole
lumie-backend/libs/internal-api/src/main/java/com/lumie/file/api/FileService.javaPublished in-process contract for deleting files, deleting links, and replacing reusable file links
StoragePortObject-storage abstraction used for direct uploads, presigned URLs, and object deletion
ContentServiceUsed by FileMetadataLookupService to authorize student downloads for announcements and Q&A
StudentServiceUsed by getFileDownloaders(...) to resolve student names for download history

Aggregates And Tables

AggregateNotes
FileMetadataTenant-owned file row with storage object key and upload-completed flag
FileLinkReusable attachment link from an existing source file to another entity
FileDownloadPer-user download history and count
TextbookFolderTenant folder tree capped at depth 5

Runtime Flow

Contract Notes

The anonymous public download surface is intentionally narrow.

// lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/FileController.java
private static final java.util.Set<EntityType> PUBLIC_ALLOWED_TYPES =
java.util.Set.of(EntityType.HOMEPAGE_ASSET);

That route is for explicitly public assets such as homepage images, not for arbitrary tenant files.

Storage identity is separate from the visible filename. FilePath.toObjectKey() builds MinIO object keys as tenant/entityType/fileId/filename, while FileMetadata.originalFilename preserves the user-facing filename. Same-named uploads are allowed because fileId makes each object key unique.

Example Contracts

These examples come directly from FileController, PresignedUploadRequest, PresignedUploadResponse, RegisterUploadRequest, and FileMetadataResponse.

Presigned Upload

POST /v1/files/presigned-upload
Content-Type: application/json

{
"entityType": "HOMEPAGE_ASSET",
"entityId": 15,
"filename": "hero.png",
"contentType": "image/png",
"fileSize": 24576
}
HTTP/1.1 201 Created

{
"fileId": "<uuid>",
"uploadUrl": "https://<object-store>/...",
"expiresInSeconds": <seconds>
}

Upload Completion

POST /v1/files only marks a presigned upload complete after FileCommandService.registerUploadCompleted(...) confirms the object exists.

POST /v1/files
Content-Type: application/json

{
"fileId": "<uuid>"
}
HTTP/1.1 200 OK

{
"id": "<uuid>",
"entityType": "HOMEPAGE_ASSET",
"entityId": 15,
"folderId": null,
"originalFilename": "hero.png",
"contentType": "image/png",
"fileSize": 24576,
"uploadCompleted": true,
"createdAt": "<timestamp>"
}

Failure, Retry, And Observability

  • POST /v1/files/upload rejects files larger than 1 MiB and tells callers to switch to the presigned-upload flow.
  • If a direct upload uses Idempotency-Key, the controller fingerprints file bytes and metadata; reusing a key with different content becomes an idempotency conflict.
  • FileMetadataLookupService only authorizes student downloads for files attached to visible announcements or Q&A entries. Staff users bypass that content-specific check.
  • deleteFile(...) refuses to delete a file that is still referenced by file_links.
  • FileServiceAdapter defers MinIO cleanup until after the caller's DB transaction commits, but textbook-folder recursive deletion deletes objects inline during the transaction. That asymmetry matters when investigating storage-versus-database drift after failures.
  • getFileDownloaders(...) is staff-only and returns "삭제된 학생" when the user record no longer resolves through the student module.
  • If a download fails with MinIO NoSuchKey while metadata exists, use the object-key collision troubleshooting guide before changing authorization or folder queries.

Verification

cd lumie-backend
./gradlew :modules:file:test
./gradlew :modules:file:test --tests '*FileController*'
./gradlew :modules:file:test --tests '*TextbookFolder*'

Expected success signals:

  • Gradle exits with BUILD SUCCESSFUL, and the file module tests still cover controller-level upload and folder flows.
  • PresignedUploadResponse still exposes fileId, uploadUrl, and expiresInSeconds, and FileMetadataResponse.uploadCompleted flips to true after registerUploadCompleted(...).