9.2. Storage Structure
9.2.1. Storage Path Strategy
Files are distributed and stored using a date-based directory structure.
/data/uploads/
├── 2026/
│ └── 02/
│ ├── 28/
│ │ ├── 550e8400-e29b-41d4-a716-446655440000.pdf
│ │ └── 6ba7b810-9dad-11d1-80b4-00c04fd430c8.png
│ └── 27/
│ └── ...
└── ...java
public class StoragePath {
public static Path resolve(String basePath, String storedFilename) {
LocalDate now = LocalDate.now();
return Path.of(basePath,
String.valueOf(now.getYear()),
String.format("%02d", now.getMonthValue()),
String.format("%02d", now.getDayOfMonth()),
storedFilename);
}
}9.2.2. Metadata Table
A table must be created to manage file metadata.
sql
-- V10__create_file_metadata_table.sql
CREATE TABLE file_metadata (
id BIGSERIAL PRIMARY KEY,
original_filename VARCHAR(500) NOT NULL,
stored_filename VARCHAR(255) NOT NULL,
file_path VARCHAR(1000) NOT NULL,
content_type VARCHAR(255) NOT NULL,
file_size BIGINT NOT NULL,
uploaded_by BIGINT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT fk_file_metadata_users
FOREIGN KEY (uploaded_by) REFERENCES users (id)
);
CREATE INDEX idx_file_metadata_uploaded_by ON file_metadata (uploaded_by);9.2.3. File Retrieval / Download API
java
@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
@Tag(name = "Files", description = "File upload/download API")
public class FileController {
private final FileService fileService;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "Upload file")
public ResponseEntity<FileResponse> upload(
@RequestParam("file") MultipartFile file,
@AuthenticationPrincipal UserPrincipal principal) {
FileResponse response = fileService.upload(file, principal.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@GetMapping("/{id}")
@Operation(summary = "Download file")
public ResponseEntity<Resource> download(@PathVariable Long id) {
FileDownload download = fileService.download(id);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(download.contentType()))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + download.originalFilename() + "\"")
.body(download.resource());
}
}