1894 lines
58 KiB
Markdown
1894 lines
58 KiB
Markdown
# Fileserver Platform 설계서
|
|
|
|
**문서 상태:** 설계 확정안
|
|
**작성 기준일:** 2026-08-07
|
|
**입력 근거:** `Spring 기반 Fileserver 설계 심층 리서치`
|
|
**대상 저장소:** Spring 기반 Backend Skeleton
|
|
|
|
---
|
|
|
|
## 1. 요약
|
|
|
|
이 설계는 Fileserver를 단순한 업로드·다운로드 컨트롤러가 아니라 다음 네 계층을 분리한 공통 파일 서비스 플랫폼으로 정의한다.
|
|
|
|
1. **Content Store** — byte stream, staging, range read, publish, delete를 담당한다.
|
|
2. **Metadata Store** — 파일 상태, 소유·권한 연결 정보, 크기, digest, MIME 판정, version, lease, 만료를 관리한다.
|
|
3. **Transfer Adapter** — Spring MVC, Spring WebFlux, Nginx 위임으로 HTTP 전송을 제공한다.
|
|
4. **Verification Layer** — checksum, 형식 판정, 악성 파일 검사, quarantine을 담당한다.
|
|
|
|
공개 API는 `fileId`와 `uploadId`만 사용한다. `Path`, 실제 파일명, 디렉터리, mount 경로, symlink와 같은 파일시스템 개념은 로컬 저장소 어댑터 밖으로 노출하지 않는다. 파일 내용과 메타데이터는 하나의 ACID transaction으로 묶을 수 없으므로 상태 머신, version, writer lease, reconciliation을 통해 일관성을 유지한다.
|
|
|
|
최초 Stable 릴리스는 Linux 로컬 파일시스템과 인증된 Kubernetes PVC RWO를 대상으로 다음을 제공한다.
|
|
|
|
- raw 및 multipart 단일 업로드
|
|
- 제한형 다중 파일 업로드
|
|
- streaming append와 SHA-256 검증
|
|
- GET, HEAD, 단일 Range, 조건부 요청
|
|
- 직접 전송과 Nginx 위임
|
|
- logical delete와 비동기 physical cleanup
|
|
- MVC와 WebFlux 어댑터
|
|
- 다중 인스턴스용 DB version·lease
|
|
- tus 1.0 별도 Stable 모듈
|
|
- NFS·PVC RWX 제한 지원 프로파일
|
|
- IETF resumable upload draft-12 Experimental 모듈
|
|
|
|
---
|
|
|
|
## 2. 목표와 성공 기준
|
|
|
|
### 2.1 목표
|
|
|
|
- 다양한 웹 서비스가 파일 업로드·다운로드를 즉시 사용할 수 있는 공통 기술 모듈을 제공한다.
|
|
- 로컬 디스크, PVC, NFS, 향후 Object Storage가 동일한 저장소 의미론을 공유하도록 한다.
|
|
- 최대 파일 크기에서도 JVM heap 사용량이 파일 크기에 비례하지 않도록 한다.
|
|
- 부분 파일, 경로 탈출, 권한 우회, 검사 전 공개를 구조적으로 차단한다.
|
|
- 장애 후 성공 여부가 모호한 작업을 단순 실패와 구분하고 복구할 수 있게 한다.
|
|
- 구현자가 설계 중 다시 판단하지 않도록 HTTP 계약, 상태 전이, 오류, 설정, 테스트 완료 조건을 고정한다.
|
|
|
|
### 2.2 성공 기준
|
|
|
|
| 영역 | 완료 기준 |
|
|
|---|---|
|
|
| 공개 식별자 | 외부 API가 `fileId`, `uploadId`만 사용하고 실제 경로를 노출하지 않는다. |
|
|
| 업로드 | raw·multipart 스트리밍이 bounded memory로 동작하며 부분 파일은 READY 이전에 읽을 수 없다. |
|
|
| 무결성 | 서버가 actual size와 SHA-256을 계산하고 client digest가 있으면 검증한다. |
|
|
| publish | atomic move probe가 통과하거나 metadata pointer publish를 사용한다. |
|
|
| 다운로드 | `200`, `206`, `304`, `412`, `416`과 관련 header 계약을 일관되게 제공한다. |
|
|
| 보안 | traversal, symlink escape, 원본명 저장, 무조건 overwrite, 검사 전 공개를 차단한다. |
|
|
| 다중 인스턴스 | upload별 단일 writer lease와 metadata version 충돌 검사가 동작한다. |
|
|
| 장애 복구 | process kill, disk full, network interruption 후 READY invariant가 깨지지 않는다. |
|
|
| 운영 | temp, orphan, quota, disk usage, transfer, verification metric과 cleanup job을 제공한다. |
|
|
| 플랫폼 | Linux local과 지정 PVC 프로파일의 인증 테스트를 통과한다. |
|
|
|
|
---
|
|
|
|
## 3. 범위
|
|
|
|
### 3.1 포함 범위
|
|
|
|
- Spring MVC와 Spring WebFlux
|
|
- blocking channel SPI와 async publisher SPI
|
|
- Linux local disk
|
|
- Kubernetes PVC RWO 인증 프로파일
|
|
- 인증된 PVC RWX·NFSv4.1 제한 프로파일
|
|
- Windows NTFS 호환성 CI 프로파일
|
|
- 단일·다중 인스턴스
|
|
- `multipart/form-data`, `application/octet-stream`
|
|
- 단일·제한형 다중 파일 업로드
|
|
- streaming upload, cancellation, status, cleanup
|
|
- GET, HEAD, byte range, conditional request, cache header
|
|
- 애플리케이션 직접 전송, zero-copy capability, Nginx 위임
|
|
- SHA-256, MIME·signature 검사 SPI, AV·CDR SPI
|
|
- quota reservation, concurrency limit, storage high-water 보호
|
|
- tus 1.0
|
|
- IETF resumable upload draft-12 Experimental
|
|
- 관리자 health, orphan scan, reconcile, cleanup, reverify
|
|
- metric, trace, audit, problem detail
|
|
|
|
### 3.2 제외 범위
|
|
|
|
- 공개 API의 임의 절대·상대 경로 입력
|
|
- 공개 디렉터리 list·scan
|
|
- symlink follow·생성
|
|
- hard link 생성
|
|
- 공개 재귀 삭제
|
|
- webroot 내부 저장
|
|
- 원본 파일명 그대로의 physical filename
|
|
- 조건 없는 overwrite
|
|
- READY 이전 다운로드
|
|
- 하나의 offset에 대한 동시 append
|
|
- proxy가 이미 전달한 비멱등 upload의 자동 재시도
|
|
- NFS lock만을 이용한 다중 인스턴스 정합성
|
|
- 다른 `FileStore` 사이의 atomic move 보장
|
|
- copy 실패 시 자동 rollback 보장
|
|
- 모든 파일 형식의 안전성 판정
|
|
- 임의 ZIP extraction
|
|
- Object Storage provider 구현과 signed URL
|
|
- FTP, SFTP, SMB client 기능
|
|
|
|
---
|
|
|
|
## 4. 고정 설계 결정
|
|
|
|
| 항목 | 결정 |
|
|
|---|---|
|
|
| 운영 우선 플랫폼 | Linux |
|
|
| Java | Java 21 |
|
|
| Spring | 6.2 최신 patch와 7.0 최신 patch를 release matrix에서 검증 |
|
|
| MVC | 정식 지원, streaming 전용 `AsyncTaskExecutor` 사용 |
|
|
| WebFlux | 정식 지원, event loop에서 blocking filesystem I/O 금지 |
|
|
| 공통 저장소 계약 | `Path`가 아니라 create·append·finalize·stat·openRead·delete 의미론 |
|
|
| metadata 기준 | 관계형 DB의 metadata가 authoritative |
|
|
| publish 기준 | same-FileStore atomic move 또는 metadata pointer publish |
|
|
| 공개 식별자 | opaque `FileId`, `UploadId` |
|
|
| physical key | 서버가 생성한 `ContentKey` |
|
|
| 원본명 | 비신뢰 표시 metadata |
|
|
| 기본 업로드 | create-only |
|
|
| overwrite | `If-Match` 또는 metadata version 필수 |
|
|
| checksum | 서버 계산 SHA-256 필수, client digest 선택 검증 |
|
|
| ETag | immutable READY bytes의 SHA-256 strong ETag |
|
|
| private cache | `private, no-store` 기본 |
|
|
| 재개 업로드 | tus 1.0 Stable, HTTPbis draft-12 Experimental |
|
|
| 다중 append | 단일 writer lease, 병렬 업로드는 독립 part 후 concatenate 방식만 |
|
|
| 삭제 | logical delete 후 physical cleanup |
|
|
| NFS | 외부 DB version·lease와 reconciliation을 전제로 제한 지원 |
|
|
| Windows | 초기 non-blocking compatibility profile |
|
|
|
|
---
|
|
|
|
## 5. 지원 매트릭스
|
|
|
|
### 5.1 런타임·저장소
|
|
|
|
| 대상 | 지원 수준 | 조건 |
|
|
|---|---|---|
|
|
| Linux ext4/XFS local | 완전 지원 | startup capability probe 통과 |
|
|
| Kubernetes PVC RWO | 조건부 완전 | 지정 CSI·StorageClass·mount option 인증 |
|
|
| Kubernetes PVC RWX | 제한 지원 | 실제 backend별 release certification |
|
|
| NFSv4.1 | 제한 지원 | DB lease·version, ambiguous completion reconciliation |
|
|
| Windows NTFS | 호환성 | nightly test, 운영 지원은 후속 확정 |
|
|
| Nginx stable | 완전 지원 | internal location과 Range 계약 인증 |
|
|
| 단일 인스턴스 | 완전 지원 | process-local serialization 가능 |
|
|
| 다중 인스턴스 | 완전 지원 조건부 | 공유 metadata DB와 writer lease 필수 |
|
|
|
|
### 5.2 프로토콜·기능
|
|
|
|
| 기능 | 수준 | 모듈 |
|
|
|---|---|---|
|
|
| raw upload | Stable | `fileserver-mvc`, `fileserver-webflux` |
|
|
| multipart 단일 | Stable | MVC·WebFlux |
|
|
| multipart batch | Stable 제한형 | 별도 batch endpoint, 비원자적 결과 배열 |
|
|
| direct download | Stable | MVC·WebFlux |
|
|
| single Range | Stable | core HTTP contract |
|
|
| multi Range | Beta | 개수·overlap·총량 budget 필수 |
|
|
| Nginx delegation | Stable | `fileserver-nginx` |
|
|
| tus 1.0 | Stable 별도 모듈 | `fileserver-tus` |
|
|
| HTTPbis draft-12 | Experimental | `fileserver-resumable-httpbis-draft12` |
|
|
| NFS RWX | Limited | 인증 프로파일 |
|
|
| Windows | Compatibility | CI profile |
|
|
|
|
---
|
|
|
|
## 6. 전체 아키텍처
|
|
|
|
```text
|
|
HTTP Client
|
|
│
|
|
├─ Spring MVC Adapter
|
|
├─ Spring WebFlux Adapter
|
|
└─ tus / HTTPbis Adapter
|
|
│
|
|
▼
|
|
Application Services
|
|
├─ UploadApplicationService
|
|
├─ FinalizeUploadService
|
|
├─ DownloadApplicationService
|
|
├─ FileLifecycleService
|
|
├─ CleanupApplicationService
|
|
└─ ReconciliationService
|
|
│
|
|
├───────────────┐
|
|
▼ ▼
|
|
Metadata Store Port Content Store Port
|
|
│ │
|
|
▼ ├─ Local Filesystem Adapter
|
|
JPA Metadata Adapter └─ Future Object Storage Adapter
|
|
│
|
|
├─ Verification Port
|
|
├─ Authorization Port
|
|
├─ Quota Port
|
|
└─ Observability
|
|
|
|
Download path
|
|
Application authorization
|
|
├─ Direct transfer
|
|
└─ Nginx X-Accel-Redirect
|
|
```
|
|
|
|
### 6.1 의존 방향
|
|
|
|
- `fileserver-core-api`는 Spring MVC, WebFlux, JPA, NIO 구현 타입에 의존하지 않는다.
|
|
- `fileserver-application`은 core port만 사용한다.
|
|
- `fileserver-storage-local`은 NIO와 local path를 캡슐화한다.
|
|
- `fileserver-metadata-jpa`는 metadata port를 구현한다.
|
|
- HTTP adapter는 application service만 호출한다.
|
|
- Nginx 모듈은 물리 경로 대신 안전한 internal URI descriptor만 생성한다.
|
|
- 검사·권한·quota 정책은 SPI로 주입하며 Fileserver가 비즈니스 규칙을 내장하지 않는다.
|
|
|
|
### 6.2 업로드 실행 흐름
|
|
|
|
```text
|
|
1. 인증·기술 정책 확인
|
|
2. quota 예약
|
|
3. FileRecord(CREATED)와 UploadSession 생성
|
|
4. ContentStore.createUpload(CREATE_NEW)
|
|
5. FileRecord → UPLOADING
|
|
6. stream append + actual size + SHA-256 계산
|
|
7. channel close
|
|
8. FileRecord → UPLOADED
|
|
9. verification 실행
|
|
10. VERIFYING / QUARANTINED / REJECTED
|
|
11. publish strategy 실행
|
|
12. physical stat 재검증
|
|
13. metadata pointer, size, digest, MIME, version 기록
|
|
14. FileRecord → READY
|
|
15. quota 예약을 committed usage로 전환
|
|
```
|
|
|
|
### 6.3 다운로드 실행 흐름
|
|
|
|
```text
|
|
1. FileId 조회
|
|
2. 존재 은닉 정책을 포함한 authorization
|
|
3. READY 상태 확인
|
|
4. conditional header 평가
|
|
5. Range parsing·budget 검증
|
|
6. transfer mode 선택
|
|
- DIRECT
|
|
- ZERO_COPY capability
|
|
- NGINX_DELEGATED
|
|
7. 응답 header 확정
|
|
8. bytes 전송 또는 internal redirect
|
|
9. 성공·중단·전송량 관측
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 모듈 구조
|
|
|
|
```text
|
|
backend-skeleton/
|
|
├── modules/fileserver/
|
|
│ ├── fileserver-core-api/
|
|
│ ├── fileserver-application/
|
|
│ ├── fileserver-metadata-jpa/
|
|
│ ├── fileserver-storage-local/
|
|
│ ├── fileserver-verification/
|
|
│ ├── fileserver-mvc/
|
|
│ ├── fileserver-webflux/
|
|
│ ├── fileserver-nginx/
|
|
│ ├── fileserver-admin/
|
|
│ ├── fileserver-tus/
|
|
│ ├── fileserver-resumable-httpbis-draft12/
|
|
│ ├── fileserver-spring-boot-starter/
|
|
│ └── fileserver-testkit/
|
|
├── infra/fileserver/
|
|
│ ├── local/
|
|
│ ├── nginx/
|
|
│ ├── nfs/
|
|
│ └── kubernetes/
|
|
└── docs/fileserver/
|
|
├── support-matrix.md
|
|
├── http-contract.md
|
|
├── storage-certification.md
|
|
├── security.md
|
|
├── operations.md
|
|
└── upgrade-guide.md
|
|
```
|
|
|
|
| 모듈 | 책임 |
|
|
|---|---|
|
|
| `fileserver-core-api` | ID, 상태, value object, port, 오류, capability |
|
|
| `fileserver-application` | upload·download·lifecycle orchestration |
|
|
| `fileserver-metadata-jpa` | metadata, lease, quota reservation persistence |
|
|
| `fileserver-storage-local` | staging, append, range read, publish, delete, probe |
|
|
| `fileserver-verification` | digest, MIME verdict, scanner pipeline |
|
|
| `fileserver-mvc` | Servlet multipart/raw/download adapter |
|
|
| `fileserver-webflux` | `PartEvent`, `DataBuffer`, reactive transfer adapter |
|
|
| `fileserver-nginx` | internal URI와 `X-Accel-Redirect` response strategy |
|
|
| `fileserver-admin` | health, orphan, reconcile, cleanup, reverify |
|
|
| `fileserver-tus` | tus 1.0 protocol adapter |
|
|
| `fileserver-resumable-httpbis-draft12` | versioned Experimental protocol adapter |
|
|
| `fileserver-spring-boot-starter` | properties, auto-configuration, startup gate |
|
|
| `fileserver-testkit` | contract, filesystem, HTTP, fault, performance harness |
|
|
|
|
---
|
|
|
|
## 8. 핵심 공개 모델
|
|
|
|
### 8.1 식별자
|
|
|
|
```java
|
|
public record FileId(UUID value) {
|
|
public FileId {
|
|
Objects.requireNonNull(value, "value");
|
|
}
|
|
}
|
|
|
|
public record UploadId(UUID value) {
|
|
public UploadId {
|
|
Objects.requireNonNull(value, "value");
|
|
}
|
|
}
|
|
|
|
public record ContentKey(String value) {
|
|
public ContentKey {
|
|
if (value == null || !value.matches("[a-z0-9/_-]{16,200}")) {
|
|
throw new IllegalArgumentException("invalid content key");
|
|
}
|
|
}
|
|
}
|
|
|
|
public record StorageNamespace(String value) {
|
|
public StorageNamespace {
|
|
if (value == null || !value.matches("[a-z][a-z0-9-]{1,62}")) {
|
|
throw new IllegalArgumentException("invalid storage namespace");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`ContentKey`는 public HTTP contract에 포함하지 않는다. `FileId`는 추측하기 어려운 ID를 사용하지만 비밀 token으로 취급하지 않으며 모든 요청에서 authorization을 수행한다.
|
|
|
|
### 8.2 파일 상태
|
|
|
|
```java
|
|
public enum FileState {
|
|
CREATED,
|
|
UPLOADING,
|
|
UPLOADED,
|
|
VERIFYING,
|
|
QUARANTINED,
|
|
READY,
|
|
REJECTED,
|
|
FAILED,
|
|
DELETING,
|
|
DELETED,
|
|
EXPIRED
|
|
}
|
|
```
|
|
|
|
허용 전이는 `FileStateMachine` 하나에서 관리한다. persistence adapter나 controller가 상태를 직접 대입하지 않는다.
|
|
|
|
```java
|
|
public interface FileStateMachine {
|
|
void requireTransition(FileState current, FileState target);
|
|
boolean canTransition(FileState current, FileState target);
|
|
}
|
|
```
|
|
|
|
### 8.3 ByteRange
|
|
|
|
```java
|
|
public record ByteRange(long startInclusive, long endInclusive) {
|
|
public ByteRange {
|
|
if (startInclusive < 0 || endInclusive < startInclusive) {
|
|
throw new IllegalArgumentException("invalid byte range");
|
|
}
|
|
}
|
|
|
|
public long length() {
|
|
return Math.addExact(Math.subtractExact(endInclusive, startInclusive), 1);
|
|
}
|
|
}
|
|
```
|
|
|
|
HTTP suffix/open-ended Range는 HTTP adapter의 parser가 현재 representation 길이를 기준으로 위 value object로 정규화한다.
|
|
|
|
### 8.4 파일 metadata
|
|
|
|
```java
|
|
public record FileDescriptor(
|
|
FileId fileId,
|
|
StorageNamespace namespace,
|
|
FileState state,
|
|
String originalFilename,
|
|
String mediaType,
|
|
long size,
|
|
String sha256,
|
|
String strongEtag,
|
|
Instant publishedAt,
|
|
long version
|
|
) {}
|
|
```
|
|
|
|
실제 path, scanner 원문 응답, user metadata 원문은 public descriptor에 포함하지 않는다.
|
|
|
|
---
|
|
|
|
## 9. 상태 머신과 invariant
|
|
|
|
### 9.1 상태 전이
|
|
|
|
```text
|
|
CREATED → UPLOADING
|
|
UPLOADING → UPLOADED | FAILED | EXPIRED | DELETING
|
|
UPLOADED → VERIFYING | FAILED | DELETING
|
|
VERIFYING → READY | QUARANTINED | REJECTED | FAILED
|
|
QUARANTINED → VERIFYING | READY | REJECTED | DELETING
|
|
READY → DELETING
|
|
REJECTED → DELETING
|
|
FAILED → UPLOADING | VERIFYING | DELETING | EXPIRED
|
|
DELETING → DELETED | FAILED
|
|
EXPIRED → DELETING
|
|
```
|
|
|
|
`FAILED`에서의 복구 전이는 저장된 `lastErrorCode`와 recovery policy가 허용할 때만 수행한다.
|
|
|
|
### 9.2 필수 invariant
|
|
|
|
- READY에는 읽을 수 있는 immutable content가 존재한다.
|
|
- READY의 size와 SHA-256은 실제 bytes와 일치한다.
|
|
- READY가 아닌 레코드는 direct download와 Nginx internal mapping에서 제외된다.
|
|
- 하나의 upload에는 하나의 유효 writer lease만 존재한다.
|
|
- offset은 durable append가 확인된 byte 수만큼만 증가한다.
|
|
- client가 주장한 크기·MIME·파일명은 authoritative 값이 아니다.
|
|
- REJECTED, DELETED, EXPIRED는 public API에서 재활성화되지 않는다.
|
|
- DB와 storage가 불일치하면 READY를 추정하지 않고 recovery queue로 보낸다.
|
|
- logical delete가 성공하면 신규 download authorization은 즉시 차단된다.
|
|
- physical cleanup 실패는 DELETING 또는 FAILED 상태와 운영 경보로 남는다.
|
|
|
|
---
|
|
|
|
## 10. Metadata Store 설계
|
|
|
|
### 10.1 Port
|
|
|
|
```java
|
|
public interface FileMetadataStore {
|
|
FileRecord insert(FileRecordDraft draft);
|
|
Optional<FileRecord> find(FileId fileId);
|
|
FileRecord transition(
|
|
FileId fileId,
|
|
long expectedVersion,
|
|
FileState expectedState,
|
|
FileState targetState,
|
|
FileRecordMutation mutation
|
|
);
|
|
FileRecord markDeleting(FileId fileId, long expectedVersion);
|
|
List<FileRecord> findRecoverable(FileRecoveryQuery query);
|
|
}
|
|
|
|
public interface UploadSessionStore {
|
|
UploadSession create(UploadSessionDraft draft);
|
|
Optional<UploadSession> find(UploadId uploadId);
|
|
WriterLease acquireLease(
|
|
UploadId uploadId,
|
|
String owner,
|
|
Instant now,
|
|
Duration leaseDuration,
|
|
long expectedVersion
|
|
);
|
|
UploadSession commitOffset(
|
|
UploadId uploadId,
|
|
WriterLease lease,
|
|
long expectedOffset,
|
|
long committedOffset
|
|
);
|
|
void releaseLease(UploadId uploadId, WriterLease lease);
|
|
List<UploadSession> findExpired(Instant cutoff, int limit);
|
|
}
|
|
```
|
|
|
|
### 10.2 관계형 schema
|
|
|
|
| Table | 핵심 컬럼 |
|
|
|---|---|
|
|
| `fs_file` | `file_id`, `namespace`, `state`, `content_key`, `original_name`, `claimed_media_type`, `verified_media_type`, `expected_size`, `actual_size`, `sha256`, `strong_etag`, `published_at`, `version`, `last_error_code`, timestamps |
|
|
| `fs_upload_session` | `upload_id`, `file_id`, `expected_length`, `committed_offset`, `protocol`, `expires_at`, `lease_owner`, `lease_until`, `version` |
|
|
| `fs_verification_result` | `file_id`, `verifier`, `verdict`, `details_code`, `started_at`, `completed_at` |
|
|
| `fs_quota_reservation` | `reservation_id`, `scope`, `reserved_bytes`, `committed_bytes`, `expires_at`, `status`, `version` |
|
|
| `fs_cleanup_item` | `cleanup_id`, `file_id`, `content_key`, `type`, `attempt`, `next_attempt_at`, `status`, `last_error_code` |
|
|
|
|
`fs_file.version`과 `fs_upload_session.version`은 optimistic locking에 사용한다. 모든 상태 전이는 `WHERE version = ? AND state = ?` 조건을 포함한다.
|
|
|
|
### 10.3 authoritative source
|
|
|
|
- 공개 metadata는 `fs_file`을 기준으로 한다.
|
|
- physical `stat`은 publish 검증과 reconciliation에 사용한다.
|
|
- NFS·PVC의 timestamp는 Last-Modified의 authoritative source로 사용하지 않는다.
|
|
- `published_at`을 HTTP Last-Modified로 사용한다.
|
|
|
|
|
|
## 11. Content Store Port
|
|
|
|
### 11.1 Capability
|
|
|
|
```java
|
|
public record ContentStoreCapabilities(
|
|
boolean rangedRead,
|
|
boolean atomicCreate,
|
|
boolean atomicPublish,
|
|
boolean conditionalWrite,
|
|
boolean serverSideCopy,
|
|
boolean delegatedDownload,
|
|
boolean resumableAppend
|
|
) {}
|
|
```
|
|
|
|
Capability는 설정값만 읽지 않고 실제 저장소 root에서 startup probe한 결과로 생성한다.
|
|
|
|
### 11.2 Blocking SPI
|
|
|
|
```java
|
|
public interface BlockingContentStore {
|
|
UploadHandle createUpload(CreateContentCommand command);
|
|
|
|
AppendResult append(
|
|
UploadHandle handle,
|
|
long expectedOffset,
|
|
ReadableByteChannel source,
|
|
long contentLength
|
|
);
|
|
|
|
StoredContent finalizeUpload(
|
|
UploadHandle handle,
|
|
FinalizeContentCommand command
|
|
);
|
|
|
|
ContentMetadata stat(ContentKey key);
|
|
|
|
ReadableByteChannel openRead(ContentKey key, ByteRange range);
|
|
|
|
DeleteResult delete(ContentKey key, DeletePrecondition precondition);
|
|
|
|
ContentStoreCapabilities capabilities();
|
|
}
|
|
```
|
|
|
|
### 11.3 Async SPI
|
|
|
|
```java
|
|
public interface AsyncContentStore {
|
|
CompletionStage<UploadHandle> createUpload(CreateContentCommand command);
|
|
|
|
CompletionStage<AppendResult> append(
|
|
UploadHandle handle,
|
|
long expectedOffset,
|
|
Flow.Publisher<ByteBuffer> content
|
|
);
|
|
|
|
CompletionStage<StoredContent> finalizeUpload(
|
|
UploadHandle handle,
|
|
FinalizeContentCommand command
|
|
);
|
|
|
|
CompletionStage<ContentMetadata> stat(ContentKey key);
|
|
|
|
Flow.Publisher<ByteBuffer> openRead(ContentKey key, ByteRange range);
|
|
|
|
CompletionStage<DeleteResult> delete(
|
|
ContentKey key,
|
|
DeletePrecondition precondition
|
|
);
|
|
|
|
ContentStoreCapabilities capabilities();
|
|
}
|
|
```
|
|
|
|
공통 SPI에 Spring `Resource`, `DataBuffer`, Reactor 타입을 포함하지 않는다. WebFlux adapter는 `Flow.Publisher<ByteBuffer>`와 `Flux<DataBuffer>` 사이를 변환하고 pooled buffer의 수명주기를 책임진다.
|
|
|
|
### 11.4 Capability 확장
|
|
|
|
```java
|
|
public interface CopyCapableContentStore {
|
|
CompletionStage<StoredContent> copy(
|
|
ContentKey source,
|
|
ContentKey target,
|
|
CopyPrecondition precondition
|
|
);
|
|
}
|
|
|
|
public interface CapacityAwareContentStore {
|
|
StorageCapacity capacity();
|
|
}
|
|
|
|
public interface DelegatedDownloadStore {
|
|
DelegatedDownloadDescriptor createDelegation(
|
|
ContentKey key,
|
|
ByteRange range,
|
|
Duration ttl
|
|
);
|
|
}
|
|
```
|
|
|
|
`copy`, capacity, delegation은 최소 Port에 강제하지 않는다.
|
|
|
|
---
|
|
|
|
## 12. Local Filesystem Adapter
|
|
|
|
### 12.1 저장 레이아웃
|
|
|
|
```text
|
|
${root}/
|
|
├── staging/
|
|
│ └── ab/cd/<upload-id>.part
|
|
├── content/
|
|
│ └── ab/cd/<content-key>.bin
|
|
├── quarantine/
|
|
│ └── ab/cd/<content-key>.bin
|
|
└── probe/
|
|
```
|
|
|
|
- shard는 server-generated ID의 앞 2 byte씩 사용한다.
|
|
- 원본 파일명과 확장자를 physical filename에 사용하지 않는다.
|
|
- `staging`, `content`, `quarantine`은 동일 `FileStore`에 위치해야 한다.
|
|
- root는 application source, config, webroot와 분리한다.
|
|
- startup에서 디렉터리 owner·permission을 검증한다.
|
|
|
|
### 12.2 경로 안전 규칙
|
|
|
|
```java
|
|
public interface PhysicalPathResolver {
|
|
Path stagingPath(UploadId uploadId);
|
|
Path contentPath(ContentKey contentKey);
|
|
Path quarantinePath(ContentKey contentKey);
|
|
}
|
|
```
|
|
|
|
`PhysicalPathResolver`는 `fileserver-storage-local` 내부 package-private 구현으로 둔다. 공개 module export 대상이 아니다.
|
|
|
|
필수 검사:
|
|
|
|
1. absolute 또는 drive-qualified 입력을 받지 않는다.
|
|
2. ID에서 만든 고정 component만 resolve한다.
|
|
3. normalize 결과가 root 아래인지 확인한다.
|
|
4. 모든 open·stat·delete에 `NOFOLLOW_LINKS`를 사용한다.
|
|
5. parent component가 symlink인지 확인한다.
|
|
6. provider가 지원하면 `SecureDirectoryStream`을 사용한다.
|
|
7. open 후 file identity와 expected parent identity를 재검증한다.
|
|
|
|
### 12.3 staging 생성
|
|
|
|
- `CREATE_NEW`, `WRITE`, `NOFOLLOW_LINKS`로 연다.
|
|
- 충돌 시 새로운 storage key를 재발급하지 않고 invariant violation으로 기록한다.
|
|
- file permission은 owner read·write만 허용하는 프로파일을 기본으로 한다.
|
|
- append 전에 실제 file length와 metadata offset을 대조한다.
|
|
|
|
### 12.4 append
|
|
|
|
- 고정 크기 direct buffer pool 또는 heap buffer를 사용하며 파일 전체를 적재하지 않는다.
|
|
- 기본 buffer는 128 KiB다.
|
|
- `expectedOffset`이 실제 길이 또는 metadata offset과 다르면 append를 수행하지 않는다.
|
|
- 실제 수신 byte 수가 정책 최대값을 넘으면 즉시 중단한다.
|
|
- append 도중 실제 size와 SHA-256을 streaming 계산한다.
|
|
- `contentLength >= 0`이면 실제 append byte와 일치해야 한다.
|
|
- cancellation과 exception 시 channel을 닫고 session은 복구 가능한 상태로 남긴다.
|
|
|
|
### 12.5 delete
|
|
|
|
- symbolic link를 따라가지 않는다.
|
|
- logical delete를 먼저 수행한 뒤 cleanup worker가 physical object를 삭제한다.
|
|
- large file은 삭제 latency와 filesystem 특성을 metric으로 기록한다.
|
|
- 실제 파일이 이미 없으면 idempotent success로 처리하되 reconciliation event를 남긴다.
|
|
|
|
---
|
|
|
|
## 13. Storage Capability Probe와 Startup Gate
|
|
|
|
### 13.1 Probe 항목
|
|
|
|
| Probe | 통과 기준 | 실패 정책 |
|
|
|---|---|---|
|
|
| writable root | create·write·close·delete 성공 | startup 실패 |
|
|
| `CREATE_NEW` 경쟁 | 두 동시 create 중 정확히 하나 성공 | startup 실패 |
|
|
| same `FileStore` | staging·content·quarantine 동일 | startup 실패 |
|
|
| atomic move | observer가 partial target을 보지 않고 move 성공 | mode에 따라 실패 또는 pointer publish |
|
|
| replace | old 또는 new만 관측 | overwrite capability 비활성 |
|
|
| fsync profile | force 후 restart test 결과 저장 | durability 등급 표시 |
|
|
| symlink no-follow | target 접근이 차단됨 | startup 실패 |
|
|
| open-delete | OS 동작 기록 | lifecycle policy 조정 |
|
|
| capacity | usable·total 조회 가능 | admin capability 제한 |
|
|
|
|
### 13.2 Publish mode
|
|
|
|
```java
|
|
public enum PublishMode {
|
|
ATOMIC_MOVE_REQUIRED,
|
|
ATOMIC_MOVE_PREFERRED,
|
|
METADATA_POINTER
|
|
}
|
|
```
|
|
|
|
- `ATOMIC_MOVE_REQUIRED`: probe 실패 시 startup 실패
|
|
- `ATOMIC_MOVE_PREFERRED`: 가능하면 atomic move, 불가능하면 pointer publish
|
|
- `METADATA_POINTER`: immutable physical key를 완성한 뒤 DB pointer를 READY boundary로 사용
|
|
|
|
기본값은 `ATOMIC_MOVE_PREFERRED`다.
|
|
|
|
### 13.3 Runtime capability endpoint
|
|
|
|
`GET /internal/fileserver/capabilities`는 다음을 제공한다.
|
|
|
|
```json
|
|
{
|
|
"storageType": "LOCAL",
|
|
"publishMode": "ATOMIC_MOVE_PREFERRED",
|
|
"rangedRead": true,
|
|
"atomicCreate": true,
|
|
"atomicPublish": true,
|
|
"conditionalWrite": true,
|
|
"delegatedDownload": true,
|
|
"resumableAppend": true,
|
|
"filesystemProfile": "linux-ext4"
|
|
}
|
|
```
|
|
|
|
physical root와 mount detail은 반환하지 않는다.
|
|
|
|
---
|
|
|
|
## 14. Publish와 완료 처리
|
|
|
|
### 14.1 Atomic move strategy
|
|
|
|
```text
|
|
staging channel close
|
|
→ optional `FileChannel.force(true)`
|
|
→ verify expected length·digest
|
|
→ target parent 준비
|
|
→ `Files.move(staging, target, ATOMIC_MOVE)`
|
|
→ target stat
|
|
→ DB READY transition
|
|
```
|
|
|
|
`REPLACE_EXISTING`은 overwrite precondition이 있는 경로에서만 사용한다. create-only 경로는 target이 이미 있으면 실패한다.
|
|
|
|
### 14.2 Metadata pointer strategy
|
|
|
|
```text
|
|
staging write 완료
|
|
→ immutable content key로 새 physical object 완성
|
|
→ physical stat 검증
|
|
→ DB transaction에서 contentKey pointer와 READY 상태 publish
|
|
→ 이전 physical object를 cleanup queue에 등록
|
|
```
|
|
|
|
이 전략은 rename의 원자성 대신 metadata store transaction을 public publish boundary로 사용한다.
|
|
|
|
### 14.3 Ambiguous completion
|
|
|
|
다음 상황은 `AmbiguousCompletionException`으로 분류한다.
|
|
|
|
- NFS rename request가 서버에서 처리되었을 수 있으나 응답이 유실됨
|
|
- write·force 후 연결 또는 mount 응답이 사라짐
|
|
- DB commit 응답을 받지 못해 상태 전이 성공 여부를 알 수 없음
|
|
|
|
처리 순서:
|
|
|
|
1. operation ID와 expected physical key를 조회한다.
|
|
2. metadata version과 state를 재조회한다.
|
|
3. physical stat·size·digest를 확인한다.
|
|
4. 명백한 성공이면 성공 결과를 복원한다.
|
|
5. 명백한 미실행이면 제한적으로 재실행한다.
|
|
6. 판정 불가면 recovery queue와 `retryable=false, reconciliationRequired=true` 오류를 반환한다.
|
|
|
|
---
|
|
|
|
## 15. Upload Application 설계
|
|
|
|
### 15.1 공개 command
|
|
|
|
```java
|
|
public record CreateUploadRequest(
|
|
StorageNamespace namespace,
|
|
String originalFilename,
|
|
String claimedMediaType,
|
|
OptionalLong expectedLength,
|
|
Optional<String> expectedSha256,
|
|
UploadProtocol protocol,
|
|
Instant expiresAt
|
|
) {}
|
|
|
|
public interface UploadApplicationService {
|
|
UploadSessionView create(CreateUploadRequest request, RequestContext context);
|
|
|
|
AppendUploadResult append(
|
|
UploadId uploadId,
|
|
long expectedOffset,
|
|
ReadableByteChannel content,
|
|
long contentLength,
|
|
RequestContext context
|
|
);
|
|
|
|
FileView finalizeUpload(
|
|
UploadId uploadId,
|
|
FinalizeUploadRequest request,
|
|
RequestContext context
|
|
);
|
|
|
|
UploadSessionView status(UploadId uploadId, RequestContext context);
|
|
|
|
void cancel(UploadId uploadId, RequestContext context);
|
|
}
|
|
```
|
|
|
|
Async API는 별도 interface로 동일 의미를 제공한다.
|
|
|
|
### 15.2 Create
|
|
|
|
- authorization hook 실행
|
|
- expected length가 있으면 정책 최대값 검증
|
|
- quota reservation 생성
|
|
- FileRecord CREATED 생성
|
|
- UploadSession 생성
|
|
- storage staging 생성
|
|
- state를 UPLOADING으로 전이
|
|
- `Location`과 current offset 0 반환
|
|
|
|
DB 생성 후 storage 생성이 실패하면 FileRecord를 FAILED로 전이하고 quota reservation을 해제한다. storage 생성 후 DB 응답이 모호하면 operation ID로 reconciliation한다.
|
|
|
|
### 15.3 Append
|
|
|
|
- upload 상태·만료 확인
|
|
- writer lease 획득
|
|
- metadata offset, physical length, request offset 일치 검증
|
|
- concurrency, rate, storage high-water gate 확인
|
|
- streaming append
|
|
- committed offset 저장
|
|
- lease release
|
|
|
|
append 실패 후 offset은 실제 저장이 확인된 길이까지만 증가한다. metadata offset과 physical length가 다르면 자동 append하지 않고 reconciliation으로 보낸다.
|
|
|
|
### 15.4 Finalize
|
|
|
|
- expected length가 있으면 committed offset과 비교
|
|
- server SHA-256과 client digest 비교
|
|
- state를 UPLOADED로 전이
|
|
- verification pipeline 실행
|
|
- verdict가 ACCEPT이면 publish
|
|
- metadata READY 전이
|
|
- quota commit
|
|
- REJECT 또는 QUARANTINE이면 public download 금지
|
|
|
|
### 15.5 Multipart batch
|
|
|
|
`POST /v1/files:batch`는 다음 계약을 사용한다.
|
|
|
|
- 최대 part 수 기본 16
|
|
- 각 파일은 독립 FileRecord·UploadSession
|
|
- 요청 전체 ACID 원자성은 보장하지 않는다.
|
|
- 일부 실패 시 성공 파일을 rollback하지 않는다.
|
|
- `200 OK`와 파일별 결과 배열을 반환한다.
|
|
- 총 request byte와 tenant quota를 요청 전·중 모두 검사한다.
|
|
|
|
```json
|
|
{
|
|
"results": [
|
|
{"clientPartId":"a", "status":"CREATED", "fileId":"..."},
|
|
{"clientPartId":"b", "status":"REJECTED", "problem":{"code":"FILE_TOO_LARGE"}}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 16. Verification Layer
|
|
|
|
### 16.1 Port
|
|
|
|
```java
|
|
public interface FileVerifier {
|
|
String verifierId();
|
|
CompletionStage<VerificationResult> verify(VerificationRequest request);
|
|
}
|
|
|
|
public record VerificationResult(
|
|
VerificationVerdict verdict,
|
|
String code,
|
|
Optional<String> verifiedMediaType,
|
|
Map<String, String> safeMetadata
|
|
) {}
|
|
|
|
public enum VerificationVerdict {
|
|
ACCEPT,
|
|
QUARANTINE,
|
|
REJECT,
|
|
RETRY
|
|
}
|
|
```
|
|
|
|
### 16.2 기본 pipeline
|
|
|
|
```text
|
|
Length verifier
|
|
→ SHA-256 verifier
|
|
→ filename policy
|
|
→ media type detector
|
|
→ signature/parser verifier
|
|
→ optional AV scanner
|
|
→ optional CDR
|
|
→ final policy combiner
|
|
```
|
|
|
|
- client `Content-Type`은 claimed metadata로만 저장한다.
|
|
- 단순 magic byte 일치만으로 안전 판정을 내리지 않는다.
|
|
- scanner timeout은 READY로 우회하지 않는다.
|
|
- 위험 형식은 quarantine 또는 reject한다.
|
|
- HTML, SVG 등 scriptable 문서는 기본 attachment이며 inline은 명시적 안전 프로파일에서만 허용한다.
|
|
|
|
### 16.3 검사 비동기화
|
|
|
|
- 검사 시간이 짧은 프로파일은 upload request 안에서 완료하여 `201`을 반환할 수 있다.
|
|
- AV·CDR처럼 긴 검사는 `202 Accepted`와 VERIFYING 상태를 반환한다.
|
|
- READY 전환은 verification worker가 수행한다.
|
|
- retryable scanner 장애는 exponential backoff와 최대 시도 횟수를 사용한다.
|
|
- 최대 시도 초과는 FAILED 또는 QUARANTINED로 전이한다.
|
|
|
|
---
|
|
|
|
## 17. Authorization과 기술 정책 Hook
|
|
|
|
```java
|
|
public interface FileAccessPolicy {
|
|
void authorize(FileOperation operation, FileAccessSubject subject, FileDescriptor descriptor);
|
|
}
|
|
|
|
public enum FileOperation {
|
|
CREATE,
|
|
APPEND,
|
|
FINALIZE,
|
|
READ_METADATA,
|
|
DOWNLOAD,
|
|
DELETE,
|
|
COPY,
|
|
MOVE,
|
|
ADMIN_REVERIFY,
|
|
ADMIN_FORCE_DELETE
|
|
}
|
|
```
|
|
|
|
Fileserver는 사용자 등급·업무 역할 같은 비즈니스 정책을 내장하지 않는다. 대신 모든 공개 operation에서 위 hook을 반드시 호출하고, starter가 no-op allow-all 구현을 운영 프로파일에서 자동 생성하지 않도록 한다.
|
|
|
|
존재 은닉 프로파일에서는 권한 없는 file에 `404`를 반환한다. 내부 audit에는 `ACCESS_DENIED`를 기록하되 fileId·userId 원문을 metric label에 사용하지 않는다.
|
|
|
|
---
|
|
|
|
## 18. Quota, Capacity와 Transfer Budget
|
|
|
|
### 18.1 Quota Port
|
|
|
|
```java
|
|
public interface FileQuotaService {
|
|
QuotaReservation reserve(QuotaScope scope, long expectedBytes, Duration ttl);
|
|
void extend(QuotaReservation reservation, long additionalBytes);
|
|
void commit(QuotaReservation reservation, long actualBytes);
|
|
void release(QuotaReservation reservation);
|
|
}
|
|
```
|
|
|
|
expected length가 없으면 프로파일별 initial reservation을 잡고 append 중 증분 예약한다.
|
|
|
|
### 18.2 기본 운영 프로파일
|
|
|
|
| 설정 | Standard | Large-file |
|
|
|---|---:|---:|
|
|
| 최대 파일 | 100 MiB | 5 GiB |
|
|
| 최대 request | 116 MiB | 5 GiB + 16 MiB |
|
|
| 최대 multipart part | 16 | 16 |
|
|
| in-memory part | 512 KiB | 256 KiB |
|
|
| stream buffer | 128 KiB | 256 KiB |
|
|
| 인스턴스 동시 upload | 16 | 32 |
|
|
| 인스턴스 direct download | 64 | 128 |
|
|
| scope 동시 upload | 4 | 8 |
|
|
| temp soft limit | usable 70% | usable 70% |
|
|
| temp hard limit | usable 85% | usable 85% |
|
|
| idle read timeout | 45 s | 60 s |
|
|
| 미완료 upload TTL | 24 h | 72 h |
|
|
| multi Range 최대 개수 | 8 | 8 |
|
|
|
|
이 값은 starter 기본값이며 운영 환경은 부하 인증 결과로 재정의한다.
|
|
|
|
### 18.3 Admission control
|
|
|
|
새 upload는 다음 중 하나가 발생하면 거절한다.
|
|
|
|
- quota reservation 실패
|
|
- storage hard high-water 초과
|
|
- instance upload permit 고갈
|
|
- scope 동시성 초과
|
|
- verification queue hard limit 초과
|
|
|
|
soft high-water에서는 대용량 upload를 throttle하거나 `429/503`과 `Retry-After`를 반환한다.
|
|
|
|
---
|
|
|
|
## 19. HTTP API
|
|
|
|
### 19.1 공개 endpoint
|
|
|
|
| Method·Path | 목적 | 성공 |
|
|
|---|---|---|
|
|
| `POST /v1/files` | multipart 단일 업로드 | `201` READY 또는 `202` VERIFYING |
|
|
| `POST /v1/files:raw` | raw streaming 업로드 | `201` 또는 `202` |
|
|
| `POST /v1/files:batch` | 제한형 다중 업로드 | `200` 결과 배열 |
|
|
| `PUT /v1/files/{fileId}/content` | create-only·조건부 교체 | `201` 또는 `204` |
|
|
| `GET /v1/files/{fileId}` | metadata | `200` |
|
|
| `GET /v1/files/{fileId}/content` | download | `200`, `206`, `304` |
|
|
| `HEAD /v1/files/{fileId}/content` | download metadata | `200`, `304` |
|
|
| `DELETE /v1/files/{fileId}` | logical delete | `202` 또는 `204` |
|
|
| `POST /v1/files/{fileId}:copy` | 조건부 copy | `202` |
|
|
| `POST /v1/files/{fileId}:move` | logical namespace move | `200` 또는 `204` |
|
|
| `POST /v1/uploads` | resumable resource 생성 | `201` |
|
|
| `HEAD /v1/uploads/{uploadId}` | offset 조회 | protocol별 `200/204` |
|
|
| `PATCH /v1/uploads/{uploadId}` | append | `204` |
|
|
| `DELETE /v1/uploads/{uploadId}` | cancel | `204` |
|
|
|
|
### 19.2 Header 계약
|
|
|
|
| Header | 계약 |
|
|
|---|---|
|
|
| `Content-Type` | client 값은 claimed type, verified type을 별도 저장 |
|
|
| `Content-Length` | 있으면 사전 검증, 없어도 streamed hard limit 적용 |
|
|
| `Content-Disposition` | `inline` 또는 `attachment`, `filename` + `filename*` |
|
|
| `Accept-Ranges` | byte range 지원 시 `bytes` |
|
|
| `Range` | 기본 single, budget이 있는 경우 제한형 multi |
|
|
| `Content-Range` | `206` 실제 범위, `416`은 `bytes */size` |
|
|
| `ETag` | SHA-256 strong validator |
|
|
| `Last-Modified` | `publishedAt` |
|
|
| `If-None-Match` | GET·HEAD revalidation, create-only `*` |
|
|
| `If-Modified-Since` | ETag 보조 |
|
|
| `If-Match` | overwrite·delete lost-update 방지 |
|
|
| `If-Range` | validator 일치 시에만 partial |
|
|
| `Cache-Control` | private 기본 `private, no-store` |
|
|
| `Content-Digest` | 실제 HTTP message content digest |
|
|
| `Repr-Digest` | 전체 representation digest 선택 제공 |
|
|
| `Location` | 생성된 file·upload resource |
|
|
| `Retry-After` | `429`, `503`, 장기 검사의 polling 힌트 |
|
|
| `X-Accel-Redirect` | Nginx 내부 응답 전용 |
|
|
|
|
### 19.3 상태 코드
|
|
|
|
| Status | 조건 |
|
|
|---:|---|
|
|
| `200` | metadata, 전체 GET, batch result |
|
|
| `201` | file 또는 upload 생성 |
|
|
| `202` | 검사 또는 physical cleanup 비동기 |
|
|
| `204` | append, cancel, body 없는 update |
|
|
| `206` | satisfiable Range |
|
|
| `304` | GET·HEAD validator 일치 |
|
|
| `400` | 잘못된 header·요청 조합 |
|
|
| `401` | 인증 없음 |
|
|
| `403/404` | 접근 거부 또는 존재 은닉 |
|
|
| `409` | 상태·offset·lease 충돌 |
|
|
| `410` | 만료 upload |
|
|
| `411` | `require-content-length=true` 프로파일 |
|
|
| `412` | precondition 실패 |
|
|
| `413` | 크기·quota 정책 위반 |
|
|
| `415` | 허용하지 않는 upload media type |
|
|
| `416` | 만족 불가능 Range |
|
|
| `422` | digest·signature·scanner reject |
|
|
| `429` | 동시성·rate limit |
|
|
| `503` | storage·scanner unavailable |
|
|
| `504` | downstream timeout |
|
|
| `507` | 저장공간 부족 |
|
|
|
|
---
|
|
|
|
## 20. Range와 Conditional Request
|
|
|
|
### 20.1 Range parser
|
|
|
|
```java
|
|
public interface HttpRangeResolver {
|
|
ResolvedRanges resolve(String rangeHeader, long representationLength, RangeBudget budget);
|
|
}
|
|
|
|
public record RangeBudget(
|
|
int maxRanges,
|
|
long maxTotalBytes,
|
|
boolean mergeOverlaps
|
|
) {}
|
|
```
|
|
|
|
기본 public 다운로드는 single Range만 허용한다. multi Range를 활성화한 profile에서는 최대 8개, overlap merge 후 총 byte가 representation 길이 이하인 경우만 허용한다.
|
|
|
|
### 20.2 응답 결정 순서
|
|
|
|
```text
|
|
authorization
|
|
→ READY 확인
|
|
→ current ETag·Last-Modified 계산
|
|
→ If-Match / If-Unmodified-Since
|
|
→ If-None-Match / If-Modified-Since
|
|
→ Range parse
|
|
→ If-Range 평가
|
|
→ 200 / 206 / 304 / 412 / 416 결정
|
|
```
|
|
|
|
`If-Range`가 불일치하면 Range를 무시하고 전체 `200`을 반환한다.
|
|
|
|
### 20.3 ETag와 digest
|
|
|
|
- stored SHA-256을 quoted strong ETag로 사용한다.
|
|
- metadata-only 변경은 representation ETag를 바꾸지 않는다.
|
|
- `Content-Digest`는 전송 bytes 기준이다.
|
|
- full response에서는 stored SHA-256을 재사용할 수 있다.
|
|
- partial response에서는 해당 range digest를 streaming 계산하거나 기능을 비활성화한다.
|
|
- 전체 representation digest가 필요하면 `Repr-Digest`를 제공한다.
|
|
|
|
|
|
## 21. Spring MVC Adapter
|
|
|
|
### 21.1 Upload
|
|
|
|
- `MultipartFile#getBytes()`를 사용하지 않는다.
|
|
- raw upload는 request input stream을 `ReadableByteChannel`로 변환한다.
|
|
- multipart는 container threshold와 temp directory를 starter가 명시적으로 설정한다.
|
|
- upload request thread가 storage write를 장시간 점유하지 않도록 전용 executor를 사용한다.
|
|
- 기본 executor는 bounded queue와 rejection policy를 가진다.
|
|
- request cancellation과 client disconnect를 application service에 전달한다.
|
|
|
|
### 21.2 Download
|
|
|
|
전송 전략은 다음 순서로 선택한다.
|
|
|
|
1. Nginx 위임이 활성화되고 threshold 이상이면 delegation
|
|
2. local `Path`를 안전하게 반환할 수 있고 zero-copy 조건이 맞으면 zero-copy capability
|
|
3. 그 외 `StreamingResponseBody`
|
|
|
|
Range 처리는 core HTTP contract가 결정한다. Spring의 자동 Range 지원에만 의존하지 않고 MVC와 WebFlux가 같은 결과를 반환하도록 공통 resolver를 사용한다. `InputStreamResource`는 반복 가능한 Range resource로 사용하지 않는다.
|
|
|
|
### 21.3 Executor
|
|
|
|
```java
|
|
public record MvcTransferExecutorProperties(
|
|
int coreThreads,
|
|
int maxThreads,
|
|
int queueCapacity,
|
|
Duration shutdownTimeout
|
|
) {}
|
|
```
|
|
|
|
기본값:
|
|
|
|
```text
|
|
coreThreads=8
|
|
maxThreads=32
|
|
queueCapacity=64
|
|
shutdownTimeout=30s
|
|
```
|
|
|
|
queue가 가득 차면 무제한 대기하지 않고 `429` 또는 `503`으로 변환한다.
|
|
|
|
---
|
|
|
|
## 22. Spring WebFlux Adapter
|
|
|
|
### 22.1 Upload
|
|
|
|
- raw body는 `Flux<DataBuffer>`를 순차 소비한다.
|
|
- multipart streaming은 `Flux<PartEvent>`를 사용한다.
|
|
- pooled `DataBuffer`는 전달하거나 명시적으로 release한다.
|
|
- blocking local filesystem adapter 호출은 bounded elastic이 아니라 전용 bounded scheduler에서 실행한다.
|
|
- async store가 제공되면 event loop를 유지한 채 `Flow.Publisher<ByteBuffer>`로 전달한다.
|
|
- cancellation 시 channel, lease, temp resource를 정리한다.
|
|
|
|
### 22.2 Download
|
|
|
|
- async store는 `Flux<DataBuffer>`로 변환한다.
|
|
- local file zero-copy가 runtime에서 가능하면 capability optimization으로 사용한다.
|
|
- Range와 conditional 결정은 MVC와 동일한 core resolver를 사용한다.
|
|
- slow client에서 in-flight buffer 수가 설정 상한을 넘지 않도록 한다.
|
|
|
|
### 22.3 Blocking 검출
|
|
|
|
CI에서 BlockHound 또는 동등한 검증으로 다음을 차단한다.
|
|
|
|
- event loop에서 `Files.*`, `FileChannel`, JDBC 호출
|
|
- synchronous scanner 호출
|
|
- blocking metadata repository 호출
|
|
|
|
---
|
|
|
|
## 23. Nginx 전송 위임
|
|
|
|
### 23.1 구조
|
|
|
|
```text
|
|
Client
|
|
→ GET /v1/files/{fileId}/content
|
|
→ Application authorization + READY gate
|
|
→ validated ContentKey를 internal relative URI로 변환
|
|
→ X-Accel-Redirect: /__files/ab/cd/<content-key>.bin
|
|
→ Nginx internal location
|
|
→ physical content transfer
|
|
```
|
|
|
|
internal URI는 절대 physical path를 포함하지 않는다. `NginxInternalUriMapper`는 검증된 `ContentKey`만 받아 `/__files/` 아래의 상대 URI를 생성한다. 이 header는 Nginx가 내부 redirect로 소비하므로 client 응답에는 노출하지 않는다. 별도 공개 signed URL을 발급하는 기능은 Object Storage 모듈의 책임으로 남긴다.
|
|
|
|
### 23.2 정책
|
|
|
|
- 기본 delegation threshold는 16 MiB다.
|
|
- private file은 Nginx shared cache를 기본 비활성화한다.
|
|
- `internal` location은 외부 직접 요청을 거부한다.
|
|
- `X-Accel-Redirect`는 downstream client에 그대로 전달되지 않도록 한다.
|
|
- Range, ETag, Content-Disposition, Cache-Control 결과가 direct mode와 동일해야 한다.
|
|
- Nginx access log에 physical root와 원본 파일명을 남기지 않는다.
|
|
- mapper가 생성한 URI는 `ContentKey`의 허용 문자와 shard 규칙을 다시 검증한다.
|
|
|
|
### 23.3 Nginx upload
|
|
|
|
| 경로 | 기본 buffering |
|
|
|---|---|
|
|
| 작은 multipart | on 허용 |
|
|
| 대용량 raw | off |
|
|
| tus PATCH | off |
|
|
| HTTPbis PATCH | off |
|
|
|
|
upstream 전송이 시작된 non-idempotent upload에는 `proxy_next_upstream` 재시도를 적용하지 않는다.
|
|
|
|
---
|
|
|
|
## 24. 재개 가능한 업로드
|
|
|
|
### 24.1 공통 원칙
|
|
|
|
- upload resource별 single writer lease
|
|
- offset은 metadata와 physical length를 함께 검증
|
|
- mismatch 시 body를 쓰지 않고 `409`
|
|
- 서버 재시작 후 offset reconciliation
|
|
- create 시 quota 예약
|
|
- expiration과 cleanup
|
|
- client checksum 검증
|
|
- upload resource는 READY file과 별도 수명주기를 가진다.
|
|
|
|
### 24.2 tus 1.0 Stable
|
|
|
|
지원 기능:
|
|
|
|
- creation
|
|
- `HEAD`와 `Upload-Offset`
|
|
- `PATCH application/offset+octet-stream`
|
|
- checksum extension
|
|
- expiration extension
|
|
- termination extension
|
|
- concatenation extension은 Beta
|
|
|
|
성공 append는 `204`와 새 `Upload-Offset`을 반환한다. offset mismatch는 resource를 변경하지 않고 `409`를 반환한다.
|
|
|
|
### 24.3 HTTPbis draft-12 Experimental
|
|
|
|
- module 이름과 package에 `draft12`를 포함한다.
|
|
- feature flag 없이는 bean을 생성하지 않는다.
|
|
- media type과 header를 draft version에 고정한다.
|
|
- 104 interim response 지원 여부를 runtime capability로 표시한다.
|
|
- 최종 RFC 변화에 따른 breaking change를 허용한다.
|
|
- Stable core와 endpoint namespace를 분리한다.
|
|
|
|
### 24.4 병렬 upload
|
|
|
|
하나의 upload offset에 여러 writer를 허용하지 않는다. 병렬 전송은 다음 구조만 제공한다.
|
|
|
|
```text
|
|
parent upload
|
|
├─ part 1 resource
|
|
├─ part 2 resource
|
|
└─ part N resource
|
|
→ 각 part checksum 검증
|
|
→ 순서와 총 길이 검증
|
|
→ concatenate
|
|
→ final verification
|
|
```
|
|
|
|
---
|
|
|
|
## 25. 파일 관리 기능
|
|
|
|
### 25.1 stat
|
|
|
|
공개 `stat`은 DB metadata를 반환한다. physical stat은 내부 일관성 검증에만 사용한다.
|
|
|
|
### 25.2 delete
|
|
|
|
```text
|
|
If-Match 검증
|
|
→ READY/REJECTED/FAILED → DELETING
|
|
→ 공개 read 즉시 차단
|
|
→ cleanup item 등록
|
|
→ physical delete
|
|
→ quota 반영
|
|
→ DELETED
|
|
```
|
|
|
|
### 25.3 copy
|
|
|
|
- capability가 없으면 application-level stream copy를 사용한다.
|
|
- target은 create-only가 기본이다.
|
|
- source와 target metadata는 별도 레코드다.
|
|
- copy 실패 시 incomplete target은 cleanup queue로 보낸다.
|
|
- 자동 rollback 보장을 선언하지 않는다.
|
|
|
|
### 25.4 move
|
|
|
|
공개 move는 physical path move가 아니라 logical namespace·ownership metadata 변경이다. physical content는 immutable key를 유지한다. physical move는 admin maintenance에만 사용한다.
|
|
|
|
### 25.5 list·scan
|
|
|
|
public API에는 제공하지 않는다. admin API는 bounded pagination, prefix allowlist, rate limit, dry-run을 요구한다.
|
|
|
|
---
|
|
|
|
## 26. 오류 모델과 Problem Detail
|
|
|
|
### 26.1 예외 hierarchy
|
|
|
|
```text
|
|
FileserverException
|
|
├─ FileNotFoundException
|
|
├─ FileAlreadyExistsException
|
|
├─ InvalidPathException
|
|
├─ PathOutsideNamespaceException
|
|
├─ FileAccessDeniedException
|
|
├─ StorageFullException
|
|
├─ QuotaExceededException
|
|
├─ FileTooLargeException
|
|
├─ UnsupportedMediaTypeException
|
|
├─ IntegrityMismatchException
|
|
├─ UploadOffsetMismatchException
|
|
├─ UploadExpiredException
|
|
├─ FileNotReadyException
|
|
├─ AtomicPublishUnsupportedException
|
|
├─ TransferTimeoutException
|
|
├─ PartialWriteException
|
|
├─ AmbiguousCompletionException
|
|
├─ StorageUnavailableException
|
|
├─ ConcurrentFileModificationException
|
|
└─ MalwareDetectedException
|
|
```
|
|
|
|
모든 예외는 다음 metadata를 가진다.
|
|
|
|
```java
|
|
public record FileserverFailureContext(
|
|
String code,
|
|
boolean retryable,
|
|
boolean ambiguous,
|
|
boolean reconciliationRequired,
|
|
Optional<FileId> fileId,
|
|
Optional<UploadId> uploadId,
|
|
OptionalLong expectedOffset,
|
|
OptionalLong currentOffset,
|
|
Optional<FileState> currentState
|
|
) {}
|
|
```
|
|
|
|
### 26.2 Problem Detail
|
|
|
|
```json
|
|
{
|
|
"type": "urn:fileserver:problem:upload-offset-mismatch",
|
|
"title": "Upload offset mismatch",
|
|
"status": 409,
|
|
"code": "UPLOAD_OFFSET_MISMATCH",
|
|
"retryable": true,
|
|
"uploadId": "...",
|
|
"expectedOffset": 1048576,
|
|
"currentOffset": 524288,
|
|
"traceId": "..."
|
|
}
|
|
```
|
|
|
|
내부 path, mount, scanner credential, storage token을 포함하지 않는다.
|
|
|
|
---
|
|
|
|
## 27. 보안 정책
|
|
|
|
### 27.1 위험 등급
|
|
|
|
| 등급 | 대상 | 정책 |
|
|
|---|---|---|
|
|
| F1 | ID 기반 create·read·delete, single Range | 기본 허용, auth·size·state gate |
|
|
| F2 | 대용량 stream, multi Range, resumable, overwrite, copy | quota·budget·precondition 필수 |
|
|
| F3 | list, capacity, orphan, force delete, reverify | internal admin plane |
|
|
| F4 | arbitrary path, symlink, recursive delete, webroot storage | 전체 차단 |
|
|
|
|
### 27.2 필수 방어
|
|
|
|
- opaque ID와 server-generated physical key
|
|
- original filename sanitization
|
|
- extension allowlist가 있더라도 Content-Type을 신뢰하지 않음
|
|
- signature/parser·scanner verdict
|
|
- executable permission 제거
|
|
- separate mount와 webroot 밖 저장
|
|
- size, part count, concurrency, minimum-rate 제한
|
|
- private download cache 제한
|
|
- READY gate
|
|
- CSRF 방어가 필요한 cookie 기반 upload endpoint
|
|
- authorization on every access
|
|
- range bomb 제한
|
|
- ZIP/XML expanded-size 제한을 verifier에 적용
|
|
|
|
### 27.3 파일명 sanitization
|
|
|
|
제거·치환 대상:
|
|
|
|
- `/`, `\`, NUL
|
|
- control characters
|
|
- bidi override characters
|
|
- CR/LF와 quote injection
|
|
- trailing dot·space
|
|
- Windows reserved names
|
|
- UTF-8 255 byte 초과
|
|
|
|
sanitized name은 Content-Disposition에만 사용하며 physical path 생성에는 사용하지 않는다.
|
|
|
|
---
|
|
|
|
## 28. 다중 인스턴스와 NFS
|
|
|
|
### 28.1 Writer lease
|
|
|
|
```java
|
|
public record WriterLease(
|
|
UploadId uploadId,
|
|
String owner,
|
|
UUID token,
|
|
Instant expiresAt,
|
|
long version
|
|
) {}
|
|
```
|
|
|
|
- DB conditional update로 획득한다.
|
|
- append 중 주기적으로 갱신한다.
|
|
- lease token이 다르면 offset commit을 거부한다.
|
|
- process pause로 lease가 만료된 writer는 이후 commit하지 못한다.
|
|
- local file lock이나 NFS lock을 correctness 근거로 사용하지 않는다.
|
|
|
|
### 28.2 NFS reconciliation
|
|
|
|
다음 이벤트에서 metadata와 physical state를 재확인한다.
|
|
|
|
- rename timeout
|
|
- stale file handle
|
|
- mount reconnect
|
|
- attribute mismatch
|
|
- server restart
|
|
- lease takeover
|
|
|
|
reconciliation 결과:
|
|
|
|
```text
|
|
CONFIRMED_SUCCESS
|
|
CONFIRMED_NOT_APPLIED
|
|
RECOVERABLE_PARTIAL
|
|
QUARANTINE_REQUIRED
|
|
UNRESOLVED
|
|
```
|
|
|
|
`UNRESOLVED`는 자동 retry하지 않고 운영 queue로 보낸다.
|
|
|
|
### 28.3 PVC certification unit
|
|
|
|
지원 단위는 `PVC`라는 이름이 아니라 다음 tuple이다.
|
|
|
|
```text
|
|
Kubernetes version
|
|
+ CSI driver/version
|
|
+ StorageClass
|
|
+ access mode
|
|
+ filesystem/backend
|
|
+ mount options
|
|
```
|
|
|
|
---
|
|
|
|
## 29. Cleanup와 Reconciliation
|
|
|
|
### 29.1 Cleanup 종류
|
|
|
|
- expired upload
|
|
- cancelled staging
|
|
- failed verification content
|
|
- deleted READY content
|
|
- orphan physical object
|
|
- stale quota reservation
|
|
- abandoned lease
|
|
- previous version after pointer publish
|
|
|
|
### 29.2 안전 규칙
|
|
|
|
- cleanup은 version과 lease를 확인한다.
|
|
- 기본 admin 실행은 dry-run이다.
|
|
- active upload와 동일 physical key는 삭제하지 않는다.
|
|
- batch size와 bytes budget을 둔다.
|
|
- 실패는 exponential backoff와 최대 retry를 사용한다.
|
|
- 장기 실패는 orphan metric과 alert로 승격한다.
|
|
|
|
### 29.3 Reconciliation
|
|
|
|
```java
|
|
public interface FileReconciliationService {
|
|
ReconciliationResult reconcile(FileId fileId);
|
|
ReconciliationBatchResult reconcileOrphans(ReconciliationQuery query);
|
|
}
|
|
```
|
|
|
|
자동 reconciliation이 READY를 임의 추정해서는 안 된다. size, digest, expected content key, metadata version이 모두 맞을 때만 상태를 복원한다.
|
|
|
|
---
|
|
|
|
## 30. 관측성
|
|
|
|
### 30.1 Metric
|
|
|
|
| Metric | 주요 tag |
|
|
|---|---|
|
|
| upload count·duration | protocol, storageType, resultCode, sizeBucket |
|
|
| download count·duration | transferMode, rangeType, resultCode, sizeBucket |
|
|
| transfer bytes | direction, storageType |
|
|
| active transfers | direction, instance |
|
|
| interruption | direction, reason |
|
|
| resumable append | protocol, result |
|
|
| offset mismatch | protocol, clientType |
|
|
| checksum failure | algorithm, stage |
|
|
| verification queue | verifier, verdict, ageBucket |
|
|
| temp·orphan bytes | storagePool, ageBucket |
|
|
| storage usage | pool, mountProfile |
|
|
| quota | scopeType, result |
|
|
| cleanup | type, result |
|
|
| delegation ratio | sizeBucket |
|
|
| access denial | operation, policyCode |
|
|
|
|
실제 file ID, upload ID, filename, path, user ID를 metric label로 사용하지 않는다.
|
|
|
|
### 30.2 Trace
|
|
|
|
```text
|
|
upload.create
|
|
upload.append
|
|
upload.finalize
|
|
verify.digest
|
|
verify.media-type
|
|
verify.malware
|
|
storage.publish
|
|
storage.stat
|
|
metadata.transition
|
|
download.authorize
|
|
download.resolve-range
|
|
download.open
|
|
download.delegate
|
|
cleanup.item
|
|
reconcile.file
|
|
```
|
|
|
|
### 30.3 Audit
|
|
|
|
다음 작업은 audit 대상이다.
|
|
|
|
- overwrite
|
|
- delete·force delete
|
|
- admin reverify
|
|
- orphan reconcile
|
|
- quarantine 승인·거절
|
|
- delegated download 발급
|
|
- access denial
|
|
|
|
filename, path, signed token, content sample은 audit에 기록하지 않는다.
|
|
|
|
---
|
|
|
|
## 31. Spring Boot 설정
|
|
|
|
```yaml
|
|
backend:
|
|
fileserver:
|
|
enabled: true
|
|
storage:
|
|
type: local
|
|
root: /var/lib/backend/files
|
|
publish-mode: atomic-move-preferred
|
|
require-same-file-store: true
|
|
fail-on-symlink: true
|
|
buffer-size: 128KiB
|
|
upload:
|
|
profile: standard
|
|
max-file-size: 100MiB
|
|
max-request-size: 116MiB
|
|
max-parts: 16
|
|
require-content-length: false
|
|
incomplete-ttl: 24h
|
|
idle-timeout: 45s
|
|
instance-concurrency: 16
|
|
scope-concurrency: 4
|
|
download:
|
|
single-range-only: true
|
|
max-ranges: 8
|
|
direct-concurrency: 64
|
|
private-cache-control: "private, no-store"
|
|
content-digest: false
|
|
nginx:
|
|
enabled: false
|
|
delegate-threshold: 16MiB
|
|
internal-prefix: /__files/
|
|
verification:
|
|
async: true
|
|
checksum: sha-256
|
|
require-media-type-verdict: true
|
|
scanner-required: false
|
|
max-attempts: 5
|
|
quota:
|
|
enabled: true
|
|
reservation-ttl: 24h
|
|
cleanup:
|
|
batch-size: 100
|
|
max-bytes-per-run: 10GiB
|
|
fixed-delay: 5m
|
|
tus:
|
|
enabled: false
|
|
checksum: true
|
|
expiration: true
|
|
termination: true
|
|
httpbis-draft12:
|
|
enabled: false
|
|
mvc:
|
|
executor:
|
|
core-threads: 8
|
|
max-threads: 32
|
|
queue-capacity: 64
|
|
webflux:
|
|
io-workers: 16
|
|
max-in-flight-buffers: 8
|
|
```
|
|
|
|
### 31.1 Startup validation
|
|
|
|
다음 조건은 startup 실패다.
|
|
|
|
- storage root가 webroot 또는 application config 아래임
|
|
- staging과 content가 다른 `FileStore`
|
|
- symlink no-follow probe 실패
|
|
- `ATOMIC_MOVE_REQUIRED`인데 probe 실패
|
|
- metadata store 없이 multi-instance mode 활성화
|
|
- no-op authorization policy가 production profile에서 활성화
|
|
- scanner-required인데 verifier bean 없음
|
|
- Nginx delegation을 켰는데 token service 또는 mapping 검증 없음
|
|
|
|
---
|
|
|
|
## 32. 관리자 API
|
|
|
|
| Method·Path | 기능 | 통제 |
|
|
|---|---|---|
|
|
| `GET /internal/fileserver/storage-health` | capacity와 probe 결과 | admin network·role |
|
|
| `GET /internal/fileserver/capabilities` | runtime capability | path 비노출 |
|
|
| `GET /internal/fileserver/orphans` | bounded orphan 조회 | pagination·rate limit |
|
|
| `POST /internal/fileserver/orphans:reconcile` | dry-run·apply | audit |
|
|
| `POST /internal/fileserver/files/{id}:reverify` | 재검사 | audit |
|
|
| `POST /internal/fileserver/files/{id}:force-delete` | 강제 삭제 | 사유·이중 권한 |
|
|
| `GET /internal/fileserver/uploads/incomplete` | 미완료 조회 | filename 마스킹 |
|
|
| `POST /internal/fileserver/uploads:cleanup` | cleanup | lease·version 확인 |
|
|
| `GET /internal/fileserver/verification-queue` | 검사 지연 | bounded result |
|
|
|
|
관리자 API는 public starter에서 자동 노출하지 않고 별도 `fileserver-admin` 모듈과 management port에서만 활성화한다.
|
|
|
|
---
|
|
|
|
## 33. 테스트 전략
|
|
|
|
### 33.1 계약 테스트
|
|
|
|
- Content Store blocking·async contract
|
|
- Metadata optimistic transition contract
|
|
- state machine illegal transition
|
|
- upload offset and lease
|
|
- checksum and size
|
|
- GET·HEAD header parity
|
|
- `200/206/304/412/416`
|
|
- Range first, middle, suffix, end, empty
|
|
- `If-Range`, `If-Match`, `If-None-Match`
|
|
- multipart single·batch
|
|
- tus create·HEAD·PATCH·checksum·expiry·termination
|
|
|
|
### 33.2 보안 테스트
|
|
|
|
- `../`, percent-encoded separator, absolute path, Windows drive path
|
|
- parent symlink replacement race
|
|
- hard link discovery
|
|
- filename CRLF·bidi·reserved name
|
|
- extension·Content-Type·signature mismatch
|
|
- scriptable content inline 차단
|
|
- scanner timeout·malware verdict
|
|
- internal Nginx path direct access
|
|
- unauthorized download and existence hiding
|
|
- multi Range bomb
|
|
|
|
### 33.3 장애 테스트
|
|
|
|
- write 전·중·후 process kill
|
|
- close 후 publish 전 kill
|
|
- physical publish 후 DB commit 전 kill
|
|
- disk full and quota exhaustion
|
|
- permission denied
|
|
- slow upload·download
|
|
- network disconnect
|
|
- WebFlux cancellation
|
|
- MVC executor saturation
|
|
- NFS disconnect·server restart·rename ambiguity
|
|
- PVC remount·Pod reschedule
|
|
- scanner unavailable
|
|
|
|
### 33.4 성능 테스트
|
|
|
|
- 100 MiB와 5 GiB streaming
|
|
- concurrent upload/download
|
|
- direct vs Nginx throughput
|
|
- p50, p95, p99, max latency
|
|
- heap, direct memory, allocation, GC
|
|
- temp disk and scanner throughput
|
|
- Range overhead
|
|
- cleanup throughput
|
|
|
|
### 33.5 인증 매트릭스
|
|
|
|
| 프로파일 | 빈도 | Gate |
|
|
|---|---|---|
|
|
| Linux ext4 local | PR | 필수 |
|
|
| Linux XFS local | nightly | release 필수 |
|
|
| PVC RWO 주 CSI | release | 필수 |
|
|
| PVC RWX | release | 지원 선언 시 필수 |
|
|
| NFSv4.1 | nightly | 제한 지원 필수 |
|
|
| NFS fault injection | RC | 제한 지원 필수 |
|
|
| Windows NTFS | nightly | 초기 non-blocking |
|
|
| Nginx stable | release | nginx 모듈 필수 |
|
|
| MVC Tomcat | PR | 필수 |
|
|
| MVC Jetty | release | 지원 선언 시 필수 |
|
|
| WebFlux Reactor Netty | PR | 필수 |
|
|
| Spring 6.2 latest | release | 필수 |
|
|
| Spring 7.0 latest | release | 필수 |
|
|
|
|
---
|
|
|
|
## 34. CI 품질 Gate
|
|
|
|
모든 pull request:
|
|
|
|
```text
|
|
unit test
|
|
core contract test
|
|
local ext4 integration
|
|
MVC Tomcat HTTP contract
|
|
WebFlux Reactor Netty contract
|
|
architecture test
|
|
path traversal·symlink security suite
|
|
bounded-memory regression
|
|
```
|
|
|
|
Nightly:
|
|
|
|
```text
|
|
XFS
|
|
NFSv4.1
|
|
Windows NTFS
|
|
large-file performance
|
|
slow client
|
|
process-kill matrix
|
|
scanner failure
|
|
```
|
|
|
|
Release:
|
|
|
|
```text
|
|
Spring 6.2 / 7.0 matrix
|
|
PVC certification
|
|
Nginx contract
|
|
multi-instance lease
|
|
fault injection
|
|
support-matrix diff
|
|
sensitive-log scan
|
|
```
|
|
|
|
---
|
|
|
|
## 35. 릴리스 단계
|
|
|
|
### Milestone A — Core Alpha
|
|
|
|
- core model·state machine
|
|
- JPA metadata
|
|
- local staging·append·publish
|
|
- raw upload
|
|
- full download
|
|
- checksum
|
|
|
|
### Milestone B — HTTP Beta
|
|
|
|
- multipart
|
|
- GET·HEAD·single Range
|
|
- conditional request
|
|
- MVC·WebFlux
|
|
- security verifier
|
|
- cleanup
|
|
|
|
### Milestone C — Distributed RC
|
|
|
|
- multi-instance lease
|
|
- Nginx delegation
|
|
- PVC RWO certification
|
|
- admin plane
|
|
- chaos·performance gate
|
|
|
|
### Milestone D — Extended Release
|
|
|
|
- tus 1.0
|
|
- NFS limited profile
|
|
- PVC RWX certification
|
|
- multi Range Beta
|
|
- HTTPbis draft-12 Experimental
|
|
|
|
---
|
|
|
|
## 36. 구현자가 임의로 변경하면 안 되는 결정
|
|
|
|
- 공개 API에 `Path`와 physical filename을 노출하지 않는다.
|
|
- Content Store의 최소 Port를 filesystem 명령 mirror로 바꾸지 않는다.
|
|
- READY 이전 다운로드를 허용하지 않는다.
|
|
- metadata DB를 우회해 physical file 존재만으로 READY를 추정하지 않는다.
|
|
- create-only 기본값을 unconditional overwrite로 바꾸지 않는다.
|
|
- client Content-Type과 filename을 신뢰하지 않는다.
|
|
- WebFlux event loop에서 blocking I/O를 실행하지 않는다.
|
|
- MVC streaming에 unbounded executor를 사용하지 않는다.
|
|
- NFS lock을 단독 correctness mechanism으로 사용하지 않는다.
|
|
- upload timeout 후 blind retry를 수행하지 않는다.
|
|
- arbitrary path, symlink, recursive delete를 escape hatch로 열지 않는다.
|
|
- IETF draft 모듈을 Stable API와 섞지 않는다.
|
|
- metric label에 fileId·filename·path를 넣지 않는다.
|
|
|
|
---
|
|
|
|
## 37. 완료 정의
|
|
|
|
프로젝트 완료는 다음 산출물이 코드와 CI에 연결됐을 때 선언한다.
|
|
|
|
| 산출물 | 완료 기준 |
|
|
|---|---|
|
|
| 지원 매트릭스 | runtime·filesystem·protocol별 자동 test job 연결 |
|
|
| 상태 머신 | 모든 허용·금지 전이 contract test |
|
|
| Content Store | blocking·async contract와 local adapter 인증 |
|
|
| Metadata Store | optimistic version·lease·recovery test |
|
|
| HTTP 계약 | MVC·WebFlux·Nginx mode parity |
|
|
| 보안 | traversal·symlink·MIME·권한 공격 suite |
|
|
| 장애 | crash point·disk full·network fault 후 invariant 유지 |
|
|
| 성능 | 최대 파일에서도 bounded heap·direct memory |
|
|
| 운영 | metric, trace, audit, cleanup, reconciliation, runbook |
|
|
| 재개 업로드 | tus 1.0 contract suite |
|
|
| 제한 지원 | NFS·PVC RWX·Windows 수준이 runtime capability와 문서에 표시 |
|
|
|
|
---
|
|
|
|
## 38. 구현 순서
|
|
|
|
```text
|
|
1. 모듈·품질 기반
|
|
2. core ID·상태·오류
|
|
3. Content Store와 Metadata Store 계약
|
|
4. JPA metadata
|
|
5. local path·staging·capability probe
|
|
6. append·checksum·quota
|
|
7. publish·state transition·reconciliation
|
|
8. upload application
|
|
9. HTTP Range·conditional core
|
|
10. MVC
|
|
11. WebFlux
|
|
12. verification·authorization
|
|
13. delete·cleanup·admin
|
|
14. Nginx delegation
|
|
15. multi-instance·PVC
|
|
16. tus 1.0
|
|
17. NFS limited certification
|
|
18. HTTPbis draft Experimental
|
|
19. chaos·performance·release matrix
|
|
```
|