Skip to content

9.2. ストレージ構造

9.2.1. 保存パス戦略

日付ベースのディレクトリ構造でファイルを分散保存します。

/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. メタデータテーブル

ファイルメタデータを管理するテーブルを作成します。

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. ファイル照会/ダウンロードAPI

java
@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
@Tag(name = "ファイル", description = "ファイルアップロード/ダウンロードAPI")
public class FileController {

  private final FileService fileService;

  @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  @Operation(summary = "ファイルアップロード")
  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 = "ファイルダウンロード")
  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