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
| Path | Role |
|---|---|
lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/{FileController,TextbookFolderController}.java | Public HTTP surface |
lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/{FileCommandService,FileQueryService,FileMetadataLookupService}.java | Upload, download, authorization, and metadata logic |
lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/{TextbookFolderCommandService,TextbookFolderQueryService}.java | Folder tree commands and reads |
lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/internal/FileServiceAdapter.java | Published in-process file API used by other modules |
lumie-backend/modules/file/src/main/java/com/lumie/file/domain/entity/{FileMetadata,FileLink,FileDownload,TextbookFolder}.java | Main file aggregates |
lumie-backend/modules/file/src/main/java/com/lumie/file/domain/vo/FilePath.java | Object key builder for MinIO-backed file storage |
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sql | Base 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}.sql | Reusable link and download-history follow-up tables |
Public Surface
| Endpoint | Purpose |
|---|---|
POST /v1/files/upload | Direct proxy upload through the backend |
POST /v1/files/presigned-upload | Create metadata plus a presigned object-storage upload URL |
POST /v1/files/presigned-download | Create a presigned object-storage download URL for an authorized file |
POST /v1/files | Mark 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}/download | Stream an authorized download through the backend |
GET /v1/files/{fileId}/downloaders | Return 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
| Surface | Role |
|---|---|
lumie-backend/libs/internal-api/src/main/java/com/lumie/file/api/FileService.java | Published in-process contract for deleting files, deleting links, and replacing reusable file links |
StoragePort | Object-storage abstraction used for direct uploads, presigned URLs, and object deletion |
ContentService | Used by FileMetadataLookupService to authorize student downloads for announcements and Q&A |
StudentService | Used by getFileDownloaders(...) to resolve student names for download history |
Aggregates And Tables
| Aggregate | Notes |
|---|---|
FileMetadata | Tenant-owned file row with storage object key and upload-completed flag |
FileLink | Reusable attachment link from an existing source file to another entity |
FileDownload | Per-user download history and count |
TextbookFolder | Tenant 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/uploadrejects 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. FileMetadataLookupServiceonly 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 byfile_links.FileServiceAdapterdefers 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
NoSuchKeywhile 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. PresignedUploadResponsestill exposesfileId,uploadUrl, andexpiresInSeconds, andFileMetadataResponse.uploadCompletedflips totrueafterregisterUploadCompleted(...).