ありがとうございます。
ここから 第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に依存させません。
悪い例:
を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を利用します。
必要情報:
| Account ID
Access Key
Secret Key
Bucket Name
Endpoint URL
|
6. Gradle依存関係
AWS SDK S3互換を利用します。
| 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設計
同じファイルでも用途を区別します。
例:
| 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
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. 外部動画
対象:
保存:
| 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
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:
| 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
+-------------+
|
変更箇所:
| R2StorageProvider
↓
AWSStorageProvider
|
のみ。
26. 第24段階終了時点
実装可能:
✅ 共通ファイル管理
✅ Cloudflare R2対応
✅ S3互換設計
✅ ファイルアップロード
✅ ファイルダウンロード制御
✅ PDF/Office/ZIP管理
✅ 動画URL管理
✅ HLS動画配信基盤
✅ ストレージ交換可能設計
✅ セキュア教材配信基盤
次の 第25段階 では、教師・管理者向けの運用機能を設計します。
内容:
-
教師ダッシュボード
-
教材作成一覧
-
審査ワークフロー
-
管理者承認画面
-
差分確認(Version比較)
-
コメント・修正依頼
-
監査ログ
-
操作履歴管理
へ進みます。