1단계 FileStore API 계약
1. 범위
이 문서는 PortalConsole에 의존하지 않고 포탈 서버가 FileStore를 직접 호출하는 1단계 연동 계약을 정의한다. 포탈은 FileStore의 저장 Backend와 자격증명을 알지 않는다. 포탈은 서버 간 인증과 포탈 식별자, FileStore 관리자 UI에서 등록한 정책 ID만 사용한다.
모든 API는 다음 기본 경로를 사용한다.
/api/file-store/v12. 인증
포탈 서버가 호출하는 API에는 다음 헤더를 포함한다.
Authorization: Bearer <FILE_STORE_SERVICE_TOKEN>
Content-Type: application/json서비스 토큰은 브라우저에 전달하지 않는다. 포탈의 클라이언트가 직접 FileStore API를 호출하지 않도록 하고, 포탈 서버가 요청을 중계한다.
포탈 서버는 권한 판정 결과와 추적 ID를 요청 body의 metadata로 전달할 수 있다. authorizationOperation은 API 세부 경로명이 아니라 권한 동작 종류를 나타내며, 허용 값은 read, write, delete이다. 업로드 세션과 업로드 완료는 모두 write로 표시한다.
{
"requestId": "request-uuid",
"authorizationIssuer": "process-center",
"authorizationAllowed": true,
"authorizationOperation": "write"
}3. 정책 조회
정책 조회는 포탈 서버가 자신의 consumerId와 mappingId를 확인할 때 사용한다.
GET /api/file-store/v1/consumer-policies응답에는 저장 정책 메타데이터가 포함될 수 있지만, Access Key, Secret Key, 관리자 세션, 서버 파일 경로는 포함하지 않는다.
{
"data": [
{
"id": "policy-uuid",
"consumerId": "document-library",
"backendId": "r2-primary",
"pathPrefix": "attachments",
"maxBytes": 104857600,
"allowedContentTypes": ["application/pdf", "image/png"],
"encryptionRequired": true,
"readEnabled": true,
"writeEnabled": true,
"enabled": true
}
]
}4. 업로드 세션
포탈 서버는 파일 자체를 먼저 보내지 않고 업로드 세션을 요청한다.
POST /api/file-store/v1/assets/upload-session{
"consumerId": "document-library",
"mappingId": "policy-uuid",
"ownerType": "document",
"ownerId": "document-123",
"fileName": "manual.pdf",
"contentType": "application/pdf",
"classification": "INTERNAL",
"organizationIds": ["00000000-0000-0000-0000-000000000001"],
"sourceVersion": "v1",
"sourceMenuId": "board",
"sourceEntityType": "post",
"sourceEntityId": "post-123",
"context": { "team": "hr", "revision": 3 },
"actorSubject": "portal-user-or-service-subject",
"metadata": {
"requestId": "request-uuid",
"authorizationIssuer": "process-center",
"authorizationAllowed": true,
"authorizationOperation": "write"
}
}sourceMenuId·sourceEntityType·sourceEntityId·context 는 선택 필드다. 소비 포탈이 파일의 출처(사내 메뉴·게시물 등)를 함께 넘기면 FileStore가 파일에 보존해 이후 파일 관리·분석에 쓴다. context 는 문자열/숫자/불리언 값의 평면 map 이다.
FileStore는 다음 항목을 검증한다.
consumerId와mappingId의 일치 여부- 정책과 Backend의 활성 상태
- write 권한
- 파일명과 MIME type 형식
- 포탈 정책의 저장 경로
응답에는 파일 ID와 제한된 업로드 URL이 포함된다.
{
"data": {
"asset": {
"id": "asset-uuid",
"consumerId": "document-library",
"mappingId": "policy-uuid",
"status": "PENDING_SCAN",
"storageKey": "attachments/document-library/asset-uuid/manual.pdf"
},
"uploadUrl": "https://storage-provider/upload-url"
}
}uploadUrl은 브라우저에 전달할 수 있지만, 서비스 토큰은 전달하지 않는다. URL의 만료 시간과 사용 범위는 Storage Backend 구현에 따른다.
4.1 업로드 보안 검증
FileStore는 포탈의 사전 검증과 별개로 다음 규칙을 최종 적용한다.
- 파일명에는 제어문자,
\,/,.또는..만으로 된 경로 요소를 사용할 수 없다. contentType에는 MIME type만 허용하며; charset=...같은 parameter를 포함할 수 없다.- HTML, XHTML, JavaScript 및 SVG(
text/html,application/xhtml+xml,application/javascript,text/javascript,image/svg+xml)는 업로드할 수 없다. SVG를 허용해야 하는 포탈은 별도 sanitization·변환 파이프라인을 먼저 제공해야 한다. - 직접 base64 업로드를 사용하는 호환 API는
data:<mime>;base64,...의 MIME과contentType이 일치해야 하며, malformed base64와 빈 payload를 거부한다. - 파일 크기와 허용 MIME은 consumer policy의
maxBytes및allowedContentTypes로 다시 검증한다.
이 검증은 파일 확장자만 신뢰하지 않는다. 업로드된 객체의 실제 파일 형식 검사나 malware scanning이 필요한 경우 PENDING_SCAN 상태에서 별도 scanner가 검사한 뒤 ACTIVE로 전환해야 한다.
5. 업로드 완료
파일 업로드가 끝나면 포탈 서버는 FileStore에 완료를 알린다.
POST /api/file-store/v1/assets/:assetId/complete{
"bytes": 48321,
"checksum": "sha256-hex-64-characters",
"actorSubject": "portal-user-or-service-subject"
}FileStore는 저장 객체의 실제 크기를 확인한 뒤 파일을 ACTIVE로 전환한다. 크기가 다르거나 객체가 없으면 완료를 거부한다.
성공 응답은 업로드 세션처럼 data.asset으로 중첩하지 않고, 파일 자체를 data에 반환한다.
{
"data": {
"id": "asset-uuid",
"status": "ACTIVE",
"bytes": 48321,
"checksum": "sha256-hex-64-characters"
}
}6. 파일 조회
GET /api/file-store/v1/assets/:assetId삭제된 파일은 일반 조회에서 404로 처리한다.
7. 다운로드 URL
POST /api/file-store/v1/assets/:assetId/read-url{
"actorSubject": "portal-user-or-service-subject"
}FileStore는 파일이 ACTIVE이고 정책의 readEnabled가 true인 경우에만 locator를 발급한다. 응답 locator는 만료 시간을 가진다. 포탈은 locator를 일반 저장소 자격증명과 함께 저장하지 않는다.
파일이 없으면 404 asset_not_found를 반환한다. 일반적인 요청 형식 오류나 정책 처리 오류와 구분할 수 있도록 포탈은 이 응답을 “읽을 수 없는 파일”으로 처리해야 한다.
8. 삭제 (출처 단위)
삭제는 소비 포탈의 출처(게시물/노드) 수명주기가 결정한다. FileStore 는 삭제를 결정하지 않고, 포탈이 게시물/노드를 지울 때 그 출처에 묶인 파일을 출처 단위로 하드 삭제한다.
POST /api/file-store/v1/assets/delete-by-source
Authorization: Bearer <FILE_STORE_SERVICE_TOKEN>
X-FileStore-Consumer: document-library{
"consumerId": "document-library",
"sourceEntityType": "document",
"sourceEntityId": "document-123",
"actorSubject": "directory-admin"
}FileStore 는 레코드·저장 객체를 즉시 삭제하고 파일별 DELETED 감사 이벤트를 남긴다. 보존·법적보존 개념은 두지 않으며, 매칭이 없으면 deleted: 0(멱등)이다.
9. 상태와 오류
| 상태 | 의미 |
|---|---|
PENDING_SCAN | 업로드는 완료되었지만 아직 활성화되지 않은 상태 |
ACTIVE | 일반 읽기와 정책상 후속 작업이 가능한 상태 |
QUARANTINED | 검사 또는 보안 정책에 의해 격리된 상태 |
| 오류 | 처리 원칙 |
|---|---|
401 service_auth_required | 서버 간 토큰을 확인하고 재시도한다. 브라우저에는 노출하지 않는다. |
404 asset_not_found | 파일이 없다. 새 read URL을 재시도하지 않는다. |
| 정책 불일치 | consumerId와 mappingId를 관리자 UI에서 확인한다. |
| 비활성 정책 또는 Backend | 관리자 UI에서 활성 상태와 Backend 연결을 확인한다. |
| MIME·크기 제한 초과 | 포탈의 사전 검증과 FileStore의 최종 검증을 모두 통과해야 한다. |
| 업로드 객체 검증 실패 | 업로드 URL과 완료 요청의 파일 ID를 확인한다. |
10. 1단계 완료 기준
다음 흐름이 실제 Backend 구현과 테스트에서 확인되면 1단계 API 계약이 완료된 것으로 본다.
정책 조회
→ 업로드 세션
→ 저장소 업로드
→ 업로드 완료
→ ACTIVE 전환
→ 다운로드 URL 발급
→ 출처 삭제(delete-by-source)
→ 감사 로그 확인11. LOCAL_VOLUME 서명 업로드·다운로드
LOCAL_VOLUME backend는 공개 객체 저장소가 아니므로, 4. 업로드 세션의 uploadUrl과 7. 다운로드 URL의 locator가 FileStore가 직접 제공하는 서명 URL로 발급된다. 서명 키는 FILE_STORE_SERVICE_TOKEN에서 파생하며 URL은 만료된다(기본 900초, 최대 3600초).
PUT /api/file-store/v1/local-uploads/<assetId>?expires=<epoch-ms>&signature=<hex> # 본문 = 원본 바이트, 201 반환
GET /api/file-store/v1/local-objects/<assetId>?expires=<epoch-ms>&signature=<hex> # 스트림 반환- 서명·만료가 유효하지 않으면
401 { "error": "invalid_signature" }다. 두 경로 모두 서비스 토큰 인증을 쓰지 않고 URL 서명만으로 통과한다. - 업로드는
PENDING_SCAN상태 파일에만 허용하고(활성 파일 덮어쓰기 금지), 소비 정책(maxBytes·allowedContentTypes·writeEnabled, HTML/JS/SVG 차단)을 그대로 적용한다. 바이트 기록 후 소비자는5. 업로드 완료를 호출해 크기·체크섬을 확정한다. - base 주소는
FILE_STORE_PUBLIC_URL에서 온다. 운영에서는 필수이고(미설정 시 업로드 불가), 개발에서는http://127.0.0.1:<PORT>로 대체한다. CLOUDFLARE_R2·AMAZON_S3·S3_COMPATIBLE은 기존대로 presigned URL을 쓴다(서명 URL은 LOCAL_VOLUME 전용).