From 7597d8632b0df8e8027709d1895362f8e33e0f18 Mon Sep 17 00:00:00 2001 From: son Date: Wed, 18 Mar 2026 17:53:41 +0900 Subject: [PATCH] add pipelines --- 설계/db/{개념적 설계.md => 설계.md} | 22 +- 설계/flows/다운로드 흐름.md | 136 ----------- 설계/img/download_flowchart_part1.svg | 127 ++++++++++ 설계/img/download_flowchart_part2.svg | 125 ++++++++++ 설계/img/file_download_flow.svg | 121 --------- 설계/img/file_download_sequence_chart.svg | 175 +++++++++++++ 설계/pipeline/다운로드 파이프라인.md | 284 ++++++++++++++++++++++ 설계/pipeline/업로드 파이프라인.md | 194 +++++++++++++++ 8 files changed, 906 insertions(+), 278 deletions(-) rename 설계/db/{개념적 설계.md => 설계.md} (94%) delete mode 100644 설계/flows/다운로드 흐름.md create mode 100644 설계/img/download_flowchart_part1.svg create mode 100644 설계/img/download_flowchart_part2.svg delete mode 100644 설계/img/file_download_flow.svg create mode 100644 설계/img/file_download_sequence_chart.svg create mode 100644 설계/pipeline/다운로드 파이프라인.md create mode 100644 설계/pipeline/업로드 파이프라인.md diff --git a/설계/db/개념적 설계.md b/설계/db/설계.md similarity index 94% rename from 설계/db/개념적 설계.md rename to 설계/db/설계.md index a7accaa..3a10722 100644 --- a/설계/db/개념적 설계.md +++ b/설계/db/설계.md @@ -1,24 +1,4 @@ -# Cloud# — 데이터베이스 개념적 설계서 - -> 본 문서는 Cloud# 서비스의 핵심 엔티티에 대한 개념적 DB 설계를 정의한다. -> 물리적 DDL이 아닌 **엔티티의 역할, 컬럼 의미, 설계 의도, 제약 조건**을 중심으로 기술한다. - ---- - -## 목차 - -1. [엔티티 목록](#1. 엔티티 목록) -2. [ER 다이어그램](#2-er-다이어그램) -3. [엔티티 상세 설계](#3-엔티티-상세-설계) - - [3.1 User](#31-user) - - [3.2 Folder](#32-folder) - - [3.3 FileItem](#33-fileitem) - - [3.4 UploadSession](#34-uploadsession) -4. [관계 정의](#4-관계-정의) -5. [제약 조건 요약](#5-제약-조건-요약) -6. [설계 결정 사항 (Decision Log)](#6-설계-결정-사항-decision-log) - ---- +# Cloud# — 데이터베이스 설계서 ## 1. 엔티티 목록 diff --git a/설계/flows/다운로드 흐름.md b/설계/flows/다운로드 흐름.md deleted file mode 100644 index 311e7a5..0000000 --- a/설계/flows/다운로드 흐름.md +++ /dev/null @@ -1,136 +0,0 @@ - -![[file_download_flow.svg]] -## 단계별 상세 - -### 1. 사용자 인증 / 공유 링크 접근 - -요청의 진입점으로, 두 가지 접근 경로를 처리한다. - -- **인증 사용자**: `Authorization` 헤더의 JWT Bearer 토큰을 파싱 및 검증 -- **공유 링크**: URL 쿼리 파라미터의 `share_token`을 파싱 - -두 경로 모두 이후 단계에서 동일한 인터페이스로 처리될 수 있도록 내부적으로 정규화한다. - ---- - -### 2. 파일 권한 / 공유 상태 검증 - -DB를 조회하여 요청자가 해당 파일에 접근할 수 있는지 확인한다. - -- 인증 사용자: 파일의 소유자 또는 공유 대상 여부 확인 -- 공유 링크: 링크의 활성화 여부, 만료 여부, 비밀번호 보호 여부 확인 -- 권한이 없으면 즉시 **`403 Forbidden`** 반환 (이후 단계로 진행하지 않음) - -##### Risk -권한이 없는 경우 `403`, 파일이 없는 경우 `404`를 분리 반환한다. 이 차이를 이용하면 공격자가 파일 ID를 열거하여 존재하는 파일 목록을 추론할 수 있다. - ---- - -### 3. 다운로드 토큰 발급 - -실제 스트리밍 요청에 사용할 단명(short-lived) 토큰을 생성한다. - -- TTL은 **약 60초** 권장 (재사용·탈취 위험 최소화) -- 토큰에는 파일 ID, 요청자 식별자, 만료 시각을 포함 -- HMAC 서명 또는 JWT로 위변조 방지 - -``` -# Redis 기반 원자적 무효화 -result = REDIS.SET(f"dl_token:{token}", "used", NX=True, EX=60) -# NX: key가 없을 때만 SET → 원자적 보장 -# result가 None이면 이미 사용된 토큰 → 401 - -``` - -> API 서버와 스트리밍 서버를 분리 운영할 때 특히 유용하다. -> 클라이언트는 이 토큰을 받아 스트리밍 엔드포인트에 직접 요청한다. - ---- - -### 4. 다운로드 토큰으로 파일 스트림 요청 - -클라이언트가 발급받은 토큰으로 스트리밍 엔드포인트에 요청한다. - -``` -GET /files/stream?token={download_token} -Range: bytes=0-1048575 (선택, Range 요청 시) -``` - -- 토큰이 만료되었거나 유효하지 않으면 **`401 Unauthorized`** 반환 -- 토큰은 1회 사용 후 무효화하는 것을 권장 (재생 공격 방어) - -#### Risk -- "토큰은 1회 사용 후 무효화하는 것을 권장"이라고만 기술되어 있으나, 동시에 같은 토큰으로 2개 이상의 요청이 들어오는 경우의 원자적 무효화 전략이 없다. --  토큰을 1회 사용 후 무효화하면, 대용량 파일의 Range 기반 이어받기(resume)가 불가능해진다. 네트워크 끊김 후 클라이언트가 `Range: bytes=10485760-`으로 재요청하면, 이미 무효화된 토큰이므로 `401`이 반환된다. -- ---- - -### 5. storage_key → 로컬 FS 경로 resolve - -`storage_key`를 실제 파일 시스템 경로로 변환한다. - -- 키-경로 맵(DB 또는 설정 파일)을 조회하여 절대 경로 획득 -- **Path traversal 방어 필수**: `realpath()` 등으로 경로를 정규화하고, 허용된 루트 디렉터리(`/data/uploads/` 등) 내에 있는지 반드시 검증 -- 파일이 존재하지 않으면 **`404 Not Found`** 반환 - ---- - -### 6. 파일 스트림 응답 (Range 지원 권장) - -파일을 청크 단위로 스트리밍하여 응답한다. - -**응답 헤더:** - -|헤더|값| -|---|---| -|`Content-Type`|파일 MIME 타입| -|`Content-Disposition`|`attachment; filename="파일명"`| -|`Accept-Ranges`|`bytes`| -|`Content-Length`|파일 크기 (전체 또는 청크)| - -**Range 지원 시 동작:** - -- `Range` 헤더가 있으면 **`206 Partial Content`** 로 응답 -- `Range` 헤더가 없으면 **`200 OK`** 로 전체 파일 응답 -- Range를 지원하면 대용량 파일 resume, 미디어 스트리밍, 멀티파트 다운로드 모두 대응 가능 - - -#### Risk -- 응답 헤더에 `Content-Type: 파일 MIME 타입`이라고만 되어 있고, 이 값을 **어디서 가져오는지** 명시되어 있지 않다. 업로드 시 클라이언트가 보낸 값을 DB에 저장하고 그대로 사용하는 경우, 공격자가 `text/html`이나 `image/svg+xml`로 위장한 악성 파일을 업로드할 수 있다. --  로컬 FS에서 직접 스트리밍하므로 API/스트리밍 서버의 egress 대역폭이 병목이 된다. 동시 다운로드 수 제한, 사용자별 대역폭 제한, 전체 서버 대역폭 보호 정책이 없다. - - ---- - -### 7. 로그 기록 / 카운트 증가 - -다운로드 완료 후 감사(Audit) 로그를 기록하고, 공유 링크의 경우 카운트를 증가시킨다. - -- **로그 항목**: 파일 ID, 요청자, IP, User-Agent, 다운로드 시각, 파일 크기 -- **카운트 증가**: 공유 링크에만 적용 (`download_count++`) -- **타이밍 주의**: 스트림이 **완전히 전송된 이후**에 카운트를 올린다. 요청 시점에 올리면 중단된 다운로드도 집계된다. - ---- - -## 에러 응답 정리 - -|상황|HTTP 상태 코드| -|---|---| -|인증 실패 / 토큰 만료|`401 Unauthorized`| -|권한 없음|`403 Forbidden`| -|파일 없음|`404 Not Found`| -|Range 범위 초과|`416 Range Not Satisfiable`| -|정상 전체 응답|`200 OK`| -|정상 부분 응답 (Range)|`206 Partial Content`| - ---- - -## 보안 체크리스트 - -- [ ] JWT / share_token 서명 검증 -- [ ] 파일 접근 권한 DB 조회 (캐시 사용 시 TTL 주의) -- [ ] 다운로드 토큰 단명 발급 (TTL ≤ 60s) -- [ ] 토큰 1회 사용 후 무효화 -- [ ] Path traversal 방어 (`realpath` + 루트 경로 검증) -- [ ] `Content-Disposition: attachment` 헤더 명시 -- [ ] 다운로드 완료 후 감사 로그 기록 \ No newline at end of file diff --git a/설계/img/download_flowchart_part1.svg b/설계/img/download_flowchart_part1.svg new file mode 100644 index 0000000..9a84967 --- /dev/null +++ b/설계/img/download_flowchart_part1.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + 클라이언트 요청 + + + + +다운로드 시도 + + + + + 1단계 — 사용자 인증 + JWT Bearer 또는 share_token + + + + + + +토큰 유효? + + + +No + + + 401 Unauthorized + + + + +Yes + + + + + 2단계 — 파일 권한 검증 + 소유자·공유 링크·만료·비밀번호 확인 + + + + + + +접근 허용? + + + +No + + + 404 Not Found + + + + +Yes + + + + + 3단계 — 메타데이터 사전 검증 + storage_key · 삭제/손상/격리 상태 확인 + + + + + + +다운로드 가능? + + + +No + + + 404 Not Found + + + + +Yes + + + + + 4단계 — 세션 토큰 발급 + HMAC/JWT · TTL 5분 · fileId 바인딩 + + + + + + + + 클라이언트에 응답 + downloadUrl + sessionToken + expiresAt + + + + + + + + → 파트 2: 스트리밍 단계 + + + + +인증·세션 + +권한·검증 + +에러 응답 + +시작·종료 + + \ No newline at end of file diff --git a/설계/img/download_flowchart_part2.svg b/설계/img/download_flowchart_part2.svg new file mode 100644 index 0000000..b67aa05 --- /dev/null +++ b/설계/img/download_flowchart_part2.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + GET /files/stream?sessionToken=… + + + + + + + + 5단계 — 세션 토큰 검증 + 만료·위변조·fileId 불일치 확인 + + + + + + +토큰 유효? + + + +No + + + 401 Unauthorized + + + + +Yes + + + + + 6단계 — 경로 resolve 및 최종 검증 + realpath() · 루트 경로 검증 · 파일 open 확인 + + + + + + +파일 존재·접근 가능? + + + +No + + + 404 + 경고 로그 + + + + +Yes + + + + + 7단계 — 파일 스트림 응답 + 200 OK / 206 Partial · Content-Disposition: attachment + + + + + + +Range 헤더 있음? + + + +Yes + + + 206 Partial Content + + + + +No + + + 200 OK (전체 파일) + + + + + + + + + + + 8단계 — 로그 기록 / 카운트 증가 + attempt / completed 분리 집계 · 감사 로그 + + + + + + + + 다운로드 완료 + + + + +인증·세션·로그 + +검증·스트리밍 + +에러 응답 + +시작·종료·상태 + + \ No newline at end of file diff --git a/설계/img/file_download_flow.svg b/설계/img/file_download_flow.svg deleted file mode 100644 index a564223..0000000 --- a/설계/img/file_download_flow.svg +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - 1. 사용자 인증 / 공유 링크 접근 - JWT 토큰 또는 share_token 파싱 - - - - - - - - 2. 파일 권한 / 공유 상태 검증 - DB 조회 → 접근 허용 여부 확인 - - - - - - - 403 Forbidden - - - - - - - - 3. 다운로드 토큰 발급 - 단기 서명 토큰 생성 (TTL ~60s) - - - - - - - - 4. 토큰으로 파일 스트림 요청 - GET /stream?token=… 호출 - - - - - - - 401 Expired - - - - - - - - 5. storage_key → 로컬 FS 경로 resolve - 키-경로 맵 조회 / path traversal 방어 - - - - - - - 404 Not Found - - - - - - - - 6. 파일 스트림 응답 - 206 Partial Content (Range 지원) - - - - - - - - 7. 로그 기록 / 카운트 증가 - 공유 링크인 경우 download_count++ - - - - - - 클라이언트 - - - - Auth / ACL - - - - Token Service - - - - Storage Layer - - - - Stream Handler - - - - Audit Logger - - - - -정상 흐름 → - -에러 분기 → - \ No newline at end of file diff --git a/설계/img/file_download_sequence_chart.svg b/설계/img/file_download_sequence_chart.svg new file mode 100644 index 0000000..c56b7ef --- /dev/null +++ b/설계/img/file_download_sequence_chart.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + 클라이언트 + + + + Auth + + + + 권한 검증 + + + + 파일 서버 + + + + 스토리지 + + + + + + + + + + + +1단계 — 사용자 인증 / 공유 링크 접근 + + + +JWT / share_token + + + +401 or subjectType/Id + + + +2단계 — 파일 권한 / 공유 상태 검증 + + + +파일 접근 요청 + + + +subjectType/Id 전달 + + + +권한 없음/파일 없음 → 404 + + + +3단계 — 파일 메타데이터 및 다운로드 가능 상태 검증 + + + +메타데이터 조회 + + + +storage_key, 상태 확인 + + + +다운로드 불가 → 404 + + + +4단계 — 다운로드 세션 토큰 발급 (TTL ~5분) + + + +HMAC/JWT 서명 +fileId + subjectId 바인딩 + + + + +downloadUrl + sessionToken + + + +5단계 — 세션 토큰으로 파일 스트림 요청 + + + +GET /files/stream?sessionToken [Range: bytes=…] + + + +토큰 검증 / 만료 확인 + + + + +만료/위변조 → 401 + + + +6단계 — storage_key → 실제 경로 resolve 및 최종 검증 + + + +storage_key 조회 + + + +절대 경로 반환 + + + +realpath() 정규화 +루트 경로 검증 + + + + +경로 이탈 / 파일 없음 → 404 + + + +7단계 — 파일 스트림 응답 + + + +파일 스트림 open + + + +200 OK / 206 Partial Content + + + +Content-Disposition: attachment +Content-Type, Accept-Ranges, ETag +Cache-Control: private, no-store + + + + +8단계 — 로그 기록 / 카운트 증가 + + + +download_attempt_count 증가 +전송 완료 시 completed_count 증가 +감사 로그 기록 (공유 링크 한정) + + + + +요청 (동기) + +응답 / 에러 + +내부 처리 + + \ No newline at end of file diff --git a/설계/pipeline/다운로드 파이프라인.md b/설계/pipeline/다운로드 파이프라인.md new file mode 100644 index 0000000..5209b9b --- /dev/null +++ b/설계/pipeline/다운로드 파이프라인.md @@ -0,0 +1,284 @@ +## 개요 + +이 문서는 **인증 사용자**와 **공유 링크 사용자** 모두를 지원하는 파일 다운로드 시스템의 전체 흐름을 정의한다. 핵심 구조는 다음과 같다: + +> **권한 검증 → 다운로드 가능 상태 사전 검증 → 다운로드 세션 발급 → 최종 경로 검증 및 스트리밍** + +--- + +## 전체 흐름 (8단계) +![[download_flowchart_part1.svg]] + +![[download_flowchart_part2.svg]] +--- + +## 단계별 상세 + +### 1단계: 사용자 인증 / 공유 링크 접근 + +**목적:** 요청자의 신원을 확인하고 내부 컨텍스트로 정규화한다. + +| 접근 경로 | 인증 방식 | 정규화 결과 | +|-----------|-----------|-------------| +| 인증 사용자 | `Authorization` 헤더의 JWT Bearer 토큰 | `subjectType=user`, `subjectId={userId}` | +| 공유 링크 사용자 | 쿼리 파라미터 또는 별도 엔드포인트의 `share_token` | `subjectType=share_link`, `subjectId={shareLinkId}` | + +**에러 처리:** + +| 상황 | 응답 | +|------|------| +| 인증 토큰 유효하지 않음 / 만료 | `401 Unauthorized` | +| 공유 토큰 유효하지 않음 | `404 Not Found` | + +--- + +### 2단계: 파일 권한 / 공유 상태 검증 + +**목적:** DB 조회를 통해 요청자가 해당 파일에 접근할 수 있는지 확인한다. + +**검증 항목:** + +| 접근 유형 | 검증 내용 | +| --------- | ---------------------------------------- | +| 인증 사용자 | 파일 소유자 여부, 공유 대상 여부, 조직/프로젝트/폴더 단위 접근 권한 | +| 공유 링크 사용자 | 링크 활성화 여부, 만료 여부, 비밀번호 검증, 다운로드 허용 여부 | + +**핵심 원칙:** 파일 존재 여부 노출을 최소화하기 위해 아래 경우는 **모두 `404 Not Found`** 로 처리한다. + +- 파일이 실제로 존재하지 않음 +- 요청자에게 접근 권한이 없음 +- 공유 링크가 파일에 매핑되지 않음 +- 비활성화된 공유 링크로 접근 시도 + +> 외부 클라이언트 관점에서 **"접근 불가"와 "존재하지 않음"을 구분할 수 없다.** + +--- + +### 3단계: 파일 메타데이터 및 다운로드 가능 상태 검증 + +**목적:** 다운로드가 불가능한 파일에 대해 세션을 발급하지 않기 위한 **사전 검증**이다. + +**검증 항목:** + +- 파일 메타데이터 존재 여부 +- `storage_key` 존재 여부 +- 삭제 / 손상 / 비활성 / 격리 상태 여부 +- 다운로드 정책상 차단 대상 여부 +- (가능 시) 실제 스토리지 객체 존재 여부 + +**실패 시:** `404 Not Found` + +> ⚠️ 이 단계는 사전 검증이다. 세션 발급 후 실제 스트리밍 시점까지 파일 상태가 변할 수 있으므로, **최종 검증은 6단계에서 다시 수행**한다. + +--- + +### 4단계: 다운로드 세션 토큰 발급 + +**목적:** 실제 파일 스트리밍에 사용할 세션 토큰을 발급한다. + +**세션 토큰 속성:** + +| 항목 | 값 | +|------|-----| +| TTL | 약 5분 | +| 포함 정보 | 파일 ID, 요청자 식별자(`subjectType`, `subjectId`), 세션 ID, 만료 시각 | +| 보호 방식 | HMAC 또는 JWT 서명, 필요 시 Redis/DB에 메타데이터 저장 | + +**세션이 지원하는 요청 유형:** + +- 브라우저의 동일 다운로드 재요청 +- `Range` 기반 이어받기(resume) +- 네트워크 끊김 후 재시도 +- 일부 브라우저/다운로드 매니저의 병렬 Range 요청 + +**응답 예시:** + +```json +{ + "downloadUrl": "/files/stream?sessionToken={download_session_token}", + "expiresAt": "2026-03-17T12:00:00Z" +} +``` + +**리스크 및 대응:** + +| 리스크 | 대응 | +|--------|------| +| TTL이 너무 길면 URL 탈취 악용 가능 | TTL 5분 내외로 제한 | +| TTL이 너무 짧으면 대용량 파일 재요청 실패 | 세션 TTL 내 반복 요청 허용 | +| 토큰 유출 | 파일 ID + 요청자 식별자에 바인딩, 선택적 IP/UA 바인딩 | + +**추가 보안:** + +- 다운로드 세션은 **발급 시점 권한**을 기준으로 TTL 동안 유효 +- Access log 마스킹 +- `Cache-Control: private, no-store` + +--- + +### 5단계: 세션 토큰으로 파일 스트림 요청 + +**클라이언트 요청 예시:** + +```http +GET /files/stream?sessionToken={download_session_token} +Range: bytes=0-1048575 +``` + +**처리 규칙:** + +| 상황 | 처리 | +|------|------| +| 세션 토큰 만료 / 위변조 | `401 Unauthorized` | +| 세션이 가리키는 파일과 요청 대상 불일치 | `404 Not Found` | +| 세션 TTL 내 반복 GET / Range / resume | ✅ 허용 | +| Multi-range 요청 | ❌ 미지원 (정책에 따라 `416` 또는 전체 응답) | + +**설계 원칙:** + +> 세션 자체는 짧게 유지하되, 세션이 살아있는 동안은 브라우저 다운로드 모델을 수용한다. + +**"1회용 토큰 + 즉시 무효화" 방식을 사용하지 않는 이유:** + +브라우저는 다운로드 중 동일 URL로 재요청, Range resume, 병렬 요청, 프록시 중복 요청 등을 발생시킬 수 있어 충돌한다. + +--- + +### 6단계: `storage_key` → 실제 파일 경로 resolve 및 최종 검증 + +**목적:** 스트림을 열기 직전에 실제 파일 경로를 확정하고 최종 검증을 수행한다. + +**처리 절차:** + +1. 키-경로 맵(DB 또는 설정) 조회 → 절대 경로 획득 +2. `realpath()` 등으로 경로 정규화 +3. 정규화된 경로가 허용된 루트 디렉터리 내부인지 검증 (예: `/data/uploads/`) +4. 실제 파일 존재 여부 확인 +5. 파일 open 가능 여부 확인 + +**보안 요구사항:** + +- Path traversal 방어 필수 (`../`, 심볼릭 링크 우회, 상대 경로 등 차단) +- 허용 루트 외부로 벗어나면 즉시 실패 처리 + +**에러 처리:** + +| 상황 | 응답 | 내부 처리 | +|------|------|-----------| +| 실제 파일 미존재 | `404 Not Found` | - | +| 메타데이터 존재하나 스토리지 파일 누락 | `404 Not Found` | 경고 로그 / 장애 알림 | + +--- + +### 7단계: 파일 스트림 응답 + +**기본 응답 헤더:** + +| 헤더 | 값 | +|------|-----| +| `Content-Type` | 서버가 결정한 안전한 MIME 타입 | +| `Content-Disposition` | `attachment; filename="..."; filename*=UTF-8''...` | +| `Accept-Ranges` | `bytes` | +| `Content-Length` | 전체 또는 부분 응답 크기 | +| `ETag` | 파일 버전 식별자 | +| `Last-Modified` | 파일 최종 수정 시각 (선택) | + +**Range 처리:** + +| 조건 | 응답 | +|------|------| +| `Range` 헤더 없음 | `200 OK` (전체 파일) | +| `Range` 헤더 있음 | `206 Partial Content` (부분 응답) | +| 범위 초과 | `416 Range Not Satisfiable` | +| `If-Range` 일치 | 부분 응답 | +| `If-Range` 불일치 | 전체 파일 재전송 또는 정책에 따라 처리 | + +**MIME 타입 처리 원칙:** + +1. 서버 측 MIME 판별 사용 (클라이언트 업로드 값 불신) +2. 저장된 메타데이터와 확장자는 보조적으로 사용 +3. 판별 불확실 시 `application/octet-stream` +4. XSS 가능 타입이라도 `Content-Disposition: attachment`로 강제 + +**파일명 처리 원칙:** + +- 제거/이스케이프 대상: `"`, `\r`, `\n`, `\0`, `/`, `\` +- 최대 길이 제한 적용 +- 비 ASCII 문자는 `filename*`에 RFC 5987 방식으로 인코딩 + +```http +Content-Disposition: attachment; filename="safe_name.ext"; filename*=UTF-8''original%20name.ext +``` + +**성능 / 운영 보호 정책:** + +- 사용자별 동시 다운로드 수 제한 +- 공유 링크별 rate limit +- 전체 서버 동시 스트림 수 제한 +- 대용량 파일 전송 속도 제한 (필요 시) +- `sendfile` / `X-Accel-Redirect` / `X-Sendfile` 활용 검토 + +### 8단계: 로그 기록 / 카운트 증가 + +**카운트 증가 정책:** + +- **공유 링크 다운로드에만 적용** +- `download_count`는 **세션 단위 전체 파일 다운로드 완료 시점**에만 증가 +- 운영 분석을 위해 시도 수와 완료 수를 분리 집계 가능 + +| 카운트 항목 | 증가 시점 | 용도 | +|------------|-----------|------| +| `download_attempt_count` | 스트림 요청 시작 시점 | 시도 횟수 추적 | +| `download_completed_count` | 전체 파일 전송 완료 시점 | 실제 완료 횟수 추적 | + +> 분리 집계를 통해 네트워크 중단, 사용자 취소, 실패율 등을 더 정확히 분석할 수 있다. + +--- + +## 에러 응답 정리 + +| 상황 | HTTP 상태 코드 | +|------|----------------| +| 인증 실패 / 인증 토큰 만료 | `401 Unauthorized` | +| 다운로드 세션 토큰 만료 / 위변조 | `401 Unauthorized` | +| 파일 없음 또는 권한 없음 | `404 Not Found` | +| Range 범위 초과 | `416 Range Not Satisfiable` | +| 정상 전체 응답 | `200 OK` | +| 정상 부분 응답 (Range) | `206 Partial Content` | + +--- + +## 보안 체크리스트 + +| # | 항목 | 관련 단계 | +|---|------|-----------| +| 1 | JWT / share token 서명 검증 | 1단계 | +| 2 | 파일 접근 권한 및 공유 상태 검증 | 2단계 | +| 3 | 권한 없음과 파일 없음은 외부에 동일하게 `404` 처리 | 2단계 | +| 4 | 파일 메타데이터 및 다운로드 가능 상태 사전 검증 | 3단계 | +| 5 | 다운로드 세션 토큰 단명 발급 (권장 TTL: 약 5분) | 4단계 | +| 6 | 세션 토큰은 파일 ID 및 요청자 식별자에 바인딩 | 4단계 | +| 7 | 세션 TTL 내 GET / Range / resume / 병렬 요청 허용 | 5단계 | +| 8 | Path traversal 방어 (`realpath` + 루트 경로 검증) | 6단계 | +| 9 | 실제 스트리밍 직전 파일 존재 및 open 가능 여부 재검증 | 6단계 | +| 10 | `Content-Disposition: attachment` 강제 | 7단계 | +| 11 | 파일명 sanitize 및 `filename*` 지원 | 7단계 | +| 12 | MIME 타입은 서버 기준으로 안전하게 결정 | 7단계 | +| 13 | `ETag` / `If-Range` 지원 | 7단계 | +| 14 | 다운로드 로그 및 감사 로그 기록 | 8단계 | +| 15 | 동시 다운로드 수 / rate limit / 대역폭 보호 정책 적용 | 7~8단계 | + +--- + +## 설계 요약 + +이 설계는 다음 **6가지 목표**를 동시에 만족한다. + +| 목표 | 달성 방법 | +|------|-----------| +| **파일 존재 여부 노출 최소화** | 권한 없음 / 파일 없음 모두 `404` 통일 처리 | +| **권한 검증 강화** | 인증 사용자·공유 링크 모두 다단계 검증 | +| **불필요한 세션 발급 방지** | 세션 발급 전 다운로드 가능 상태 사전 검증 | +| **브라우저 호환성 확보** | 세션 TTL 내 GET / Range / resume / 병렬 요청 허용 | +| **재생 공격 위험 완화** | 단명 세션 토큰 + 파일·요청자 바인딩 | +| **운영 환경 자원 보호** | 동시 다운로드 제한, rate limit, 대역폭 제어 | \ No newline at end of file diff --git a/설계/pipeline/업로드 파이프라인.md b/설계/pipeline/업로드 파이프라인.md new file mode 100644 index 0000000..cf188b6 --- /dev/null +++ b/설계/pipeline/업로드 파이프라인.md @@ -0,0 +1,194 @@ + +## 1. 업로드 준비 단계 + +### 1-1. 클라이언트가 업로드 대상 폴더를 선택한다 + +클라이언트는 업로드 요청 시 아래 정보를 서버에 전달한다. + +|항목|설명| +|---|---| +|`targetFolderId`|업로드 대상 폴더 식별자| +|원본 파일명|사용자가 선택한 파일의 이름| +|파일 크기|바이트 단위 파일 크기| +|클라이언트 MIME 타입|브라우저가 판단한 MIME 타입 (신뢰 금지)| +|체크섬 정보|(선택) 무결성 검증용 해시값| + +### 1-2. 서버가 사용자 인증 및 권한을 검증한다 + +- 로그인 여부 확인 +- 대상 폴더에 대한 쓰기 권한 확인 +- 타 사용자 폴더 접근 차단 + +### 1-3. 서버가 업로드 사전 검증을 수행한다 + +- 파일명 규칙 검증 +- 허용 최대 크기 검증 +- 확장자 / 업로드 정책 검증 +- 사용자 quota 사전 검사 +- 동일 폴더 내 중복 이름 정책 검사 + +### 1-4. 서버가 업로드 대상 폴더를 검증한다 + +- 폴더 존재 여부 확인 +- 삭제 상태 여부 확인 +- 업로드 가능 상태 여부 확인 + +--- + +## 2. 업로드 세션 생성 단계 + +### 2-1. 서버가 `UploadSession`을 생성한다 + +|항목|내용| +|---|---| +|상태|`CREATED`| +|업로드 대상 폴더 ID|`targetFolderId` 저장| +|파일 정보|원본 파일명 / 예상 크기 저장| +|임시 저장 경로|임시 저장 경로 또는 임시 키 발급| +|tus 매핑|tus 업로드 식별자 매핑 준비| + +### 2-2. 서버가 클라이언트에 tus 업로드 정보를 반환한다 + +- 업로드 URL +- 업로드 세션 ID +- 재개에 필요한 메타데이터 + +--- + +## 3. 파일 전송 단계 + +### 3-1. 클라이언트가 tus 프로토콜로 청크 업로드를 시작한다 + +- 청크 단위 전송 +- 오프셋 검증 +- 중단 시 재개 가능 +- 서버는 진행 상태를 `UploadSession.received_size` 등에 반영 + +--- + +## 4. 업로드 완료 확정 단계 + +### 4-1. 서버가 업로드 완료 이벤트 또는 콜백을 수신한다 + +- tus 훅 또는 완료 엔드포인트 기반 처리 +- 동일 세션에 대한 중복 완료 요청 방지 필요 (idempotency) + +### 4-2. 서버가 임시 업로드 파일을 최종 검증한다 + +- 실제 파일 크기 검증 +- MIME 타입 서버 측 추정 (클라이언트 값 신뢰 금지) +- 손상 여부 또는 체크섬 검증 (선택) +- quota 최종 재검사 +- 위험 파일 정책 검사 (필요 시) + +### 4-3. 서버가 최종 저장 경로를 결정하고 파일을 이동한다 + +- `storage_key` 생성 +- 임시 경로 → 최종 object 경로로 **원자적 이동** +- 사용자 표시 경로와 실제 디스크 경로는 분리 + +### 4-4. 서버가 `FileItem` 메타데이터를 생성한다 + +|필드|설명| +|---|---| +|`display_name`|사용자에게 표시되는 파일명| +|`folder_id`|소속 폴더 ID| +|`mime_type`|서버 측 추정 MIME 타입| +|`size_bytes`|실제 파일 크기| +|`storage_key`|내부 저장 경로 식별자| +|`preview_status`|초기값 `PENDING`| +|사용자 사용량|`storage_used_bytes` 갱신 (필요 시)| + +### 4-5. 서버가 `UploadSession` 상태를 완료로 변경한다 + +|항목|내용| +|---|---| +|상태|`COMPLETED`| +|완료 시각|기록| +|`file_id`|최종 생성된 `FileItem`과 연결| + +--- + +## 5. 후처리 단계 + +### 5-1. 서버가 후처리 작업을 비동기로 등록한다 + +|작업|유형| +|---|---| +|이미지 썸네일 생성|동기 또는 큐 기반| +|텍스트 / PDF 미리보기 캐시 생성|큐 기반| +|바이러스 스캔|후속 비동기| +|검색 인덱싱|후속 비동기| +|감사 로그 기록|동기 또는 큐 기반| + +### 5-2. 서버가 클라이언트에 업로드 완료 응답을 반환한다 + +- 생성된 `FileItem` 정보 +- 목록 갱신에 필요한 최소 메타데이터 +- 프론트엔드는 현재 폴더 목록을 갱신 + +--- + +## 6. 보강 포인트 + +### A. 상태 전이 + +`UploadSession`의 상태는 아래와 같이 전이한다. + +``` +CREATED → UPLOADING → COMPLETED + ↘ FAILED + ↘ ABORTED + ↘ EXPIRED (선택) +``` + +|상태|설명| +|---|---| +|`CREATED`|세션 생성 완료, 아직 전송 시작 전| +|`UPLOADING`|청크 전송 진행 중| +|`COMPLETED`|모든 검증 및 저장 완료| +|`FAILED`|완료 후 검증 실패 또는 저장 오류| +|`ABORTED`|클라이언트 또는 서버 측 명시적 중단| +|`EXPIRED`|세션 TTL 만료 (선택 구현)| + +--- + +### B. 실패 처리 + +|시나리오|처리 방식| +|---|---| +|업로드 중 네트워크 끊김|세션 유지, tus 재개 허용| +|완료 후 검증 실패|`UploadSession = FAILED`, 임시 파일 삭제| +|quota 초과|최종 확정 거부, 파일 반영 금지| +|파일 이동 성공 후 DB 저장 실패|고아 파일 정리 배치 또는 보상 로직 필요| +|완료 콜백 중복 호출|idempotency 보장 필요| + +--- + +### C. 트랜잭션 경계 + +아래 순서는 원자성 보장이 중요하며, 메타데이터 반영은 하나의 트랜잭션으로 묶는 것을 권장한다. 파일 이동은 트랜잭션 외부에서 수행되므로 전후 보상 로직이 필요하다. + +``` +1. 임시 파일 검증 +2. 최종 파일 이동 ← 트랜잭션 외부 (파일시스템 작업) +──────────────────────────── 트랜잭션 시작 +3. FileItem 생성 +4. 사용자 사용량 갱신 +5. UploadSession 완료 처리 +──────────────────────────── 트랜잭션 커밋 +``` + +> **주의:** 파일 이동 성공 후 트랜잭션 실패 시 고아 파일이 발생할 수 있으므로, 정기 정리 배치 또는 보상 트랜잭션을 설계해야 한다. + +--- + +### D. 보안 포인트 + +|항목|내용| +|---|---| +|클라이언트 MIME 신뢰 금지|서버에서 직접 MIME 타입을 추정하여 사용| +|사용자 입력 파일명 검증|특수문자, 예약어, 인코딩 우회 등 검사| +|Path Traversal 방지|`../` 등 경로 탈출 시도 차단| +|내부 실제 경로 외부 노출 금지|`storage_key`와 응답 경로 분리| +|최종 저장 경로 격리|사용자 표시 폴더 구조와 실제 디스크 경로 분리|