HTTP Storage Protocol
HTTP Storage Protocol v1은 Office의 HTTP Storage 어댑터와 스토리지 서비스를 직접 운영하는 HTTP Storage Provider 사이의 요청·응답 규격입니다. Provider가 아래 endpoint를 구현하며 Office가 요청을 보냅니다. Office에 연결하는 방법과 예제는 HTTP Storage에서 안내합니다.
이 문서는 9개 endpoint의 메서드·경로, 요청 헤더와 본문, 응답 필드와 상태 코드를 정의합니다. 공통 인증·경로·크기 제한은 모든 endpoint에 적용하며, 각 작업에서 추가로 필요한 조건은 해당 절에서 설명합니다.
URL과 경로
{PROVIDER_BASE_URL}/tfo-storage/v1/{ENCODED_DOCUMENT_PATH}/{OPERATION}
Provider base URL은 HTTP 또는 HTTPS 주소이며 경로 접두사를 포함할 수 있습니다. 사용자 정보, 쿼리 문자열, fragment, .·.. 경로는 허용하지 않습니다. 끝의 /는 제거한 뒤 프로토콜 경로를 붙입니다.
문서 경로는 Provider 루트에 상대적입니다. 각 UTF-8 경로 세그먼트를 따로 퍼센트 인코딩하고 /로 연결합니다. 루트는 빈 문서 경로이며 /tfo-storage/v1/list처럼 요청합니다. 중간에 빈 세그먼트나 추가 /를 넣지 않습니다.
| 항목 | 예시 |
|---|---|
| Provider base URL | https://storage.example.com/office |
| 문서 경로 | contracts/Proposal 2026.docx |
| 문서 정보 요청 | GET /office/tfo-storage/v1/contracts/Proposal%202026.docx/info |
| 루트 목록 요청 | GET /office/tfo-storage/v1/list |
Provider는 디코딩한 경로를 자신의 저장소 루트 아래에서만 처리합니다. .·.., 경로 세그먼트 안의 /·\, 제어 문자, 잘못된 UTF-8 인코딩과 심볼릭 링크를 통한 루트 이탈을 거부합니다. 쿼리 매개변수, 쿠키, 리디렉션, 임의의 인증 헤더 전달은 사용하지 않습니다. 프록시를 거쳐도 JWT 검증에 쓰는 원시 인코딩 경로가 유지되어야 합니다.
공통 요청 헤더
| 헤더 | 적용 범위 | 값 |
|---|---|---|
X-TFO-Storage-Adapter | 모든 요청, 필수 | 등록한 어댑터 이름. 영문·숫자로 시작하는 1~128자의 영문·숫자·.·_·- |
X-TFO-Storage-Request-JWT | 모든 요청, 필수 | 아래 규칙으로 서명한 JWT 원문. Bearer 접두사를 붙이지 않음 |
Content-Type | put, lock, unlock, mkdir, rename | put은 application/octet-stream, JSON 작업은 application/json |
Content-Length | 본문이 있는 요청, 필수 | 정확한 본문 바이트 수를 나타내는 10진수 정수 하나 |
info, list, get, delete 요청에는 본문과 Content-Type이 없습니다. 서명에는 본문 길이 0과 빈 본문의 SHA-256을 넣습니다. 본문은 multipart나 base64로 감싸지 않으며 chunked 업로드와 압축을 사용하지 않습니다. JSON 본문은 UTF-8로 직렬화한 실제 바이트를 서명합니다.
JWT 서명과 검증
Provider는 문서를 읽거나 변경하기 전에 요청이 연결된 Office 어댑터의 시크릿으로 서명됐는지, 전송 중 메서드·경로·본문이 바뀌지 않았는지 확인해야 합니다. 이를 위해 Office는 공유 시크릿으로 요청 정보를 담은 JWT에 서명하고, Provider는 같은 시크릿으로 서명을 검증한 뒤 실제 요청과 비교합니다. 짧은 유효 기간과 요청별 고유 ID를 함께 검사해 이미 사용한 요청의 재전송도 거부합니다.
서명은 요청의 무결성을 보호하며 문서 내용을 암호화하지는 않습니다. 전송 내용의 보호에는 HTTPS를 사용하고, 사용자·테넌트별 문서 접근 권한은 Provider가 별도로 확인합니다.
Office와 Provider는 어댑터별로 UTF-8 기준 최소 32바이트의 무작위 시크릿을 공유합니다. HMAC 키는 이 문자열의 UTF-8 바이트입니다. 시크릿을 요청 URL이나 본문으로 전달하지 않습니다.
JWT는 compact JWS 형식입니다. 헤더와 payload를 각각 UTF-8 JSON으로 직렬화하고 padding 없는 Base64URL로 인코딩합니다. 두 값을 .으로 연결한 바이트에 HMAC-SHA256을 적용한 뒤, 서명을 같은 Base64URL로 인코딩해 마지막에 붙입니다.
{"alg":"HS256","typ":"tfo-storage-request+jwt"}
다음은 본문 hello(5바이트)를 저장하는 요청의 payload 예시입니다. iat, exp, jti는 실제 호출마다 새로 생성합니다.
{
"iss": "thinkfree-office",
"aud": "tfo-http-storage-provider",
"iat": 1789344000,
"exp": 1789344060,
"jti": "26bc9b5e-9dab-4a78-95d8-b1ae34a5d9eb",
"request": {
"adapter": "customer-storage-a",
"method": "PUT",
"path": "/tfo-storage/v1/contracts/hello.txt/put",
"content_type": "application/octet-stream",
"content_length": 5,
"content_sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"office_connection_id": "example-connection",
"arguments": {"save_type": "save"},
"client_metadata": {"project": "example-project"}
}
}
| 필드 | 타입 | 필수 | 검증 규칙 |
|---|---|---|---|
헤더 alg | string | 예 | 정확히 HS256 |
헤더 typ | string | 예 | 정확히 tfo-storage-request+jwt |
iss | string | 예 | 정확히 thinkfree-office |
aud | string 또는 원소 1개의 배열 | 예 | 유일한 대상이 tfo-http-storage-provider |
iat | integer | 예 | 발급 Unix 시각(초). 현재 시각보다 미래가 아님 |
exp | integer | 예 | 만료 Unix 시각(초). 현재 시각보다 뒤이며 0 < exp - iat <= 60 |
jti | string | 예 | 요청마다 고유한 ID. Office는 UUID를 생성하며 공개 예제는 1~64자를 허용 |
request | object | 예 | 아래 서명된 요청 필드를 담는 객체 |
request.adapter | string | 예 | 실제 어댑터 헤더 및 Provider에 설정한 연결 이름과 일치 |
request.method | string | 예 | 실제 대문자 HTTP 메서드와 일치 |
request.path | string | 예 | Provider base path를 포함한 실제 원시 인코딩 경로와 일치. scheme·host·query는 포함하지 않음 |
request.content_type | string | 헤더가 있을 때 | 실제 Content-Type과 정확히 일치 |
request.content_length | integer | 예 | 실제 수신한 본문의 바이트 수. 본문이 없으면 0 |
request.content_sha256 | string | 예 | 실제 본문 바이트의 SHA-256, 소문자 16진수 64자리 |
request.office_connection_id | string | 아니요 | Office의 불투명한 연결 컨텍스트. 사용자 인증 근거로 사용하지 않음 |
request.arguments | object | 아니요 | save_type 등 작업 컨텍스트. 접근 권한을 부여하는 값이 아님 |
request.client_metadata | object | 아니요 | 호출자가 제공한 JSON. 전송 무결성만 보장하며 Office가 확인한 사용자 신원으로 신뢰하지 않음 |
빈 본문의 SHA-256은 다음과 같습니다.
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
전체 JWT는 최대 5,120바이트입니다. client_metadata는 UTF-8 JSON 기준 2,048바이트 이하이며 객체 키는 공백이 아닌 1~64자, 문자열 값은 최대 512자입니다. 루트 객체의 깊이를 0으로 세어 최대 깊이 8을 허용합니다. 비밀값이나 문서 내용을 넣지 않습니다.
검증 순서
- 실제 HTTP 메서드와 원시 경로, 쿼리 유무, 미디어 유형과 고정 길이를 검사합니다. 길이가 제한을 넘으면 본문을 읽거나 임시 보관하기 전에 거부합니다.
- 허용된 본문을 제한된 메모리 또는 임시 파일로 읽으면서 실제 길이와 SHA-256을 계산합니다. 업로드가 끝나기 전에는 대상 문서를 교체하지 않습니다.
- 어댑터 헤더로 설정된 시크릿을 찾아 JWT 서명·헤더·발급자·대상·시간을 검증합니다.
- 서명된 어댑터·메서드·원시 경로·본문 유형·길이·해시를 실제 요청과 비교합니다.
jti를 원자적으로 기록하고exp까지 재사용을 거부합니다. Provider가 여러 인스턴스라면 같은 재전송 방지 저장소를 사용합니다.- 문서 접근 권한을 확인하고 작업을 실행하거나, 선택적 작업에 대한 정확한 미지원 응답을 반환합니다.
서명 오류나 재사용된 JWT는 저장소에 접근하거나 기능 미지원 여부를 응답하기 전에 거부합니다. 서명 검증은 Provider가 담당하는 사용자·테넌트별 문서 접근 권한 검사를 대체하지 않습니다. 서버의 시각을 동기화하고, 같은 작업을 재요청할 때도 새 iat·exp·jti로 서명합니다. jti는 중복 작업의 성공 결과를 돌려주는 멱등 키가 아닙니다. 응답이 유실되면 저장소 상태를 먼저 확인한 뒤 작업 특성에 맞게 재시도합니다.
응답 형식과 크기 제한
성공한 info, list, get, put 응답에는 정확한 10진수 Content-Length 헤더가 하나 있어야 합니다. 값은 실제 전송한 바이트 수와 같아야 하며, Transfer-Encoding과 Content-Encoding은 허용하지 않습니다. 미디어 유형의 charset 매개변수는 허용하지만 JSON 본문은 UTF-8이어야 합니다.
| 대상 | Content-Type | 제한 |
|---|---|---|
info, list, put 성공 응답 | application/json | 각각 5 MiB (5,242,880바이트) |
get 성공 응답 | application/octet-stream | 300 MiB (314,572,800바이트) |
put 요청 | application/octet-stream | 300 MiB (314,572,800바이트) |
메타데이터의 파일 size | JSON integer | 0~314,572,800 |
list의 entries | JSON array | 최대 10,000개이며 전체 JSON도 5 MiB 이하 |
JSON은 한 번만 직렬화해 바이트 길이를 구하고 그 바이트 그대로 전송합니다. get은 응답 헤더를 보내기 전에 저장소에서 원본 크기를 확인하고 같은 길이만큼 스트리밍합니다. 크기를 알아내려고 문서 전체를 메모리에 올리지 않습니다.
Provider는 더 낮은 문서 제한을 둘 수 있지만 300 MiB를 넘길 수는 없습니다. 파일 메타데이터를 처리하거나 get을 전송하거나 put을 임시 보관하기 전에 검사합니다. 최대값은 포함되므로 5,242,880바이트 JSON과 314,572,800바이트 문서는 제한 내이고, 각각 1바이트 큰 5,242,881과 314,572,801은 거부합니다.
길이 누락·중복·음수·소수, 압축·chunked 응답, 잘못된 미디어 유형, 조기 EOF와 실제 길이 불일치는 현재 작업의 실패입니다. Office가 잘못된 응답을 거부한 뒤에도 Provider는 후속 요청을 처리할 수 있어야 합니다. 인증된 501과 일반 오류 응답에는 아래 별도 규칙을 적용합니다.
성공 상태와 본문
| 작업 | 정상 응답 | 본문 규격 |
|---|---|---|
info, list | 200 OK | JSON 객체가 필수. 빈 본문이나 204로 대체하지 않음 |
get | 200 OK | 파일 전체 바이트. 빈 파일은 Content-Length: 0으로 반환 |
put | 200 OK | docId를 포함하는 JSON 객체가 필수. PUT 응답 참고 |
lock, unlock, mkdir, rename, delete | 204 No Content | 본문 없음 |
Office는 2xx 상태를 성공으로 분류하지만 info·list·get·put에서는 위 미디어 유형·고정 길이·본문 검증도 통과해야 합니다. lock·unlock·mkdir·rename·delete는 다른 2xx도 허용하며 응답 JSON의 특정 필드를 요구하지 않습니다. 204에는 본문을 보내지 않습니다. 이 다섯 작업에서 200 응답으로 결과 텍스트를 보낸다면 text/plain; charset=utf-8을 사용하고, Office가 읽는 상한인 10 MiB 이내로 작성합니다. 이 결과 텍스트로 문서 내용을 반환하지 않습니다.
메타데이터 필드
info는 항목 객체 하나, list는 entries 배열을 가진 객체 하나를 반환합니다. 항목 객체에 아래 이외의 필드를 넣지 않습니다.
| 필드 | 타입 | 필수 | 의미와 제한 |
|---|---|---|---|
path | string | 예 | 루트에 상대적인 디코딩된 경로, 최대 4,096자. 루트는 빈 문자열 |
name | string | 예 | 마지막 경로 세그먼트와 동일한 1~255자 이름. 루트는 고정된 표시 이름 |
type | string | 예 | file 또는 directory |
size | integer | 예 | 파일 바이트 수, 0~314,572,800. 디렉터리는 반드시 0 |
readable | boolean | 예 | 문서를 읽을 수 있는지 여부 |
writable | boolean | 예 | 문서를 쓸 수 있는지 여부 |
locked | boolean | 예 | 현재 잠금 상태 |
locker | string 또는 null | 아니요 | 잠금 소유자, 최대 255자 |
createdAt | string 또는 null | 아니요 | RFC 3339 생성 시각, 최대 64자 |
modifiedAt | string 또는 null | 아니요 | RFC 3339 수정 시각, 최대 64자 |
revision | string 또는 null | 아니요 | 저장소 revision 또는 ETag에 해당하는 값, 최대 1,024자 |
info.path는 요청한 문서 경로와 같아야 합니다. list는 요청한 디렉터리의 바로 아래 항목만 포함하고 같은 경로를 중복해서 반환하지 않습니다. 페이지네이션 매개변수는 없습니다. 잘못된 타입·시각·이름·크기, 알 수 없는 필드, 중복 경로와 하위 디렉터리 내부의 항목은 거부됩니다.
필수 필드는 null을 허용하지 않습니다. 선택 필드는 생략하거나 null로 반환할 수 있습니다. path는 URL 인코딩한 문자열이 아니라 디코딩된 경로이며, createdAt·modifiedAt은 2026-09-14T00:00:00Z처럼 시간대가 포함된 시각입니다. readable·writable은 항목의 접근 가능 여부를 알리는 값이며, 실제 요청의 권한 검사도 계속 수행합니다. revision은 저장소의 버전 정보이며 이 프로토콜의 조건부 요청 헤더나 잠금 토큰으로 자동 사용되지 않습니다.
기계 검증에는 entry schema, info response schema, list response schema를 사용할 수 있습니다. 스키마 검증에 더해 위 경로 관계와 런타임 제한도 확인합니다.
작업별 Request·Response
path는 대상 항목, parent는 새 디렉터리를 만들 부모의 Provider 루트 아래 상대 경로입니다. 경로 전체가 아니라 각 세그먼트를 URL 규칙에 따라 인코딩합니다. 모든 요청에는 공통 인증 헤더인 X-TFO-Storage-Adapter와 X-TFO-Storage-Request-JWT가 필수이며, 쿼리 매개변수는 없습니다.
JSON 요청은 아래에 명시한 필드 하나만 가진 객체입니다. 필수 필드의 생략·null·다른 타입·추가 필드를 허용하지 않습니다. JSON 요청의 Content-Type은 정확히 application/json이며, Content-Length는 UTF-8 직렬화 결과의 바이트 수입니다. 공개 예제의 JSON 요청 제한은 16 KiB입니다. 응답의 5 MiB 제한과 구분합니다.
각 절의 오류 표는 작업별 조건입니다. 모든 endpoint에는 공통 오류의 경로·인증·권한·본문 형식·저장소 장애 조건도 적용합니다. Office가 모든 Provider에 동일한 일반 오류 본문을 강제하지는 않으며, 공개 예제에만 해당하는 충돌·파일시스템 정책은 별도로 표시합니다.
| 작업 | 메서드 | 경로 접미사 | 구현 |
|---|---|---|---|
| info | GET | /{path}/info | 필수 |
| list | GET | /{path}/list | 선택 |
| get | GET | /{path}/get | 필수 |
| put | PUT | /{path}/put | 선택 |
| lock | POST | /{path}/lock | unlock과 함께 선택 |
| unlock | POST | /{path}/unlock | lock과 함께 선택 |
| mkdir | POST | /{parent}/mkdir | 선택 |
| rename | POST | /{path}/rename | 선택 |
| delete | DELETE | /{path}/delete | 선택 |
아래는 base URL이 https://storage.example.com인 예시입니다. 모든 요청의 <SIGNED_REQUEST_JWT>는 그 요청의 메서드·경로·본문을 서명한 새 JWT로 바꿉니다. 예시 본문은 코드 블록 마지막 줄바꿈을 포함하지 않으며, 표시한 Content-Length는 그 기준입니다. hello.txt의 hello는 원시 파일 바이트를 보여 주는 5바이트 예시입니다.
info - 문서 정보 조회
문서 또는 디렉터리의 메타데이터를 조회하는 필수 endpoint입니다. 문서 내용은 반환하지 않습니다.
요청
GET /tfo-storage/v1/{path}/info
GET /tfo-storage/v1/contracts/hello.txt/info HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
| 항목 | 형식 | 규격 |
|---|---|---|
path | 경로 · string | 파일 또는 디렉터리의 상대 경로. 루트는 /tfo-storage/v1/info |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 GET 요청의 JWT. 공통 헤더 참고 |
| 요청 본문 | 없음 | Content-Type 생략. JWT의 본문 길이는 0, 해시는 빈 본문의 SHA-256 |
응답
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 229
{"path":"contracts/hello.txt","name":"hello.txt","type":"file","size":5,"readable":true,"writable":true,"locked":false,"locker":null,"createdAt":"2026-09-14T00:00:00Z","modifiedAt":"2026-09-14T00:10:00Z","revision":"revision-17"}
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 200 OK | 대상 항목의 메타데이터 조회 성공 |
Content-Type | 필수 헤더 | application/json |
Content-Length | 필수 헤더 | 응답 JSON의 정확한 바이트 수. 최대 5,242,880 |
| 본문 | object | 메타데이터 필드를 직접 담은 항목 객체 하나. entry나 data로 감싸지 않음 |
필수 필드는 path, name, type, size, readable, writable, locked입니다. 선택 필드는 locker, createdAt, modifiedAt, revision이며 생략 또는 null을 허용합니다. 각 필드의 타입·길이·허용값은 공통 항목 스키마를 따릅니다. 응답 path는 요청 경로를 디코딩한 값과 같아야 합니다. 루트는 path: "", type: "directory", size: 0을 사용합니다.
| 상태 | 반환 조건과 처리 |
|---|---|
404 Not Found | 대상이 없음. Office는 항목이 없는 것으로 처리하며 가짜 메타데이터를 요구하지 않음 |
413 Payload Too Large | 파일 크기 또는 응답 JSON이 제한을 넘음 |
501 Not Implemented | 필수 endpoint이므로 기능 미지원으로 인정하지 않음 |
list - 하위 항목 조회
디렉터리의 바로 아래 파일과 디렉터리를 조회하는 선택 endpoint입니다. 재귀 검색이나 페이지네이션은 제공하지 않습니다.
요청
GET /tfo-storage/v1/{path}/list
GET /tfo-storage/v1/contracts/list HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
| 항목 | 형식 | 규격 |
|---|---|---|
path | 경로 · string | 조회할 디렉터리. 루트는 /tfo-storage/v1/list |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 GET 요청의 JWT. 공통 헤더 참고 |
| 요청 본문 | 없음 | Content-Type 생략. JWT의 본문 길이는 0, 해시는 빈 본문의 SHA-256 |
응답
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 243
{"entries":[{"path":"contracts/hello.txt","name":"hello.txt","type":"file","size":5,"readable":true,"writable":true,"locked":false,"locker":null,"createdAt":"2026-09-14T00:00:00Z","modifiedAt":"2026-09-14T00:10:00Z","revision":"revision-17"}]}
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 200 OK | 디렉터리 조회 성공. 빈 디렉터리도 성공 |
Content-Type | 필수 헤더 | application/json |
Content-Length | 필수 헤더 | 전체 JSON의 정확한 바이트 수. 최대 5,242,880 |
entries | 본문 · array · 필수 | 0~10,000개의 항목 객체. 최상위 객체의 유일한 필드 |
entries[] | object | info와 같은 필수·선택 필드. 디코딩된 path는 조회 디렉터리의 바로 아래 경로이며 중복 불가 |
항목의 경로는 항상 루트 기준입니다. 루트 목록에는 contracts, contracts의 목록에는 contracts/hello.txt처럼 반환합니다. 현재 디렉터리 자신, 상위 디렉터리, 하위 폴더 내부 항목을 섞지 않습니다. 순서는 규격에서 정하지 않으며 total, cursor, hasMore 같은 추가 필드를 보내지 않습니다.
빈 디렉터리는 200과 {"entries":[]}를 반환합니다. 이 본문의 길이는 14바이트입니다. 목록 기능을 구현하지 않았다면 인증 후 501과 LIST_NOT_SUPPORTED를 반환합니다. 빈 목록, 대상이 없는 404, 기능 미지원은 서로 다른 결과입니다.
| 상태 | 반환 조건과 처리 |
|---|---|
404 Not Found | 조회할 디렉터리가 없음 |
409 Conflict | 대상이 디렉터리가 아님(공개 예제) |
413 Payload Too Large | 항목 수·메타데이터 크기·포함된 파일 크기가 제한을 넘음. 일부 목록을 성공으로 반환하지 않음 |
501 Not Implemented | 인증 후 {"code":"LIST_NOT_SUPPORTED"}. Office에서 파일 목록을 사용할 수 없음 |
get - 문서 다운로드
파일의 전체 원본 바이트를 반환하는 필수 endpoint입니다. Office는 이 내용을 읽어 문서를 엽니다.
요청
GET /tfo-storage/v1/{path}/get
GET /tfo-storage/v1/contracts/hello.txt/get HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
| 항목 | 형식 | 규격 |
|---|---|---|
path | 경로 · string | 다운로드할 파일. 디렉터리의 목록은 list 사용 |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 GET 요청의 JWT. 공통 헤더 참고 |
| 요청 본문 | 없음 | Content-Type 생략. JWT의 본문 길이는 0, 해시는 빈 본문의 SHA-256 |
부분 다운로드용 Range 요청은 이 계약에 없습니다. 다운로드 URL이나 JSON·Base64 대신 파일 자체를 반환합니다.
응답
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 5
hello
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 200 OK | 파일 전체 다운로드 성공 |
Content-Type | 필수 헤더 | 파일 확장자와 관계없이 application/octet-stream |
Content-Length | 필수 헤더 | 실제 파일 바이트 수. 0~314,572,800 |
| 본문 | binary | Content-Length와 정확히 같은 길이의 원본 파일. JSON 필드 없음 |
빈 파일은 200과 Content-Length: 0으로 반환합니다. 압축·chunked·리디렉션·부분 응답으로 대체하지 않습니다. 스트리밍 중 파일이 바뀌어 헤더의 길이와 달라지지 않도록 저장소에서 일관된 내용을 읽어야 합니다.
| 상태 | 반환 조건과 처리 |
|---|---|
404 Not Found | 파일이 없음 |
409 Conflict | 대상이 파일이 아님(공개 예제) |
413 Payload Too Large | 파일이 허용 크기를 넘음. 성공 헤더나 파일 본문을 보내기 전에 거부 |
501 Not Implemented | 필수 endpoint이므로 기능 미지원으로 인정하지 않음 |
put - 문서 저장
편집이 끝난 전체 파일을 원시 바이트로 받습니다. 부분 업데이트나 multipart 업로드는 사용하지 않습니다. 모든 바이트를 임시 보관하고 인증·길이·해시·접근 권한을 검증한 뒤 대상 파일을 원자적으로 교체합니다. 실패하면 기존 파일을 보존합니다.
요청
PUT /tfo-storage/v1/{path}/put
PUT /tfo-storage/v1/contracts/hello.txt/put HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/octet-stream
Content-Length: 5
hello
| 항목 | 형식 | 규격 |
|---|---|---|
path | 경로 · string | 저장할 파일의 상대 경로. 빈 경로로 Provider 루트를 덮어쓸 수 없음 |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 PUT 요청의 JWT. 공통 헤더 참고 |
Content-Type | 필수 헤더 | 정확히 application/octet-stream |
Content-Length | 필수 헤더 | 전체 파일의 실제 바이트 수. 0~314,572,800 |
| 요청 본문 | binary | 파일 전체 바이트. JSON 객체·Base64·multipart 형식이 아님 |
request.arguments.save_type | JWT · string · 선택 | Office가 전달하는 저장 컨텍스트. 예: save. 별도 본문 필드나 쿼리로 전달하지 않음 |
save_type은 접근 권한이나 잠금 소유자를 증명하지 않습니다. 조건부 저장용 revision·If-Match·잠금 토큰 필드는 이 요청에 정의돼 있지 않습니다. 동시 저장을 허용하거나 충돌을 거부하는 정책은 Provider가 정합니다. 공개 예제는 부모 디렉터리가 있으면 파일을 생성하거나 교체하며, 부모 디렉터리는 자동으로 만들지 않습니다.
응답
저장소 반영을 완료한 뒤 저장된 문서의 식별자를 JSON 객체로 반환합니다.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 29
{"docId":"saved-document-id"}
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 200 OK | 본문 검증을 통과하는 다른 2xx도 허용. 204 No Content는 허용하지 않음 |
Content-Type | 필수 헤더 | UTF-8 application/json |
Content-Length | 필수 헤더 | 실제 JSON 본문 바이트 수. 고정 길이 규칙 적용 |
| 본문 | JSON object · 필수 | docId 하나만 포함. 추가·중복 필드, 뒤따르는 JSON 값이나 텍스트는 허용하지 않음 |
docId | string · 필수 | ASCII 영문·숫자로 시작하는 1~1,024자의 ASCII 영문·숫자·.·_·:·-. 대소문자와 관계없이 true·false는 예약값 |
docId는 실제 저장된 대상 문서를 식별합니다. 같은 어댑터의 같은 저장 경로는 내용이 바뀌어도 같은 ID를 유지하고, 다른 경로는 다른 ID를 사용합니다. 일반 저장과 다른 이름으로 저장 모두 이 규칙을 따릅니다. revision이나 원본 세션 ID로 대체하지 않습니다.
빈 본문, 맨몸 ID, XML, JSON 문자열·숫자·불리언(true 포함)·null·배열은 거부합니다. 값을 trim하거나 다른 형식으로 자동 변환하지 않습니다. 기계 판독용 계약은 PUT 응답 스키마를 참고합니다. Node.js·Spring Boot·FastAPI 예제는 디코딩한 루트 기준 상대 경로를 UTF-8로 인코딩한 뒤 SHA-256을 계산하고, 64자의 소문자 16진수 문자열을 ID로 반환합니다. 실제 Provider는 위 형식에 맞는 자체 문서 ID를 사용할 수 있습니다.
| 상태 | 반환 조건과 처리 |
|---|---|
400 Bad Request | 빈 문서 경로, 선언한 길이와 실제 본문 불일치 등 |
404 Not Found | 부모 디렉터리가 없음(공개 예제) |
409 Conflict | 파일 위치에 디렉터리가 있거나 저장소의 충돌 정책에 따라 저장을 거부함 |
413 Payload Too Large | 문서가 프로토콜 또는 Provider의 더 낮은 제한을 넘음 |
501 Not Implemented | 인증 후 {"code":"PUT_NOT_SUPPORTED"}. 저장소에 반영하지 않으며 Office에는 저장 실패로 표시 |
실패 시 기존 파일을 보존하고 임시 데이터를 정리합니다. 같은 경로에 다시 저장하면 내용이나 revision이 바뀌어도 같은 docId를 반환합니다.
lock - 문서 잠금
문서에 잠금 소유자를 기록하는 선택 endpoint입니다. 같은 소유자의 재요청은 성공하고, 다른 소유자가 잠근 상태면 충돌로 처리합니다. unlock과 함께 구현합니다.
요청
POST /tfo-storage/v1/{path}/lock
POST /tfo-storage/v1/contracts/hello.txt/lock HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 20
{"owner":"editor-1"}
| 항목 | 형식 | 규격 |
|---|---|---|
path | 경로 · string | 잠글 문서의 상대 경로 |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 POST 요청의 JWT. 공통 헤더 참고 |
Content-Type | 필수 헤더 | 정확히 application/json |
Content-Length | 필수 헤더 | JSON 본문의 UTF-8 바이트 수 |
owner | 본문 · string · 필수 | 비어 있지 않은 잠금 소유자 식별자. JSON의 유일한 필드 |
owner는 그대로 저장하고 해제 요청에서 동일한 값인지 비교합니다. 사용자 인증 토큰으로 사용하지 않습니다. info·list의 locker에 반환할 값은 해당 필드의 최대 255자 제한에 맞춰야 합니다. 잠금 만료 시각·임대 기간·갱신 주기는 이 요청의 필드로 정의하지 않습니다.
응답
HTTP/1.1 204 No Content
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 204 No Content | 새 잠금 획득 또는 같은 owner의 기존 잠금 확인. 다른 2xx도 성공 |
| 본문 | 없음 | 잠금 토큰이나 결과 JSON을 요구하지 않음. 다른 2xx의 본문도 성공 판단에 필요한 필드가 아님 |
| 상태 | 반환 조건과 처리 |
|---|---|
400 Bad Request | owner 생략·빈 문자열·잘못된 타입·추가 JSON 필드 |
404 Not Found | 잠글 대상이 없음(공개 예제) |
409 Conflict | 다른 owner가 잠근 상태. 기존 잠금을 바꾸지 않음 |
501 Not Implemented | 인증 후 {"code":"LOCK_NOT_SUPPORTED"}. Office는 실제 잠금 없이 성공으로 처리 |
같은 소유자로 다시 잠글 때도 JWT는 새로 생성합니다. 미지원이면 unlock도 미지원이어야 하며, 실제 잠금 충돌을 LOCK_NOT_SUPPORTED로 바꾸지 않습니다.
unlock - 잠금 해제
현재 소유자의 잠금을 해제하는 선택 endpoint입니다. 잠금이 이미 없으면 성공하고, 다른 소유자의 잠금을 해제하려 하면 충돌로 처리합니다. lock과 함께 구현합니다.
요청
POST /tfo-storage/v1/{path}/unlock
POST /tfo-storage/v1/contracts/hello.txt/unlock HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 20
{"owner":"editor-1"}
| 항목 | 형식 | 규격 |
|---|---|---|
path | 경로 · string | 잠금을 해제할 문서의 상대 경로 |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 POST 요청의 JWT. 공통 헤더 참고 |
Content-Type | 필수 헤더 | 정확히 application/json |
Content-Length | 필수 헤더 | JSON 본문의 UTF-8 바이트 수 |
owner | 본문 · string · 필수 | 잠글 때 사용한 소유자 식별자와 같은 값. JSON의 유일한 필드 |
owner를 생략하거나 다른 값으로 보내 강제 해제하는 기능은 없습니다.
응답
HTTP/1.1 204 No Content
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 204 No Content | 같은 소유자의 잠금 해제 또는 이미 잠금이 없는 상태. 다른 2xx도 성공 |
| 본문 | 없음 | 해제 결과 JSON을 요구하지 않음 |
| 상태 | 반환 조건과 처리 |
|---|---|
400 Bad Request | owner 생략·빈 문자열·잘못된 타입·추가 JSON 필드 |
409 Conflict | 현재 잠금 소유자가 다름. 기존 잠금을 유지 |
501 Not Implemented | 인증 후 {"code":"UNLOCK_NOT_SUPPORTED"}. Office는 실제 해제 작업 없이 성공으로 처리 |
이미 해제된 잠금에 대한 재요청도 새 JWT로 보냅니다. 미지원이면 lock도 미지원이어야 합니다.
mkdir - 디렉터리 생성
지정한 부모 디렉터리 바로 아래에 새 디렉터리 하나를 만드는 선택 endpoint입니다. 중간 디렉터리를 포함한 여러 단계의 경로를 생성하는 요청이 아닙니다.
요청
POST /tfo-storage/v1/{parent}/mkdir
POST /tfo-storage/v1/contracts/mkdir HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 18
{"name":"archive"}
| 항목 | 형식 | 규격 |
|---|---|---|
parent | 경로 · string | 기존 부모 디렉터리의 상대 경로. 루트 아래 생성은 /tfo-storage/v1/mkdir |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 POST 요청의 JWT. 공통 헤더 참고 |
Content-Type | 필수 헤더 | 정확히 application/json |
Content-Length | 필수 헤더 | JSON 본문의 UTF-8 바이트 수 |
name | 본문 · string · 필수 | 새 디렉터리 이름 하나. 1~255자이며 JSON의 유일한 필드 |
name은 URL 인코딩한 경로가 아닌 실제 이름입니다. /·\·제어 문자와 이름 전체가 . 또는 ..인 값은 허용하지 않습니다. 예를 들어 2026/reports를 넣어 하위 경로를 한꺼번에 만들 수 없습니다.
응답
HTTP/1.1 204 No Content
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 204 No Content | parent/name 디렉터리 생성 완료. 다른 2xx도 성공 |
| 본문 | 없음 | 생성한 항목 객체를 요구하지 않음. 이후 info 또는 list로 조회 가능 |
| 상태 | 반환 조건과 처리 |
|---|---|
400 Bad Request | name 생략·잘못된 타입·추가 필드·허용하지 않는 이름 |
404 Not Found | 부모 디렉터리가 없음(공개 예제) |
409 Conflict | 같은 이름의 항목이 있거나 부모가 디렉터리가 아님(공개 예제) |
501 Not Implemented | 인증 후 {"code":"MKDIR_NOT_SUPPORTED"}. 디렉터리를 생성하지 않음 |
공개 예제는 이미 만들어진 디렉터리에 같은 생성 요청을 보내도 409를 반환합니다. 응답이 유실됐다면 먼저 항목이 생성됐는지 확인합니다.
rename - 이름 변경
파일 또는 디렉터리의 이름을 같은 부모 디렉터리 안에서 바꾸는 선택 endpoint입니다. 다른 디렉터리로 이동하거나 기존 항목을 덮어쓰는 옵션은 정의하지 않습니다.
요청
POST /tfo-storage/v1/{path}/rename
POST /tfo-storage/v1/contracts/hello.txt/rename HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 23
{"name":"greeting.txt"}
| 항목 | 형식 | 규격 |
|---|---|---|
path | 경로 · string | 이름을 바꿀 기존 항목의 상대 경로. Provider 루트는 변경 불가 |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 POST 요청의 JWT. 공통 헤더 참고 |
Content-Type | 필수 헤더 | 정확히 application/json |
Content-Length | 필수 헤더 | JSON 본문의 UTF-8 바이트 수 |
name | 본문 · string · 필수 | 새 이름 하나. 1~255자이며 JSON의 유일한 필드 |
name에는 디코딩된 실제 이름을 넣고 mkdir의 이름 규칙을 적용합니다. contracts/hello.txt에 greeting.txt를 보내면 결과 경로는 contracts/greeting.txt입니다.
응답
HTTP/1.1 204 No Content
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 204 No Content | 같은 부모 안에서 이름 변경 완료. 다른 2xx도 성공 |
| 본문 | 없음 | 새 경로나 항목 객체를 요구하지 않음. 새 경로의 info 또는 부모의 list로 조회 가능 |
| 상태 | 반환 조건과 처리 |
|---|---|
400 Bad Request | 잘못된 name, 추가 JSON 필드 또는 Provider 루트 변경 시도 |
404 Not Found | 원래 경로의 항목이 없음 |
409 Conflict | 새 이름이 이미 사용 중. 공개 예제는 잠긴 항목의 이름 변경도 거부 |
501 Not Implemented | 인증 후 {"code":"RENAME_NOT_SUPPORTED"}. 원래 이름을 유지 |
이름 변경 후 원래 경로로 재요청하면 대상이 없을 수 있습니다. 응답이 유실됐다면 원래 경로와 새 경로의 상태를 확인합니다.
delete - 항목 삭제
파일 또는 디렉터리를 삭제하는 선택 endpoint입니다. Provider 루트 삭제는 거부합니다. 비어 있지 않은 디렉터리를 거부할지 재귀 삭제할지는 Provider가 명시적으로 정해야 하며, 공개 예제는 409로 거부합니다.
요청
DELETE /tfo-storage/v1/{path}/delete
DELETE /tfo-storage/v1/contracts/greeting.txt/delete HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Adapter: customer-storage-a
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
| 항목 | 형식 | 규격 |
|---|---|---|
path | 경로 · string | 삭제할 항목의 상대 경로. Provider 루트는 삭제 불가 |
| 인증 헤더 | 필수 · string | 어댑터 이름과 이 DELETE 요청의 JWT. 공통 헤더 참고 |
| 요청 본문 | 없음 | Content-Type 생략. JWT의 본문 길이는 0, 해시는 빈 본문의 SHA-256 |
recursive, force 같은 쿼리·본문 옵션은 없습니다.
응답
HTTP/1.1 204 No Content
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 204 No Content | 항목 삭제 완료. 다른 2xx도 성공 |
| 본문 | 없음 | 삭제한 항목 객체나 삭제 개수를 요구하지 않음 |
| 상태 | 반환 조건과 처리 |
|---|---|
400 Bad Request | Provider 루트 삭제 시도 |
404 Not Found | 삭제할 항목이 없음. 이미 삭제한 항목의 재요청도 여기에 해당(공개 예제) |
409 Conflict | 비어 있지 않은 디렉터리 또는 잠긴 항목 삭제 거부(공개 예제) |
501 Not Implemented | 인증 후 {"code":"DELETE_NOT_SUPPORTED"}. 항목을 삭제하지 않음 |
기능 미지원 응답
선택적 작업을 구현하지 않았을 때만 아래 응답을 사용합니다. 해당 요청의 인증·본문 검증·재전송 검사를 마친 뒤 반환해야 합니다. 상태는 501, 미디어 유형은 application/json, JSON 본문은 현재 작업과 일치하는 code 하나만 포함합니다. charset 매개변수는 허용합니다.
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | 필수 | 정확히 501 Not Implemented |
Content-Type | 필수 헤더 | application/json. charset 매개변수 허용 |
code | 본문 · string · 필수 | 아래 표의 현재 작업에 해당하는 정확한 값. 다른 필드 추가 불가 |
| 본문 길이 | 짧은 UTF-8 JSON | Office의 오류 본문 읽기 한도 8 KiB 안에 전체 객체가 들어가야 함 |
성공한 읽기 응답의 고정 길이 검사와는 별개이지만, 아래 예시처럼 정확한 Content-Length를 함께 반환할 수 있습니다.
HTTP/1.1 501 Not Implemented
Content-Type: application/json
Content-Length: 29
{"code":"LIST_NOT_SUPPORTED"}
| 작업 | 정확한 code | Office 동작 |
|---|---|---|
list | LIST_NOT_SUPPORTED | 목록 기능 사용 불가. 등록 시 안내를 확인하고 연결을 저장할 수 있음 |
put | PUT_NOT_SUPPORTED | 저장 실패 |
lock | LOCK_NOT_SUPPORTED | 실제 잠금 없이 성공으로 처리 |
unlock | UNLOCK_NOT_SUPPORTED | 실제 잠금 해제 없이 성공으로 처리 |
mkdir | MKDIR_NOT_SUPPORTED | 디렉터리 생성 불가 |
rename | RENAME_NOT_SUPPORTED | 이름 변경 불가 |
delete | DELETE_NOT_SUPPORTED | 삭제 불가 |
info와 get은 필수이므로 모든 501을 작업 실패로 처리합니다. 다른 작업에서도 빈 본문, HTML, 추가 JSON 필드, 다른 작업의 code, 404나 다른 5xx는 미지원 선언이 아닙니다. 예를 들어 {"code":"LIST_NOT_SUPPORTED","message":"..."} 또는 list에 대한 DELETE_NOT_SUPPORTED는 잘못된 응답입니다.
Office가 성공으로 처리하는 잠금 응답은 정확한 LOCK_NOT_SUPPORTED와 UNLOCK_NOT_SUPPORTED 쌍뿐입니다. 둘 중 하나만 미지원으로 선언하면 안 됩니다. 실제 잠금이 없으므로 동시 저장 결과는 저장소의 마지막 쓰기 또는 충돌 정책을 따릅니다. 구현된 잠금의 409, 인증 실패, 네트워크 오류와 저장소 장애는 실제 실패로 유지됩니다.
관리자 연결 테스트는 list만 호출합니다. 나머지 기능은 실제 문서 작업에서 확인하며 등록 과정에서 쓰기·삭제 작업을 임의로 실행하지 않습니다. 기능 미지원 JSON schema도 함께 참고합니다.
오류 응답
info의 404는 대상 없음으로 처리합니다. 이 경우와 위의 정확한 기능 미지원 응답을 제외하면 2xx가 아닌 응답은 Office 작업 실패입니다. 일반 오류 본문에는 고정된 JSON 스키마가 없습니다. 짧은 오류 설명을 반환하되 시크릿, JWT, 내부 경로, 문서 내용, 스택 추적을 포함하지 않습니다. Office에 오류 본문의 최대 8 KiB가 표시될 수 있습니다.
| 항목 | 형식 | 규격 |
|---|---|---|
| 상태 | HTTP 상태 코드 | 아래 오류 원인에 맞게 선택. 상태가 성공 여부를 결정 |
Content-Type | 본문에 맞게 지정 | 일반 오류는 고정 미디어 유형이 없음. 공개 예제는 text/plain; charset=utf-8 |
| 본문 | text 또는 Provider가 정의한 JSON | 선택 사항. 공개 예제의 설명 문자열도 프로토콜의 고정 오류 식별자가 아님 |
인증 오류 응답 예시는 다음과 같습니다. 알 수 없는 어댑터·서명 불일치·만료·재사용을 상세 문구로 구별하지 않습니다.
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Content-Length: 12
Unauthorized
| 상태 | 사용 조건 |
|---|---|
400 Bad Request | 잘못된 경로·JSON·본문 길이 또는 허용하지 않는 쿼리 |
401 Unauthorized | 알 수 없는 어댑터, 잘못된 서명·claim, 만료되거나 재사용된 JWT |
403 Forbidden | 저장소 또는 문서 접근 권한 없음 |
404 Not Found | 대상 항목이나 요청 경로 없음 |
405 Method Not Allowed | 알려진 작업 경로에 잘못된 메서드 사용(공개 예제) |
409 Conflict | 이름·타입·잠금 소유자 충돌, 비어 있지 않은 디렉터리 삭제 거부 |
411 Length Required | 본문에 필요한 Content-Length 누락 |
413 Payload Too Large | 요청 본문, 문서, 목록 크기 또는 Provider가 설정한 제한 초과 |
415 Unsupported Media Type | 요청 Content-Type이 작업의 규격과 다름 |
500 Internal Server Error | Provider가 처리하지 못한 내부 오류(공개 예제). 세부 예외는 응답에 노출하지 않음 |
501 Not Implemented | 위의 정확한 선택적 작업 미지원 선언 |
503 Service Unavailable | 일시적인 저장소 장애 |
인증 오류를 기능 미지원 응답으로 바꾸거나 저장소 장애를 성공으로 응답하지 않습니다. put 실패 시 기존 파일을 보존하고 임시 파일을 정리합니다. 크기를 초과한 요청이나 응답을 chunked 전송으로 바꿔 재시도하지 않습니다.
공개 예제는 JSON 작업의 요청 본문에 별도로 16 KiB 제한을 둡니다. 이는 info·list 응답의 5 MiB 제한과 다른 경계입니다. 배포별 제한은 예제 설정과 실행 방법에서 확인합니다.