Skip to content

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());
  }
}

TIENIPIA QUALIFIED STANDARD