본문으로 건너뛰기

HTTP Storage Protocol

HTTP Storage Protocol v1은 Office의 HTTP Storage 어댑터와 스토리지 서비스를 직접 운영하는 HTTP Storage Provider 사이의 요청·응답 규격입니다. 고객은 Provider의 endpoint를 구현하고, Office는 이 endpoint로 요청을 보냅니다. Office에 연결하는 방법과 예제는 HTTP Storage에서 안내합니다.

이 문서는 9개 endpoint의 메서드·경로, 요청 헤더와 본문, 응답 필드와 상태 코드를 정의합니다. 공통 인증·경로·크기 제한은 모든 endpoint에 적용하며, 각 작업에서 추가로 필요한 조건은 해당 절에서 설명합니다.

문서 목록 조회부터 닫기까지​

먼저 고객 애플리케이션에서 문서 열기 URL을 만드는 코드를 준비합니다. 아래 예시는 http-remote-directory 어댑터에 list·put·lock·unlock을 지원하는 Provider가 연결되어 있다고 가정합니다.

  1. Office 주소와 등록한 어댑터 이름을 정합니다. /open은 Provider가 아니라 Office 주소로 요청합니다.
  2. 목록에서 선택한 문서의 상대 경로를 URL 경로에 넣고, app, docId, user_id를 설정합니다.
  3. 고객 서비스에서 사용자와 문서 접근 권한을 확인한 뒤 발급한 userSessionToken을 메타데이터에 넣습니다. JSON 문자열로 바꿔 __tfo_adapter_http_storage_client_metadata에 설정하며, URL 인코딩은 URLSearchParams가 한 번 수행합니다.
function createOpenUrl(documentPath, docId, userSessionToken) {
const encodedPath = documentPath.split("/").map(encodeURIComponent).join("/");
const url = new URL(
`/cloud-office/api/http-remote-directory/${encodedPath}/open`,
"https://office.example.com",
);
url.searchParams.set("app", "WORD_EDITOR");
url.searchParams.set("docId", docId);
url.searchParams.set("user_id", "example-user");
url.searchParams.set("__tfo_adapter_http_storage_client_metadata", JSON.stringify({
project: "example-project",
userSessionToken,
}));
return url.toString();
}

const openUrl = createOpenUrl(
"contracts/sample.docx",
"sample01",
"example-short-lived-document-session-token",
);

위 토큰은 설명용 값입니다. 실제로는 문서를 선택한 시점에 고객 서비스가 발급한 값을 사용합니다. 완성된 openUrl을 브라우저에서 열거나 Office를 표시할 iframe의 src에 설정합니다. JSON 문자열과 인코딩된 매개변수, 완성 URL은 고객 메타데이터 예제에서 확인할 수 있습니다.

아래 다이어그램은 목록 조회 → 문서 선택 → URL을 만들어 문서 열기 → 편집 → 저장 → 닫기 순서로 진행하는 예시입니다. list는 문서 선택 화면의 목록 조회이고, info 이후는 선택한 문서의 /open 처리와 후속 작업입니다. 목록 조회에도 Provider가 요구하는 고객 세션과 목록 조회 권한이 필요합니다.

TFO와 HTTP Storage Adapter는 Office 프레임워크에서 기본 제공하므로 고객이 구현할 필요가 없습니다. 스토리지 연동에서 고객이 구현·운영해야 하는 부분은 HTTP Storage Provider입니다. 고객 애플리케이션은 위와 같이 문서 열기 URL을 만들고 고객 세션을 전달합니다.

Office의 Adapter는 문서 경로·요청 정보와 request.client_metadata를 담은 JWT에 서명하고, Provider로 보내는 각 HTTP 요청의 X-TFO-Storage-Request-JWT 헤더에 넣습니다. 문서 파일은 HTTP 본문으로 전송합니다.

HTTP Storage Provider 구현자는 HTTP 요청을 받을 때마다 JWT 서명을 검증하고, JWT에 기록된 요청 정보가 실제 요청과 일치하는지 확인해야 합니다. 또한 고객 세션과 해당 문서·작업에 대한 현재 접근 권한을 직접 확인해야 합니다. 검증이나 권한 확인에 실패하면 저장소 작업을 수행하지 않고 요청을 거부해야 합니다. 세부 검증 항목은 JWT 서명과 검증을 따릅니다.

Office 기본 제공 영역·JWT 헤더·고객 Provider의 구현 책임을 구분한 문서 처리 시퀀스Office 내장 HTTP Storage Adapter가 서명된 요청을 고객 Provider로 보냅니다. Provider는 요청과 고객의 문서 권한을 확인한 뒤 문서를 읽거나 저장합니다.목록 조회부터 편집·저장·닫기까지Office 프레임워크 · 기본 제공 (고객 구현 불필요)고객이 구현·운영하는 부분TFOHTTP Storage AdapterHTTP Storage ProviderAdapter → Provider: info HTTP 요청 예시 · 요청마다 새 JWT 사용GET /tfo-http-storage/v1/contracts/sample.docx/info HTTP/1.1Host: storage.example.comX-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>JWT 내부: request.path · request.client_metadata { project, userSessionToken }1. 문서 선택 화면에서 목록 조회list()GET …/list + JWT200 · entries문서 목록 표시2. 고객 애플리케이션에서 문서를 선택하고 /open URL을 만들어 열기/open에 넣은 고객 메타데이터 → JWT의 request.client_metadatainfo()GET …/info + JWT200 · file metadata문서 정보 확인문서가 없거나 인증·권한 확인에 실패하면 열기 중단아래는 검증에 성공해 편집을 계속하는 흐름입니다.lock()POST …/lock + JWT · owner204 · lockedlock 결과 확인get()GET …/get + JWT200 · document bytes편집기에 문서 표시3. 문서 편집 · 저장 선택put()PUT …/put + JWT · file bytes200 · { docId }저장 완료 확인4. 공동 편집의 마지막 사용자가 저장을 마치고 문서 닫기unlock()POST …/unlock + JWT · owner204 · unlockedunlock 결과 확인Provider는 매 요청의 JWT 서명·유효 기간·재사용 여부를 검사합니다.JWT의 요청 정보와 수신한 요청을 비교하고, 고객 세션과 문서·작업 권한을 확인합니다.검증에 실패하거나 권한이 없으면 저장소에 접근하지 않고 요청을 거부합니다.문서가 닫힌 뒤 Provider에 별도의 close 또는 stop 요청을 보내지는 않습니다.각 endpoint는 이전 요청의 처리 state에 의존하지 않고 독립적으로 완료합니다(stateless).

각 endpoint는 이전 요청에서 만든 임시 처리 state에 의존하지 않고, 요청 하나로 처리를 완료하도록 구현해야 합니다(stateless). 예를 들어 info에서 만든 state를 보관해 두었다가 get에서 이어받아 사용하도록 구현하면 안 됩니다. get은 이전 info의 처리 state 없이도 전달받은 요청을 검증하고 문서를 읽을 수 있어야 합니다.

lock·unlock은 문서의 lock 상태와 owner를 공유해야 하는 작업 쌍입니다. 이 정보와 JWT 재사용 방지 기록은 유지해야 하며, info에서 get으로 이어 쓰는 임시 처리 state와 구분합니다.

문서가 닫혀도 Provider에 별도의 close 또는 stop 요청은 전달되지 않습니다. 문서를 닫는 과정에서 발생하는 자동 저장은 put 요청으로 처리합니다. 이때 전달되는 save_type: close의 의미는 저장 유형에서 설명합니다.

요청 순서와 횟수는 편집기 상태와 공동 편집 여부에 따라 달라질 수 있습니다. 편집 중에는 저장이 여러 번 발생할 수 있습니다. 위 그림은 공동 편집에 참여한 마지막 사용자가 저장을 마치고 문서를 닫는 경우입니다. lock·unlock을 지원하지 않을 때의 처리는 기능 미지원 응답을 참고합니다.

URL과 경로​

{PROVIDER_BASE_URL}/tfo-http-storage/v1/{ENCODED_DOCUMENT_PATH}/{OPERATION}

Provider base URL은 HTTP 또는 HTTPS 주소이며 경로 접두사를 포함할 수 있습니다. 사용자 정보, 쿼리 문자열, fragment, .·.. 경로는 허용하지 않습니다. 끝의 /는 제거한 뒤 프로토콜 경로를 붙입니다.

문서 경로는 Provider의 저장소 루트를 기준으로 지정합니다. /로 구분한 각 폴더명·파일명을 UTF-8로 퍼센트 인코딩한 뒤 다시 /로 연결합니다. 루트를 조회할 때는 문서 경로를 비워 두고 /tfo-http-storage/v1/list처럼 요청합니다. 그 외에는 폴더명·파일명을 비워 두거나 /를 연달아 넣지 않습니다.

항목예시
Provider base URLhttps://storage.example.com/office
문서 경로contracts/Proposal 2026.docx
문서 정보 요청GET /office/tfo-http-storage/v1/contracts/Proposal%202026.docx/info
루트 목록 요청GET /office/tfo-http-storage/v1/list

Provider는 디코딩한 경로를 자신의 저장소 루트 아래에서만 처리합니다. .·.., 경로 세그먼트 안의 /·\, 제어 문자, 잘못된 UTF-8 인코딩과 심볼릭 링크를 통한 루트 이탈을 거부합니다. 쿼리 매개변수, 쿠키, 리디렉션, 임의의 인증 헤더 전달은 사용하지 않습니다. 프록시를 거치더라도 JWT 검증에 사용할 URL 경로의 인코딩을 바꾸면 안 됩니다.

공통 요청 헤더​

헤더적용 범위값
X-TFO-Storage-Request-JWT모든 요청, 필수아래 규칙으로 서명한 JWT 원문. Bearer 접두사를 붙이지 않음
Content-Typeput, lock, unlock, mkdir, renameput은 application/octet-stream, JSON 작업은 application/json
Content-Length본문이 있는 요청, 필수정확한 본문 바이트 수를 나타내는 10진수 정수 하나

info, list, get, delete 요청에는 본문과 Content-Type이 없습니다. 서명에는 본문 길이 0과 빈 본문의 SHA-256을 넣습니다. 본문은 multipart나 base64로 감싸지 않으며 chunked 업로드와 압축을 사용하지 않습니다. JSON 본문은 UTF-8로 변환해 전송하며, 그 바이트의 길이와 SHA-256을 JWT에 넣어 서명합니다.

JWT 서명과 검증​

Office는 공유 시크릿으로 요청 정보를 담은 JWT에 서명합니다. Provider는 문서를 읽거나 변경하기 전에 같은 시크릿으로 JWT 서명을 검증하고, JWT에 기록된 메서드·경로·본문 정보가 수신한 요청과 일치하는지 확인해야 합니다. 이를 통해 전송 중 요청이 변조되었는지 확인할 수 있습니다. JWT의 유효 기간과 요청별 고유 ID도 검사해 만료되거나 이미 사용한 JWT를 거부합니다.

서명은 문서 내용을 암호화하지 않습니다. 전송 내용을 보호하려면 HTTPS를 사용합니다. 사용자 인증과 문서 접근 권한은 고객 서비스의 세션·권한 정보로 별도로 확인해야 합니다.

Office와 Provider는 어댑터별로 UTF-8 기준 최소 32바이트의 무작위 시크릿을 공유합니다. HMAC 키는 이 문자열의 UTF-8 바이트입니다. 시크릿을 요청 URL이나 본문으로 전달하지 않습니다.

JWT는 HS256으로 서명한 compact JWS 형식이며, 헤더는 다음과 같습니다.

{
"alg": "HS256",
"typ": "tfo-storage-request+jwt"
}

다음은 contracts/sample.docx를 저장하는 요청의 payload 예시입니다. 파일 크기와 SHA-256은 예시 값이므로 전송할 파일에서 계산한 값으로 바꿉니다. iat, exp, jti는 요청마다 새로 생성합니다.

{
"iss": "thinkfree-office",
"aud": "tfo-http-storage-provider",
"iat": 1789344000,
"exp": 1789344060,
"jti": "26bc9b5e-9dab-4a78-95d8-b1ae34a5d9eb",
"request": {
"adapter": "http-remote-directory",
"method": "PUT",
"path": "/tfo-http-storage/v1/contracts/sample.docx/put",
"content_type": "application/octet-stream",
"content_length": 231958,
"content_sha256": "754464a613ba4d87d1535f69a9b1e3a0a8f84b8277d2bf4d0c74aacceceb23d7",
"office_connection_id": "example-connection",
"arguments": {
"save_type": "save"
},
"client_metadata": {
"project": "example-project",
"userSessionToken": "example-short-lived-document-session-token"
}
}
}
필드타입필수검증 규칙
헤더 algstring예정확히 HS256
헤더 typstring예정확히 tfo-storage-request+jwt
issstring예정확히 thinkfree-office
audstring 또는 원소 1개의 배열예tfo-http-storage-provider 또는 이 값만 담은 배열
iatinteger예발급 Unix 시각(초). 현재 시각보다 미래가 아님
expinteger예만료 Unix 시각(초). 현재 시각보다 뒤이며 0 < exp - iat <= 60
jtistring예요청마다 고유한 ID
requestobject예요청 정보를 담는 객체. 서명으로 보호됨
request.adapterstring예등록한 어댑터 이름. ASCII 영문·숫자로 시작하는 1~128자의 영문·숫자·.·_·-. 서명 검증에 사용한 시크릿에 연결된 어댑터 이름과 일치해야 함
request.methodstring예수신한 요청의 HTTP 메서드와 일치. 대문자로 표시
request.pathstring예Provider base URL의 경로 접두사를 포함하며, 수신한 URL 경로의 인코딩을 그대로 유지. scheme·host·query 제외
request.content_typestring헤더가 있을 때실제 Content-Type과 정확히 일치
request.content_lengthinteger예실제 수신한 본문의 바이트 수. 본문이 없으면 0
request.content_sha256string예실제 본문 바이트의 SHA-256, 소문자 16진수 64자리
request.office_connection_idstring아니요Office 연결을 구분하는 값. 내부 구조를 해석하거나 사용자 인증 근거로 사용하지 않음
request.argumentsobject아니요save_type처럼 작업에 필요한 부가 정보. 접근 권한을 부여하지 않음
request.client_metadataobject아니요고객 서비스가 정한 JSON 객체. Provider가 서명 검증 후 사용자 인증과 권한 확인에 활용할 수 있음

전체 JWT는 최대 8,192바이트입니다. client_metadata의 입력 방법과 제한은 고객 메타데이터를 참고합니다.

검증 순서​

  1. 요청 메서드·경로·헤더·본문 크기가 허용 범위인지 확인합니다.
  2. request.adapter로 미리 등록된 시크릿을 선택하고 원본 JWT의 서명·발급자·대상·유효 기간을 검증합니다. 검증 전 어댑터 이름은 시크릿 선택에만 사용합니다.
  3. JWT의 요청 정보가 수신한 요청과 일치하는지 확인합니다. 본문 길이와 SHA-256은 수신한 바이트로 계산합니다.
  4. jti의 중복 확인과 기록을 하나의 원자적 작업으로 처리하고, exp까지 같은 JWT의 재사용을 거부합니다. Provider 인스턴스가 여러 개라면 이 기록을 공유합니다.
  5. 문서 접근 권한을 확인한 뒤 작업을 실행하거나 기능 미지원 응답을 반환합니다.

검증에 실패한 요청은 저장소에 접근하기 전에 거부합니다. Office와 Provider의 서버 시각을 동기화해야 합니다. Office는 재요청할 때도 iat·exp·jti를 새로 생성합니다. jti는 작업 중복 실행을 막는 멱등 키가 아니므로, 응답이 유실되면 저장소 상태를 먼저 확인합니다.

요청 인증과 고객 문서 권한 확인 순서Office 내장 HTTP Storage Adapter가 서명된 요청을 고객 Provider로 보냅니다. Provider는 요청과 고객의 문서 권한을 확인한 뒤 문서를 읽거나 저장합니다.Provider의 요청 처리 순서1. request.adapter로 미리 등록한 서명 키 선택검증 전이므로 키 선택에만 사용2. JWT 검증 후 수신한 요청과 비교서명·발급자·대상·유효 기간·요청 정보·재사용 여부 확인3. 고객 세션과 문서 접근 권한 확인client_metadata의 토큰으로 사용자와 해당 작업의 권한 확인4. 허용된 저장소 작업 수행info / get / put 등 요청 결과 반환실패·거부실패 응답문서 접근 없음요청의 진위 확인과 고객 권한 확인은 별개이며, 둘 다 통과해야 합니다.

고객 메타데이터​

문서를 열 때 고객 서비스의 정보를 전달하려면 Office /open의 __tfo_adapter_http_storage_client_metadata 매개변수에 JSON 문자열을 넣습니다. 쿼리 문자열을 사용할 때는 URL 인코딩을 한 번 적용합니다. 서명된 문서 열기 티켓에서는 command.params의 같은 이름에 JSON 문자열을 넣습니다. 서명되지 않은 쿼리 매개변수로 서명된 값을 덮어쓸 수 없습니다. 이 값은 Provider endpoint의 쿼리 매개변수가 아닙니다.

Provider가 받는 요청 JWT의 request.client_metadata에는 JSON 문자열을 파싱한 객체가 들어갑니다.

예를 들어 http-remote-directory 어댑터의 contracts/sample.docx를 열 때 다음과 같이 URL을 만듭니다. Office 주소와 문서 경로를 자신의 환경으로 바꾸고, 토큰에는 고객 서비스가 발급한 실제 값을 사용합니다. 아래 토큰은 설명용 값입니다.

const metadata = {
project: "example-project",
userSessionToken: "example-short-lived-document-session-token",
};
const openUrl = new URL(
"/cloud-office/api/http-remote-directory/contracts/sample.docx/open",
"https://office.example.com",
);
openUrl.searchParams.set("app", "WORD_EDITOR");
openUrl.searchParams.set("docId", "sample01");
openUrl.searchParams.set("user_id", "example-user");
openUrl.searchParams.set(
"__tfo_adapter_http_storage_client_metadata",
JSON.stringify(metadata),
);
console.log(openUrl.toString());

고객 애플리케이션에서 JSON.stringify(metadata)로 만든 JSON 문자열은 다음과 같습니다.

{"project":"example-project","userSessionToken":"example-short-lived-document-session-token"}

URLSearchParams가 한 번 URL 인코딩하면 /open에 다음 매개변수가 붙습니다. 이 값을 다시 인코딩하지 않습니다.

__tfo_adapter_http_storage_client_metadata=%7B%22project%22%3A%22example-project%22%2C%22userSessionToken%22%3A%22example-short-lived-document-session-token%22%7D

완성된 문서 열기 URL은 다음과 같습니다. Provider에는 이 URL의 쿼리가 아니라 JWT 안의 request.client_metadata 객체가 전달됩니다.

https://office.example.com/cloud-office/api/http-remote-directory/contracts/sample.docx/open?app=WORD_EDITOR&docId=sample01&user_id=example-user&__tfo_adapter_http_storage_client_metadata=%7B%22project%22%3A%22example-project%22%2C%22userSessionToken%22%3A%22example-short-lived-document-session-token%22%7D
항목필수제한
메타데이터 입력선택. Provider 접근 정책에 따라 필수일 수 있음JSON 객체를 문자열로 전달. 원본 입력에만 UTF-8 기준 최대 4,096바이트 적용
객체 키각 필드에 필수1~64 UTF-16 코드 단위. Java String.isBlank() 기준 공백만 있는 키는 불가
문자열 값있을 때최대 512 UTF-16 코드 단위
중첩 깊이예루트 객체는 0. 객체 필드 값이나 배열 원소마다 1 증가하며 최대 8

Provider도 수신한 메타데이터가 위 객체 구조 제한을 따르는지 확인합니다.

위 예시의 userSessionToken처럼 고객 서비스가 문서 접근용으로 발급한 토큰을 넣으면 Provider의 사용자 인증에 활용할 수 있습니다. Provider는 JWT 검증 후 이 토큰을 고객 서비스의 세션 정보와 대조해 사용자·만료 시각·대상 문서·허용 작업을 확인합니다. userSessionToken은 예시 필드명이며, 토큰 발급과 확인은 고객 서비스에서 구현합니다.

권한은 요청마다 확인하며, info·get에는 읽기 권한, put에는 쓰기 권한이 필요합니다. 토큰이 없거나 만료·폐기됐거나 작업 권한이 부족하면 해당 토큰이 필요한 요청을 거부합니다.

메타데이터는 암호화되지 않으며 문서 열기 URL·연결 상태·요청 JWT에 나타날 수 있습니다. 로그인 쿠키·refresh token·공유 서명 시크릿·장기 자격 증명·문서 내용을 넣지 않습니다. 접근 권한을 증명하는 값을 전달한다면 대상 문서·허용 작업·유효 기간을 제한한 별도 값을 발급하고 로그에서는 가립니다.

응답 형식과 크기 제한​

성공한 info, list, get, put 응답에는 정확한 10진수 Content-Length 헤더가 하나 있어야 합니다. 값은 실제 전송한 바이트 수와 같아야 하며, Transfer-Encoding과 Content-Encoding은 허용하지 않습니다. Content-Type의 charset 매개변수는 허용하지만 JSON 본문은 UTF-8이어야 합니다.

대상Content-Type제한
info, list, put 성공 응답application/json각각 5 MiB (5,242,880바이트)
get 성공 응답application/octet-stream300 MiB (314,572,800바이트)
put 요청application/octet-stream300 MiB (314,572,800바이트)
메타데이터의 파일 sizeJSON integer0~314,572,800
list의 entriesJSON array최대 10,000개이며 전체 JSON도 5 MiB 이하

표의 최댓값은 허용 범위에 포함됩니다. Provider는 더 낮은 문서 크기 제한을 둘 수 있습니다.

Office는 헤더·크기·실제 본문 길이가 위 조건과 맞지 않으면 해당 작업을 실패로 처리합니다. 기능 미지원 응답과 일반 오류 응답은 아래 각 절을 따릅니다.

성공 상태와 본문​

작업정상 응답본문 규격
info, list200 OKJSON 객체가 필수. 빈 본문이나 204로 대체하지 않음
get200 OK파일 전체 바이트. 빈 파일은 Content-Length: 0으로 반환
put200 OKdocId를 포함하는 JSON 객체가 필수. PUT 응답 참고
lock, unlock, mkdir, rename, delete204 No Content본문 없음

Office는 다른 2xx 상태도 성공으로 분류하지만, info·list·get·put은 응답 본문 검증도 통과해야 합니다. 나머지 작업은 결과 필드를 요구하지 않으므로 본문 없는 204를 사용합니다. 결과 텍스트가 필요하다면 200과 text/plain; charset=utf-8로 최대 10 MiB까지 반환할 수 있습니다.

메타데이터 필드​

info는 항목 객체 하나, list는 entries 배열을 가진 객체 하나를 반환합니다. 항목 객체에 아래 이외의 필드를 넣지 않습니다.

필드타입필수의미와 제한
pathstring예저장소 루트를 기준으로 한 상대 경로. URL 인코딩하지 않은 값, 최대 4,096자. 루트는 빈 문자열
namestring예마지막 경로 세그먼트와 동일한 1~255자 이름. 루트는 고정된 표시 이름
typestring예file 또는 directory
sizeinteger예파일 바이트 수, 0~314,572,800. 디렉터리는 반드시 0
readableboolean예문서를 읽을 수 있는지 여부
writableboolean예문서를 쓸 수 있는지 여부
lockedboolean예문서의 lock 여부
lockerstring 또는 null아니요lock의 owner, 최대 255자
createdAtstring 또는 null아니요RFC 3339 생성 시각, 최대 64자
modifiedAtstring 또는 null아니요RFC 3339 수정 시각, 최대 64자
revisionstring 또는 null아니요저장소 revision 또는 ETag에 해당하는 값, 최대 1,024자

info.path는 요청한 문서 경로와 같아야 합니다. list는 요청한 디렉터리의 바로 아래 항목만 포함하며 중복 경로와 페이지네이션을 지원하지 않습니다.

필수 필드는 null을 허용하지 않습니다. 선택 필드는 생략하거나 null로 반환할 수 있습니다. path는 URL 인코딩한 문자열이 아니라 디코딩된 경로이며, createdAt·modifiedAt은 2026-09-14T00:00:00Z처럼 시간대가 포함된 시각입니다. readable·writable은 항목의 접근 가능 여부를 알리는 값이며, Provider는 각 요청을 처리할 때 권한을 다시 확인해야 합니다. revision은 저장소의 버전 정보이며 이 프로토콜의 조건부 요청 헤더나 lock 토큰으로 자동 사용되지 않습니다.

JSON 응답 형식을 검사할 때는 entry schema, info response schema, list response schema를 사용할 수 있습니다. 스키마 검사만으로는 요청 경로와 응답 경로의 관계를 모두 확인할 수 없으므로, 위 경로 조건과 크기 제한도 검사해야 합니다.

작업별 요청·응답​

path에는 대상 항목의 상대 경로를, parent에는 새 디렉터리를 만들 부모 디렉터리의 상대 경로를 넣습니다. 둘 다 Provider의 저장소 루트를 기준으로 합니다. 경로 전체가 아니라 각 세그먼트를 URL 규칙에 따라 인코딩합니다. 모든 요청에는 공통 인증 헤더인 X-TFO-Storage-Request-JWT가 필수이며, 쿼리 매개변수는 없습니다.

lock·unlock·mkdir·rename의 요청 본문은 각 작업에 명시한 필드 하나만 담은 JSON 객체입니다. 필수 필드의 생략·null·다른 타입·추가 필드를 허용하지 않습니다. JSON 요청의 Content-Type은 정확히 application/json이며, Content-Length는 UTF-8 직렬화 결과의 바이트 수입니다.

각 작업의 오류 표에는 해당 작업에서 확인할 조건을 정리했습니다. 공통 오류에 나오는 경로·인증·권한·본문 형식·저장소 장애도 모든 endpoint에서 처리해야 합니다.

작업메서드경로 접미사구현
infoGET/{path}/info필수
listGET/{path}/list선택
getGET/{path}/get필수
putPUT/{path}/put선택
lockPOST/{path}/lockunlock과 함께 구현하거나 둘 다 미지원
unlockPOST/{path}/unlocklock과 함께 구현하거나 둘 다 미지원
mkdirPOST/{parent}/mkdir선택
renamePOST/{path}/rename선택
deleteDELETE/{path}/delete선택

아래 예시는 base URL이 https://storage.example.com인 Provider에서 contracts/sample.docx를 조회·저장하고, reviewed.docx로 이름을 바꾼 뒤 삭제하는 요청입니다. 모든 요청의 <SIGNED_REQUEST_JWT>는 그 요청의 메서드·경로·본문을 서명한 새 JWT로 바꿉니다. 예시 본문은 코드 블록 마지막 줄바꿈을 포함하지 않으며, 표시한 Content-Length는 그 기준입니다. get 응답과 put 요청의 <DOCX binary>는 실제 DOCX 파일 바이트를 생략한 표시입니다. 이 문자열 대신 파일 바이트를 그대로 전송하고, <DOCX_BYTE_LENGTH>에는 해당 파일의 실제 바이트 수를 넣습니다.

구현 스니펫 읽는 법​

Node.js(Express)·Java(Spring MVC)·Python(FastAPI) 스니펫은 각 작업의 요청 해석, JWT 검증, 저장소 호출과 응답 방법을 보여 줍니다. 언어 탭을 바꾸면 모든 스니펫의 탭이 함께 바뀝니다.

스니펫은 공개 Provider 예제의 공통 함수와 설정을 사용하는 코드입니다. 스니펫만 별도 파일에 복사해서 실행하는 예제가 아닙니다. 각 탭에서 적용할 위치와 필요한 import·설정을 확인합니다.

저장소 접근과 고객 세션·문서 권한 확인은 서비스에 맞게 구현해야 합니다. JWT 검증에는 URL 인코딩을 풀기 전의 요청 경로를 사용합니다.

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

공개 예제의 storage-router.mjs를 기준으로 합니다. 기존 import와 공통 함수를 유지하고, createStorageRouter 안의 프로토콜 처리용 router.use를 아래 작업별 handler로 교체합니다. 기존 오류 처리 middleware도 사용합니다.

requestVerifier.verify(request, { rawPath }, body)는 JWT 서명과 유효 기간을 검증하고, JWT에 기록된 요청 정보를 실제 요청과 비교합니다. body는 수신한 본문의 길이와 SHA-256을 대조하는 데 필요합니다. 검증 예외가 발생하면 오류 처리 middleware가 401 Unauthorized를 반환합니다.

기존 예제에 필요한 import (이미 있으면 유지)

import { rm } from "node:fs/promises";
import { requirePathSegment } from "../domain/storage-route.mjs";

JWT 검증에 실패하면 저장소 작업을 실행하지 않고 오류 응답을 반환해야 합니다. 고객 세션을 사용한다면 코드의 권한 확인 주석 위치에 사용자 인증과 해당 문서·작업의 권한 검사를 추가합니다.

info - 문서 정보 조회​

문서 또는 디렉터리의 메타데이터를 조회하는 필수 endpoint입니다. 문서 내용은 반환하지 않습니다.

요청​

GET /tfo-http-storage/v1/{path}/info

GET /tfo-http-storage/v1/contracts/sample.docx/info HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
항목형식규격
path경로 · string파일 또는 디렉터리의 상대 경로. 루트는 /tfo-http-storage/v1/info
인증 헤더필수 · string이 GET 요청의 JWT. 공통 헤더 참고
요청 본문없음Content-Type 생략. 공통 요청 규칙 적용

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// GET /tfo-http-storage/v1/{path}/info
router.get(/^\/tfo-http-storage\/v1\/(?:.+\/)?info$/, async function infoDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/info".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 본문이 없어야 합니다. 길이 0과 빈 본문의 SHA-256을 사용합니다.
const body = await readRequestBody(request, "none", config, stateStore);
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// 본문이 없는 요청도 길이 0과 빈 본문의 SHA-256이 JWT와 일치해야 합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
sendJson(response, await storageService.info(segments));
});

응답​

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 283

{
"path": "contracts/sample.docx",
"name": "sample.docx",
"type": "file",
"size": 231958,
"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의 바이트 수. 크기 제한 적용
본문object메타데이터 필드를 직접 담은 항목 객체 하나. entry나 data로 감싸지 않음

필드별 조건은 메타데이터 필드를 따릅니다. 루트는 path: "", type: "directory", size: 0을 사용합니다.

상태반환 조건과 처리
404 Not Found대상이 없음. 메타데이터를 만들어 반환하지 않고 404로 응답
413 Payload Too Large파일 크기 또는 응답 JSON이 제한을 넘음
501 Not Implemented필수 endpoint이므로 501을 반환하면 Office가 작업 실패로 처리

list - 하위 항목 조회​

디렉터리 바로 아래의 파일과 디렉터리 목록을 반환합니다. 구현 여부는 선택이며, 재귀 검색이나 페이지네이션은 지원하지 않습니다.

요청​

GET /tfo-http-storage/v1/{path}/list

GET /tfo-http-storage/v1/contracts/list HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
항목형식규격
path경로 · string조회할 디렉터리. 루트는 /tfo-http-storage/v1/list
인증 헤더필수 · string이 GET 요청의 JWT. 공통 헤더 참고
요청 본문없음Content-Type 생략. 공통 요청 규칙 적용

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// GET /tfo-http-storage/v1/{path}/list
router.get(/^\/tfo-http-storage\/v1\/(?:.+\/)?list$/, async function listDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/list".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 본문이 없어야 합니다. 길이 0과 빈 본문의 SHA-256을 사용합니다.
const body = await readRequestBody(request, "none", config, stateStore);
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// 본문이 없는 요청도 길이 0과 빈 본문의 SHA-256이 JWT와 일치해야 합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
if (config.unsupportedOperations?.has("list")) {
return sendOperationNotSupported(response, "list");
}
sendJson(response, await storageService.list(segments));
});

응답​

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 358

{
"entries": [
{
"path": "contracts/sample.docx",
"name": "sample.docx",
"type": "file",
"size": 231958,
"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의 바이트 수. 크기 제한 적용
entries본문 · array · 필수0~10,000개의 항목 객체. 최상위 객체의 유일한 필드
entries[]objectinfo와 같은 필수·선택 필드. 디코딩된 path는 조회 디렉터리의 바로 아래 경로이며 중복 불가

항목의 경로는 항상 루트 기준입니다. 루트 목록에는 contracts, contracts의 목록에는 contracts/sample.docx처럼 반환합니다. 현재 디렉터리 자신, 상위 디렉터리, 하위 폴더 내부 항목을 섞지 않습니다. 순서는 규격에서 정하지 않으며 total, cursor, hasMore 같은 추가 필드를 보내지 않습니다.

빈 디렉터리는 200과 빈 entries 배열을 반환합니다. 디렉터리가 없으면 404, 목록 기능을 지원하지 않으면 인증 후 501과 LIST_NOT_SUPPORTED를 반환합니다.

상태반환 조건과 처리
404 Not Found조회할 디렉터리가 없음
413 Payload Too Large항목 수·메타데이터 크기·포함된 파일 크기가 제한을 넘음. 일부 목록을 성공으로 반환하지 않음
501 Not Implemented인증 후 LIST_NOT_SUPPORTED. Office에서 파일 목록을 사용할 수 없음

get - 문서 다운로드​

파일 내용을 변환하지 않고 전체를 반환하는 필수 endpoint입니다. Office는 이 내용을 읽어 문서를 엽니다.

요청​

GET /tfo-http-storage/v1/{path}/get

GET /tfo-http-storage/v1/contracts/sample.docx/get HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
항목형식규격
path경로 · string다운로드할 파일. 디렉터리의 목록은 list 사용
인증 헤더필수 · string이 GET 요청의 JWT. 공통 헤더 참고
요청 본문없음Content-Type 생략. 공통 요청 규칙 적용

부분 다운로드용 Range 요청은 지원하지 않습니다. 다운로드 URL이나 JSON·Base64 대신 파일 자체를 반환합니다.

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// GET /tfo-http-storage/v1/{path}/get
router.get(/^\/tfo-http-storage\/v1\/(?:.+\/)?get$/, async function getDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/get".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 본문이 없어야 합니다. 길이 0과 빈 본문의 SHA-256을 사용합니다.
const body = await readRequestBody(request, "none", config, stateStore);
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// 본문이 없는 요청도 길이 0과 빈 본문의 SHA-256이 JWT와 일치해야 합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
const file = await storageService.download(segments);
response.status(200).set({
"Content-Type": "application/octet-stream",
"Content-Length": file.contentLength,
"Cache-Control": "no-store",
});
await pipeline(file.stream, response);
});

응답​

HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: <DOCX_BYTE_LENGTH>

<DOCX binary>
항목형식규격
상태200 OK파일 전체 다운로드 성공
Content-Type필수 헤더파일 확장자와 관계없이 application/octet-stream
Content-Length필수 헤더실제 파일 바이트 수. 0~314,572,800
본문binaryContent-Length와 정확히 같은 길이의 원본 파일. JSON 필드 없음

빈 파일은 200과 Content-Length: 0으로 반환합니다. 다운로드가 끝날 때까지 같은 버전의 파일을 반환해야 합니다.

상태반환 조건과 처리
404 Not Found파일이 없음
413 Payload Too Large파일이 허용 크기를 넘음. 성공 헤더나 파일 본문을 보내기 전에 거부
501 Not Implemented필수 endpoint이므로 501을 반환하면 Office가 작업 실패로 처리

put - 문서 저장​

편집한 파일 전체를 HTTP 본문으로 받아 저장합니다. 부분 업데이트와 multipart 업로드는 지원하지 않습니다. 요청 검증을 마친 뒤 파일을 저장하며, 검증이나 저장에 실패하면 기존 파일을 보존해야 합니다.

요청​

PUT /tfo-http-storage/v1/{path}/put

PUT /tfo-http-storage/v1/contracts/sample.docx/put HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/octet-stream
Content-Length: <DOCX_BYTE_LENGTH>

<DOCX binary>
항목형식규격
path경로 · string저장할 파일의 상대 경로. 빈 경로로 Provider 루트를 덮어쓸 수 없음
인증 헤더필수 · string이 PUT 요청의 JWT. 공통 헤더 참고
Content-Type필수 헤더정확히 application/octet-stream
Content-Length필수 헤더전체 파일의 실제 바이트 수. 0~314,572,800
요청 본문binary파일 전체 바이트. JSON 객체·Base64·multipart 형식이 아님
request.arguments.save_typeJWT · string · 선택Office가 파일을 저장하는 이유. 저장 유형 참고. 본문이나 쿼리가 아닌 JWT에 포함

저장 유형​

request.arguments.save_type은 어떤 동작으로 저장이 발생했는지 나타냅니다. Office가 전달하는 값은 다음과 같으며, 대소문자를 구분합니다.

값의미
save사용자가 저장을 선택하는 등 일반 저장
saveAs다른 이름으로 저장. 이 요청의 path에 지정된 대상에 저장
autoSave문서를 편집하는 동안 자동 저장
autoput브라우저 연결이 끊기는 등 편집 연결이 만료된 뒤 서버가 수행하는 자동 저장
close문서를 닫는 과정에서 발생한 자동 저장

선택 필드이므로 값이 없어도 저장 요청을 처리해야 합니다. 생략됐다고 특정 저장 유형으로 간주하지 않습니다.

문서를 닫는 과정에서 변경 내용을 자동 저장할 때는 put이 호출되고, request.arguments.save_type에 close가 전달됩니다. 이 값은 저장이 발생한 이유를 나타내며, 문서 종료 이벤트가 아닙니다. Provider는 해당 파일을 저장해야 하며, close를 받았다는 이유로 dispose를 호출하거나 연결·세션 리소스를 해제해서는 안 됩니다. 이후 요청이 더 이상 오지 않는다고 가정해서도 안 됩니다.

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// PUT /tfo-http-storage/v1/{path}/put
router.put(/^\/tfo-http-storage\/v1\/(?:.+\/)?put$/, async function putDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/put".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 수신 크기를 제한하면서 파일을 임시 저장하고, 바이트 수와 SHA-256을 구합니다.
const body = await readRequestBody(request, "document", config, stateStore);
try {
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// body는 수신한 본문의 길이·SHA-256을 JWT에 기록된 값과 비교하는 데 필요합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
if (config.unsupportedOperations?.has("put")) {
return sendOperationNotSupported(response, "put");
}
const docId = await storageService.save(segments, body.stagingFile);
sendJson(response, { docId });
} finally {
// 검증 실패·미지원 응답·저장 실패로 남은 임시 파일을 삭제합니다. 이미 이동된 파일은 건너뜁니다.
await rm(body.stagingFile, { force: true });
}
});

응답​

파일을 저장한 뒤 해당 문서의 식별자를 JSON 객체로 반환합니다.

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 34

{
"docId": "saved-document-id"
}
항목형식규격
상태200 OK본문 검증을 통과하는 다른 2xx도 허용. 204 No Content는 허용하지 않음
Content-Type필수 헤더UTF-8 application/json
Content-Length필수 헤더실제 JSON 본문 바이트 수. 고정 길이 규칙 적용
본문JSON object · 필수docId 하나만 포함. 추가·중복 필드, 뒤따르는 JSON 값이나 텍스트는 허용하지 않음
docIdstring · 필수ASCII 영문·숫자로 시작하는 1~1,024자의 ASCII 영문·숫자·.·_·:·-. 대소문자와 관계없이 true·false는 예약값

docId는 실제 저장된 대상 문서를 식별합니다. 같은 어댑터의 같은 저장 경로는 내용이 바뀌어도 같은 ID를 유지하고, 다른 경로는 다른 ID를 사용합니다. 일반 저장과 다른 이름으로 저장 모두 이 규칙을 따릅니다. revision이나 원본 세션 ID로 대체하지 않습니다.

Provider는 위 형식에 맞는 자체 문서 ID를 사용할 수 있습니다. 응답 검증에는 PUT 응답 스키마를 참고합니다.

상태반환 조건과 처리
400 Bad Request빈 문서 경로, 선언한 길이와 실제 본문 불일치 등
409 Conflict저장 대상의 현재 상태와 충돌하여 저장할 수 없음
413 Payload Too Large문서가 프로토콜 또는 Provider의 더 낮은 제한을 넘음
501 Not Implemented인증 후 PUT_NOT_SUPPORTED. 저장소에 반영하지 않으며 Office에는 저장 실패로 표시

lock - 문서 lock 설정​

lock은 문서에 lock을 설정하고 owner를 기록합니다. 같은 owner로 다시 요청하면 성공으로 처리하고, 다른 owner의 lock이 있으면 409 Conflict를 반환합니다. lock과 unlock은 함께 구현하거나 둘 다 미지원으로 처리해야 합니다.

요청​

POST /tfo-http-storage/v1/{path}/lock

POST /tfo-http-storage/v1/contracts/sample.docx/lock HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 25

{
"owner": "editor-1"
}
항목형식규격
path경로 · stringlock을 설정할 문서의 상대 경로
인증 헤더필수 · string이 POST 요청의 JWT. 공통 헤더 참고
Content-Type필수 헤더정확히 application/json
Content-Length필수 헤더JSON 본문의 UTF-8 바이트 수
owner본문 · string · 필수비어 있지 않은 lock 소유자 식별자. JSON의 유일한 필드

owner를 저장해 두고, unlock 요청의 owner와 비교합니다. 사용자 인증 토큰으로 사용하지 않습니다. info·list의 locker에 반환할 값은 해당 필드의 최대 255자 제한에 맞춰야 합니다. 이 요청에는 lock 만료 시각·유지 기간·갱신 주기를 지정하는 필드가 없습니다.

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// POST /tfo-http-storage/v1/{path}/lock
router.post(/^\/tfo-http-storage\/v1\/(?:.+\/)?lock$/, async function lockDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/lock".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 수신한 JSON 본문의 바이트 수와 SHA-256을 구합니다.
const body = await readRequestBody(request, "json", config, stateStore);
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// body는 수신한 본문의 길이·SHA-256을 JWT에 기록된 값과 비교하는 데 필요합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
if (config.unsupportedOperations?.has("lock")) {
return sendOperationNotSupported(response, "lock");
}
const owner = parseSingleField(body.bytes, "owner");
await storageService.lock(segments, owner);
response.status(204).set("Cache-Control", "no-store").end();
});

응답​

HTTP/1.1 204 No Content
항목형식규격
상태204 No Contentlock 설정 완료 또는 같은 owner의 lock이 이미 있음. 다른 2xx도 성공
본문없음lock 토큰이나 결과 JSON을 요구하지 않음. 다른 2xx의 본문도 성공 판단에 필요한 필드가 아님
상태반환 조건과 처리
400 Bad Requestowner 생략·빈 문자열·잘못된 타입·추가 JSON 필드
409 Conflict다른 owner의 lock이 있음. 기존 lock을 유지
501 Not Implemented인증 후 LOCK_NOT_SUPPORTED. Office는 lock을 설정하지 않은 채 성공으로 처리

lock·unlock을 지원하지 않을 때는 기능 미지원 응답을 따릅니다.

unlock - 문서 lock 해제​

unlock은 요청의 owner가 설정한 lock을 해제합니다. lock이 이미 없으면 성공으로 처리하고, 다른 owner의 lock이 있으면 409 Conflict를 반환합니다. lock과 함께 구현합니다.

요청​

POST /tfo-http-storage/v1/{path}/unlock

POST /tfo-http-storage/v1/contracts/sample.docx/unlock HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 25

{
"owner": "editor-1"
}
항목형식규격
path경로 · stringlock을 해제할 문서의 상대 경로
인증 헤더필수 · string이 POST 요청의 JWT. 공통 헤더 참고
Content-Type필수 헤더정확히 application/json
Content-Length필수 헤더JSON 본문의 UTF-8 바이트 수
owner본문 · string · 필수lock 요청에 사용한 owner와 같은 값. JSON의 유일한 필드

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// POST /tfo-http-storage/v1/{path}/unlock
router.post(/^\/tfo-http-storage\/v1\/(?:.+\/)?unlock$/, async function unlockDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/unlock".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 수신한 JSON 본문의 바이트 수와 SHA-256을 구합니다.
const body = await readRequestBody(request, "json", config, stateStore);
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// body는 수신한 본문의 길이·SHA-256을 JWT에 기록된 값과 비교하는 데 필요합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
if (config.unsupportedOperations?.has("unlock")) {
return sendOperationNotSupported(response, "unlock");
}
const owner = parseSingleField(body.bytes, "owner");
await storageService.unlock(segments, owner);
response.status(204).set("Cache-Control", "no-store").end();
});

응답​

HTTP/1.1 204 No Content
항목형식규격
상태204 No Content같은 owner의 lock을 해제했거나 lock이 이미 없음. 다른 2xx도 성공
본문없음해제 결과 JSON을 요구하지 않음
상태반환 조건과 처리
400 Bad Requestowner 생략·빈 문자열·잘못된 타입·추가 JSON 필드
409 Conflict현재 lock의 owner가 다름. 기존 lock을 유지
501 Not Implemented인증 후 UNLOCK_NOT_SUPPORTED. Office는 lock 해제 작업 없이 성공으로 처리

mkdir - 디렉터리 생성​

지정한 부모 디렉터리 바로 아래에 새 디렉터리 하나를 만듭니다. 구현 여부는 선택입니다. 중간 디렉터리를 포함한 여러 단계의 경로를 한 번에 만드는 요청은 지원하지 않습니다.

요청​

POST /tfo-http-storage/v1/{parent}/mkdir

POST /tfo-http-storage/v1/contracts/mkdir HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 23

{
"name": "archive"
}
항목형식규격
parent경로 · string기존 부모 디렉터리의 상대 경로. 루트 아래 생성은 /tfo-http-storage/v1/mkdir
인증 헤더필수 · string이 POST 요청의 JWT. 공통 헤더 참고
Content-Type필수 헤더정확히 application/json
Content-Length필수 헤더JSON 본문의 UTF-8 바이트 수
name본문 · string · 필수새 디렉터리 이름 하나. 1~255자이며 JSON의 유일한 필드

name은 URL 인코딩한 경로가 아닌 실제 이름입니다. /·\·제어 문자와 이름 전체가 . 또는 ..인 값은 허용하지 않습니다. 예를 들어 2026/reports를 넣어 하위 경로를 한꺼번에 만들 수 없습니다.

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// POST /tfo-http-storage/v1/{path}/mkdir
router.post(/^\/tfo-http-storage\/v1\/(?:.+\/)?mkdir$/, async function mkdirDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/mkdir".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 수신한 JSON 본문의 바이트 수와 SHA-256을 구합니다.
const body = await readRequestBody(request, "json", config, stateStore);
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// body는 수신한 본문의 길이·SHA-256을 JWT에 기록된 값과 비교하는 데 필요합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
if (config.unsupportedOperations?.has("mkdir")) {
return sendOperationNotSupported(response, "mkdir");
}
const name = requireChildName(parseSingleField(body.bytes, "name"));
await storageService.createDirectory(segments, name);
response.status(204).set("Cache-Control", "no-store").end();
});

응답​

HTTP/1.1 204 No Content
항목형식규격
상태204 No Contentparent/name 디렉터리 생성 완료. 다른 2xx도 성공
본문없음생성한 항목 객체를 요구하지 않음. 이후 info 또는 list로 조회 가능
상태반환 조건과 처리
400 Bad Requestname 생략·잘못된 타입·추가 필드·허용하지 않는 이름
501 Not Implemented인증 후 MKDIR_NOT_SUPPORTED. 디렉터리를 생성하지 않음

rename - 이름 변경​

파일 또는 디렉터리의 이름을 같은 부모 디렉터리 안에서 바꿉니다. 구현 여부는 선택입니다.

요청​

POST /tfo-http-storage/v1/{path}/rename

POST /tfo-http-storage/v1/contracts/sample.docx/rename HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
Content-Type: application/json
Content-Length: 29

{
"name": "reviewed.docx"
}
항목형식규격
path경로 · string이름을 바꿀 기존 항목의 상대 경로. Provider 루트는 변경 불가
인증 헤더필수 · string이 POST 요청의 JWT. 공통 헤더 참고
Content-Type필수 헤더정확히 application/json
Content-Length필수 헤더JSON 본문의 UTF-8 바이트 수
name본문 · string · 필수새 이름 하나. 1~255자이며 JSON의 유일한 필드

name에는 디코딩된 실제 이름을 넣고 mkdir의 이름 규칙을 적용합니다. contracts/sample.docx에 reviewed.docx를 보내면 결과 경로는 contracts/reviewed.docx입니다.

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// POST /tfo-http-storage/v1/{path}/rename
router.post(/^\/tfo-http-storage\/v1\/(?:.+\/)?rename$/, async function renameDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/rename".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 수신한 JSON 본문의 바이트 수와 SHA-256을 구합니다.
const body = await readRequestBody(request, "json", config, stateStore);
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// body는 수신한 본문의 길이·SHA-256을 JWT에 기록된 값과 비교하는 데 필요합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
if (config.unsupportedOperations?.has("rename")) {
return sendOperationNotSupported(response, "rename");
}
const name = requireChildName(parseSingleField(body.bytes, "name"));
await storageService.rename(segments, name);
response.status(204).set("Cache-Control", "no-store").end();
});

응답​

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인증 후 RENAME_NOT_SUPPORTED. 원래 이름을 유지

delete - 항목 삭제​

파일 또는 디렉터리를 삭제합니다. 구현 여부는 선택이며, Provider의 저장소 루트는 삭제할 수 없습니다.

요청​

DELETE /tfo-http-storage/v1/{path}/delete

DELETE /tfo-http-storage/v1/contracts/reviewed.docx/delete HTTP/1.1
Host: storage.example.com
X-TFO-Storage-Request-JWT: <SIGNED_REQUEST_JWT>
항목형식규격
path경로 · string삭제할 항목의 상대 경로. Provider 루트는 삭제 불가
인증 헤더필수 · string이 DELETE 요청의 JWT. 공통 헤더 참고
요청 본문없음Content-Type 생략. 공통 요청 규칙 적용

구현 예제​

이 스니펫에서 사용하는 공통 함수와 저장소 서비스: Node.js Provider 구현

// DELETE /tfo-http-storage/v1/{path}/delete
router.delete(/^\/tfo-http-storage\/v1\/(?:.+\/)?delete$/, async function deleteDocument(request, response) {
const rawPath = request.originalUrl;
if (rawPath.includes("?")) throw new StorageError(400, "Query not allowed");
// URL 경로에서 접두사와 작업 이름을 제외하고, 폴더명·파일명의 URL 인코딩을 풉니다.
const encodedPath = rawPath.slice("/tfo-http-storage/v1".length, -"/delete".length);
let segments;
try {
segments = encodedPath ? encodedPath.slice(1).split("/").map(
part => requirePathSegment(decodeURIComponent(part)),
) : [];
} catch { throw new StorageError(400, "Invalid document path"); }
// 본문이 없어야 합니다. 길이 0과 빈 본문의 SHA-256을 사용합니다.
const body = await readRequestBody(request, "none", config, stateStore);
// JWT 서명·유효 기간을 검증하고, 요청 정보가 일치하는지와 JWT가 재사용됐는지 확인합니다.
// X-TFO-Storage-Request-JWT는 request.headers에서 읽습니다.
// 본문이 없는 요청도 길이 0과 빈 본문의 SHA-256이 JWT와 일치해야 합니다.
const signedRequest = await requestVerifier.verify(request, { rawPath }, body);
// 고객 세션을 사용한다면 이 위치에 client_metadata의 토큰 검증과 문서·작업 권한 검사를 추가합니다.
if (config.unsupportedOperations?.has("delete")) {
return sendOperationNotSupported(response, "delete");
}
await storageService.delete(segments);
response.status(204).set("Cache-Control", "no-store").end();
});

응답​

HTTP/1.1 204 No Content
항목형식규격
상태204 No Content항목 삭제 완료. 다른 2xx도 성공
본문없음삭제한 항목 객체나 삭제 개수를 요구하지 않음
상태반환 조건과 처리
400 Bad RequestProvider 루트 삭제 시도
501 Not Implemented인증 후 DELETE_NOT_SUPPORTED. 항목을 삭제하지 않음

기능 미지원 응답​

구현 여부가 선택인 작업을 지원하지 않을 때는 아래 형식으로 응답합니다. 해당 요청의 인증·본문 검증·재전송 검사를 마친 뒤 반환해야 합니다. 상태는 501, 미디어 유형은 application/json, JSON 본문은 현재 작업과 일치하는 code 하나만 포함합니다. charset 매개변수는 허용합니다.

항목형식규격
상태필수정확히 501 Not Implemented
Content-Type필수 헤더application/json. charset 매개변수 허용
code본문 · string · 필수아래 표의 현재 작업에 해당하는 정확한 값. 다른 필드 추가 불가
본문 길이짧은 UTF-8 JSONOffice의 오류 본문 읽기 한도 8 KiB 안에 전체 객체가 들어가야 함

기능 미지원 응답에는 성공 응답의 Content-Length 필수 조건을 적용하지 않습니다. 아래 예시처럼 본문 길이를 명시해 반환할 수 있습니다.

HTTP/1.1 501 Not Implemented
Content-Type: application/json
Content-Length: 34

{
"code": "LIST_NOT_SUPPORTED"
}
작업정확한 codeOffice 동작
listLIST_NOT_SUPPORTED목록 기능 사용 불가. 등록 시 안내를 확인하고 연결을 저장할 수 있음
putPUT_NOT_SUPPORTED저장 실패
lockLOCK_NOT_SUPPORTEDlock을 설정하지 않은 채 성공으로 처리
unlockUNLOCK_NOT_SUPPORTEDlock을 해제하지 않은 채 성공으로 처리
mkdirMKDIR_NOT_SUPPORTED디렉터리 생성 불가
renameRENAME_NOT_SUPPORTED이름 변경 불가
deleteDELETE_NOT_SUPPORTED삭제 불가

info와 get은 필수이므로 501을 반환하면 작업이 실패합니다. Office는 선택 작업의 응답도 상태 코드·Content-Type·본문이 위 형식과 다르면 기능 미지원으로 인식하지 않고 실패로 처리합니다.

기능 미지원 응답 중 Office가 성공으로 처리하는 예외는 lock·unlock입니다. 두 작업을 모두 지원하지 않도록 설정하고, 각 요청에 맞는 LOCK_NOT_SUPPORTED 또는 UNLOCK_NOT_SUPPORTED를 반환해야 합니다. 둘 중 하나만 지원하지 않도록 구현하면 안 됩니다. 이 경우 문서의 lock은 설정되지 않으므로, 여러 사용자가 동시에 저장할 때 덮어쓰기나 충돌을 어떻게 처리할지 Provider에서 정해야 합니다.

lock·unlock을 구현한 경우 정상 응답은 204입니다. 409 Conflict, 인증 실패, 네트워크 오류, 저장소 장애는 Office가 작업 실패로 처리합니다.

관리자 연결 테스트는 list만 호출합니다. 나머지 기능은 실제 문서 작업에서 확인하며 등록 과정에서 쓰기·삭제 작업을 임의로 실행하지 않습니다. 기능 미지원 JSON schema도 함께 참고합니다.

오류 응답​

info의 404는 대상 없음으로 처리합니다. 이 경우와 위 형식에 맞는 기능 미지원 응답을 제외하면 2xx가 아닌 응답은 Office 작업 실패입니다. 일반 오류 본문에는 고정된 JSON 스키마가 없습니다. 짧은 오류 설명을 반환하되 시크릿, JWT, 내부 경로, 문서 내용, 스택 추적을 포함하지 않습니다. Office는 오류 본문을 최대 8 KiB까지 읽어 사용자에게 표시할 수 있습니다.

항목형식규격
상태HTTP 상태 코드아래 오류 원인에 맞게 선택
Content-Type본문에 맞게 지정일반 오류는 고정 미디어 유형이 없음
본문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이름·항목 타입 또는 lock의 owner 충돌
411 Length Required본문에 필요한 Content-Length 누락
413 Payload Too Large요청 본문, 문서, 목록 크기 또는 Provider가 설정한 제한 초과
415 Unsupported Media Type요청 Content-Type이 작업의 규격과 다름
500 Internal Server ErrorProvider 내부 처리 오류. 세부 예외는 응답에 노출하지 않음
501 Not Implemented선택 작업을 지원하지 않음. 위 기능 미지원 응답 형식을 따라야 함
503 Service Unavailable일시적인 저장소 장애

인증 오류를 기능 미지원 응답으로 바꾸거나 저장소 장애를 성공으로 응답하지 않습니다. 예제별 설정은 Provider 예제 실행 방법에서 확인합니다.