コンテンツにスキップ

ありがとうございます。

ここから 第24段階:ファイル管理・動画配信基盤設計(Cloudflare R2・ストレージ抽象化) に進みます。

今回は、教材管理システムで重要になる以下を設計します。

  • 動画教材の配信

  • PDFなど教材ファイル管理

  • Cloudflare R2利用

  • 将来的なAWS S3等への変更対応

  • 非公開ファイルの安全な配信

  • 添付ファイル共通管理


第24段階 全体構成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
教師

 ↓

アップロード画面

 ↓

StorageService

 ↓

StorageProvider

 ↓

+----------------+

| Local Storage  |

| Cloudflare R2  |

| AWS S3         |

+----------------+

 ↓

Attachment

 ↓

ContentAttachment

 ↓

教材表示

1. 設計方針

ストレージ処理を直接Cloudflare R2に依存させません。

悪い例:

1
R2Client.upload()

をServiceから直接呼ぶ。

将来変更が困難。


採用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Application

↓

StorageService

↓

StorageProvider

↓

R2実装

2. ストレージ関連パッケージ

追加:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
infrastructure

└ storage

    ├ StorageService.java

    ├ StorageProvider.java

    ├ R2StorageProvider.java

    ├ LocalStorageProvider.java

    ├ StorageProperties.java

    └ StoredFile.java

3. StorageProvider Interface

ストレージ共通インターフェースです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface StorageProvider {


    String upload(
        MultipartFile file,
        String directory
    );


    Resource download(
        String path
    );


    void delete(
        String path
    );


    String createAccessUrl(
        String path
    );

}

4. StorageService

業務層から利用する窓口です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Service
@RequiredArgsConstructor
public class StorageService {


private final StorageProvider provider;



public String upload(
MultipartFile file,
String directory){


return provider.upload(
file,
directory
);


}



public Resource download(
String path){


return provider.download(
path
);


}


}

5. Cloudflare R2設定

R2はS3互換APIを利用します。

必要情報:

1
2
3
4
5
6
7
8
9
Account ID

Access Key

Secret Key

Bucket Name

Endpoint URL

6. Gradle依存関係

AWS SDK S3互換を利用します。

1
2
3
4
5
6
7
dependencies {

implementation(
"software.amazon.awssdk:s3:2.25.60"
)

}

7. application.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
storage:

  type: r2

  r2:

    endpoint:
      https://xxxxxxxx.r2.cloudflarestorage.com

    access-key:
      ${R2_ACCESS_KEY}

    secret-key:
      ${R2_SECRET_KEY}

    bucket:
      lms-content

8. StorageProperties

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@ConfigurationProperties(
prefix="storage"
)
@Getter
@Setter
@Component
public class StorageProperties {


private String type;


private R2 r2;


@Getter
@Setter
public static class R2 {


private String endpoint;


private String accessKey;


private String secretKey;


private String bucket;


}

}

9. R2StorageProvider

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Component
@RequiredArgsConstructor
public class R2StorageProvider
implements StorageProvider {


private final S3Client s3Client;


private final StorageProperties properties;



@Override
public String upload(
MultipartFile file,
String directory){


String key =
directory
+
"/"
+
UUID.randomUUID()
+
"_"
+
file.getOriginalFilename();



s3Client.putObject(

PutObjectRequest.builder()

.bucket(
properties.getR2()
.getBucket()
)

.key(key)

.contentType(
file.getContentType()
)

.build(),


RequestBody.fromBytes(
file.getBytes()
)

);



return key;

}

}

10. 添付ファイル設計

以前設計したAttachmentを拡張します。


Attachment Entity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Entity
@Table(name="attachments")
@Getter
@Setter
public class Attachment {


@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;


private String originalName;


private String storedName;


private String extension;


private String contentType;


private Long fileSize;


private String storagePath;


private String storageType;


private LocalDateTime uploadedAt;


}

11. Attachmentテーブル

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE attachments (

id BIGINT AUTO_INCREMENT PRIMARY KEY,

original_name VARCHAR(255),

stored_name VARCHAR(255),

extension VARCHAR(20),

content_type VARCHAR(100),

file_size BIGINT,

storage_path VARCHAR(500),

storage_type VARCHAR(30),

uploaded_at DATETIME

);

12. ContentAttachment

教材との関連。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entity
@Table(name="content_attachments")
@Getter
@Setter
public class ContentAttachment {


@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;


@ManyToOne
@JoinColumn(
name="content_version_id"
)
private ContentVersion contentVersion;


@ManyToOne
@JoinColumn(
name="attachment_id"
)
private Attachment attachment;


private String role;


}

13. role設計

同じファイルでも用途を区別します。

例:

1
2
3
4
5
6
7
8
9
THUMBNAIL

VIDEO

DOCUMENT

REFERENCE

SOURCE_CODE

14. ファイルアップロード処理

流れ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
教師

↓

PDF選択

↓

Controller

↓

Service

↓

StorageService

↓

R2

↓

Attachment保存

↓

ContentAttachment登録

15. FileUploadService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Service
@RequiredArgsConstructor
public class FileUploadService {


private final StorageService storageService;


private final AttachmentRepository repository;



@Transactional
public Attachment upload(
MultipartFile file){


String path =
storageService.upload(
file,
"content"
);



Attachment attachment =
new Attachment();


attachment.setOriginalName(
file.getOriginalFilename()
);


attachment.setStoragePath(
path
);


attachment.setContentType(
file.getContentType()
);


attachment.setFileSize(
file.getSize()
);



return repository.save(
attachment
);


}

}

16. ファイルダウンロード制御

直接URL公開は禁止します。

悪い例:

1
https://r2.xxx/file.pdf

誰でも取得可能。


推奨:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ブラウザ

↓

Spring Controller

↓

権限確認

↓

期限付きURL発行

↓

R2取得

17. DownloadController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Controller
@RequiredArgsConstructor
public class DownloadController {


private final AttachmentService service;



@GetMapping(
"/download/{id}"
)
public ResponseEntity<Resource> download(
@PathVariable Long id){


Resource resource =
service.download(id);



return ResponseEntity.ok()
.body(resource);

}

}

18. 動画配信方式

動画は2種類対応します。


A. 外部動画

対象:

  • YouTube

  • Vimeo

保存:

1
2
3
4
5
video_type

video_id

video_url

表示:

iframe


B. R2動画

推奨:

HLS形式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
mp4

↓

FFmpeg

↓

m3u8

+

ts

↓

ブラウザ再生

19. 動画変換フロー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
教師

↓

movie.mp4

↓

Upload

↓

FFmpeg

↓

/video/course01/

    index.m3u8

    000.ts

    001.ts

↓

R2保存

↓

Video.js再生

20. VideoContent拡張

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entity
public class VideoContent {


@Id
private Long id;


@OneToOne
@MapsId
private ContentVersion contentVersion;


@Enumerated(EnumType.STRING)
private VideoType videoType;


private String videoId;


private String videoUrl;


private String streamPath;


private Integer durationSeconds;


}

21. VideoType

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public enum VideoType {


YOUTUBE,


VIMEO,


R2_HLS

}

22. HLS再生画面

利用:

  • Video.js

  • hls.js

例:

1
2
3
4
5
<video
id="player"
controls>

</video>

JavaScript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const video =
document.getElementById(
"player"
);


const hls =
new Hls();


hls.loadSource(
streamUrl
);


hls.attachMedia(
video
);

23. セキュリティ対策

ファイル

確認:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ログイン済み

↓

受講登録あり

↓

教材利用権あり

↓

Download許可

動画

対策:

対策 内容
署名URL 期限付き
Token 利用者識別
Referer制限 直リンク防止
暗号化 HLS AES対応

24. ファイルサイズ制限

application.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
spring:

 servlet:

  multipart:

   max-file-size:
    500MB

   max-request-size:
    500MB

25. 拡張可能な構成

将来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
StorageProvider

      |

+-------------+

Local

R2

AWS S3

Azure Blob

MinIO

+-------------+

変更箇所:

1
2
3
4
5
R2StorageProvider

↓

AWSStorageProvider

のみ。


26. 第24段階終了時点

実装可能:

✅ 共通ファイル管理
✅ Cloudflare R2対応
✅ S3互換設計
✅ ファイルアップロード
✅ ファイルダウンロード制御
✅ PDF/Office/ZIP管理
✅ 動画URL管理
✅ HLS動画配信基盤
✅ ストレージ交換可能設計
✅ セキュア教材配信基盤


次の 第25段階 では、教師・管理者向けの運用機能を設計します。

内容:

  1. 教師ダッシュボード

  2. 教材作成一覧

  3. 審査ワークフロー

  4. 管理者承認画面

  5. 差分確認(Version比較)

  6. コメント・修正依頼

  7. 監査ログ

  8. 操作履歴管理

へ進みます。