Data Bundle에 데이터 업로드
이 가이드에서는 RealityConnect API를 통해 캡처의 원시 입력 파일을 data bundle에 업로드하는 방법을 설명합니다.
모든 업로드 엔드포인트에는 write:bundle 스코프가 필요합니다. 세션 나열에는 read:bundle이 필요합니다.
업로드 플로우
섹션 제목: “업로드 플로우”data bundle은 단일 캡처의 원시 입력을 담습니다. 포인트 클라우드, 궤적, 이미지, 이미지별 메타데이터 등입니다. 번들이 허용하는 정확한 파일 집합은 번들이 생성된 디바이스에 따라 다릅니다.
파일은 업로드 세션으로 업로드됩니다. 세션은 번들 파일의 한 배치를 위해 서버가 생성하는 컨테이너입니다. 세션을 열 때 배치에 포함될 파일 수(expectedFileCount)를 선언합니다. 해당 개수만큼의 파일이 마무리되면 세션이 자동으로 완료됩니다.
각 개별 파일은 S3 멀티파트 업로드로 업로드됩니다.
- 번들에서 세션을 열고, 보낼 파일의 총 개수를 선언합니다.
- 각 파일에 대해: 업로드를 개시하고, 반환된 URL에 파트를 직접 PUT한 다음, 마무리합니다.
- 마무리된 파일 수가
expectedFileCount에 도달하면 세션이completed로 표시됩니다.
sequenceDiagram
participant Client as Client
participant RCAPI as RCAPI
participant S3 as S3
Client->>RCAPI: POST upload-sessions
RCAPI-->>Client: sessionId + requirements
loop for each file
Client->>RCAPI: POST initiate file
RCAPI-->>Client: fileId + presigned part URLs
loop for each part
Client->>S3: PUT part
S3-->>Client: ETag
end
Client->>RCAPI: POST finalize
RCAPI-->>Client: file completed + sessionStatus
end
업로드 requirements(세션을 열 때 반환됨)는 번들이 허용하는 파일 유형, 필수 유형, 그리고 이들이 서로 어떻게 종속되는지를 정확히 알려줍니다.
실습 예제: ZF SLAM 캡처
섹션 제목: “실습 예제: ZF SLAM 캡처”ZF SLAM 번들은 유형 간 종속성이 있는 네 가지 서로 다른 파일 유형을 허용하기 때문에 좋은 예입니다. 완전한 ZF 캡처는 다음으로 구성됩니다.
| 파일 | 유형 | 참고 |
|---|---|---|
scan.las | 포인트 클라우드 | 정확히 하나 |
…laser_0.txt | 궤적 | 정확히 하나 |
panorama/*.jpg | 파노라마 이미지 | 여러 개, 각각 메타데이터와 짝을 이룸 |
panorama/*.json | 이미지별 메타데이터 | 여러 개, 이미지당 하나 |
따라서 전체 ZF 배치는 포인트 클라우드 1 + 궤적 1 + 이미지 N + 메타데이터 파일 N입니다. 단일 세션에서 이 모두를 업로드하겠습니다.
엔드포인트
섹션 제목: “엔드포인트”모든 경로는 지역별 api_url을 기준으로 합니다. 번들은 자체 id(bundleId)로 지정됩니다.
| 메서드 | 경로 | 스코프 | 목적 |
|---|---|---|---|
POST | /v1/bundles/{bundleId}/upload-sessions | write:bundle | 세션 열기 |
GET | /v1/bundles/{bundleId}/upload-sessions | read:bundle | 세션 나열(재개 / 진행 상황) |
POST | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files | write:bundle | 파일 업로드 개시 |
GET | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex} | write:bundle | 만료된 파트 URL 재발급 |
POST | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalize | write:bundle | 파일 업로드 마무리 |
시작하기 전에
섹션 제목: “시작하기 전에”data bundle에 데이터를 업로드하려면 다음이 필요합니다.
access_token과 지역별api_url이 있는 완료된 OAuth 플로- 기존 data bundle(
bundleId)
번들이 아직 없다면 Data Bundle 생성을 따라 먼저 하나를 생성하세요. 해당 플로는 디바이스 입력 트리를 가져오고, 리프 dbuPath를 선택하고, 상위 노드 아래에 번들을 생성합니다.
1단계: 업로드 세션 열기
섹션 제목: “1단계: 업로드 세션 열기”배치에 포함될 파일의 총 개수를 선언합니다. 이미지 13개가 있는 전체 ZF 캡처의 경우 1 + 1 + 13 + 13 = 28입니다.
POST {api_url}/v1/bundles/{bundleId}/upload-sessionsAuthorization: Bearer {access_token}Content-Type: application/json
{ "expectedFileCount": 28 }응답은 sessionId와 번들의 requirements를 반환합니다.
{ "sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b", "expectedFileCount": 28, "requirements": [ { "type": "pointclouds", "allowedExtensions": ["las", "laz"], "optional": false, "allowMultiples": false, "dependencies": [] }, { "type": "trajectories", "allowedExtensions": ["txt"], "optional": false, "allowMultiples": false, "dependencies": [] }, { "type": "pictures", "allowedExtensions": ["jpg", "png", "jpeg"], "optional": true, "allowMultiples": true, "dependencies": ["picturesMetadata"] }, { "type": "picturesMetadata", "allowedExtensions": ["json"], "optional": true, "allowMultiples": true, "dependencies": ["pictures"] } ]}requirements와 유형 간 종속성 이해하기
섹션 제목: “requirements와 유형 간 종속성 이해하기”requirements의 각 항목은 번들이 허용하는 하나의 파일 유형을 설명합니다.
| 필드 | 의미 |
|---|---|
type | 파일 업로드를 개시할 때 type으로 전달하는 식별자 |
allowedExtensions | 이 유형에 허용되는 파일 확장자 |
optional | false인 경우, 이 유형의 파일이 하나 이상 없으면 번들을 처리할 수 없음 |
allowMultiples | false인 경우, 이 유형의 파일은 하나만 업로드할 수 있음 |
dependencies | 이 유형이 존재할 때 함께 존재해야 하는 다른 유형 |
ZF 예제의 경우:
- **
pointclouds**와 **trajectories**는 필수(optional: false)이며 단일(allowMultiples: false)입니다. 모든 ZF 번들에는 각각 정확히 하나가 필요합니다. - **
pictures**와 **picturesMetadata**는 선택 사항이며 여러 파일을 허용합니다. - 두 이미지 유형은 서로 종속됩니다:
pictures.dependencies = ["picturesMetadata"]이고picturesMetadata.dependencies = ["pictures"]입니다.
종속성이 의미하는 것: 한 유형이 존재하면, 나열된 모든 유형도 존재해야 합니다. ZF 이미지 유형이 서로를 참조하기 때문에, 이들은 한 쌍으로서 전부 아니면 전무입니다. 이미지와 메타데이터를 함께 업로드하거나 둘 다 건너뛸 수 있지만, 하나만 업로드할 수는 없습니다.
유효한 ZF 배치:
- 최소 —
포인트 클라우드 1 + 궤적 1(이미지 없음).expectedFileCount = 2. - 전체 —
포인트 클라우드 1 + 궤적 1 + pictures N + picturesMetadata N.expectedFileCount = 2 + 2N.
유효하지 않음:
- 메타데이터 없는 이미지(또는 그 반대). 유형 간 종속성이 충족되지 않기 때문입니다.
- 포인트 클라우드 또는 궤적이 없는 배치. 필수 유형이 누락되었기 때문입니다.
세션을 열기 전에 업로드할 모든 파일을 세고 그 총계를 expectedFileCount로 설정하세요. 선택적 유형은 배치에 포함하는 경우에만 계산됩니다. 세션은 해당 개수만큼의 파일이 마무리될 때 완료되므로, 선언하는 숫자는 실제로 업로드하는 것과 일치해야 합니다.
2단계: 각 파일 업로드
섹션 제목: “2단계: 각 파일 업로드”배치의 모든 파일에 대해 다음 세 개의 호출을 반복합니다.
2a. 개시
섹션 제목: “2a. 개시”파일 이름, 바이트 단위 크기, 그리고 type(requirements 유형 중 하나)을 전달합니다.
POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/filesAuthorization: Bearer {access_token}Content-Type: application/json
{ "fileName": "scan.las", "fileSize": 734003200, "type": "pointclouds" }응답은 멀티파트 업로드를 설명합니다. 보낼 parts 수와 파트당 하나의 사전 서명된 url입니다.
{ "fileId": "c8c8cb0b-f476-4211-a3a4-72e4e04b91d5", "parts": 8, "urls": [ { "partNumber": 1, "url": "https://s3…/part-1?…" }, { "partNumber": 2, "url": "https://s3…/part-2?…" } ]}2b. 파트 업로드
섹션 제목: “2b. 파트 업로드”파일을 ceil(fileSize / parts) 바이트의 순차적인 parts 청크로 분할하고 각 청크를 사전 서명된 url에 PUT합니다. 이러한 요청에는 인증 헤더가 전송되지 않습니다. URL이 이미 서명되어 있습니다. 마무리하는 데 필요하므로 모든 파트의 ETag 응답 헤더를 보관하세요.
import math, requests
def upload_parts(file_path, file_size, urls): part_size = math.ceil(file_size / len(urls)) parts = [] with open(file_path, "rb") as handle: for entry in sorted(urls, key=lambda u: u["partNumber"]): res = requests.put(entry["url"], data=handle.read(part_size)) res.raise_for_status() parts.append({"partNumber": entry["partNumber"], "etag": res.headers["ETag"]}) return partsETag는 주변 따옴표를 포함하여 반환된 그대로 전달하세요.
2c. 마무리
섹션 제목: “2c. 마무리”파일을 완료하기 위해 수집한 파트를 보냅니다. 엔드포인트는 200 OK를 반환합니다.
POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizeAuthorization: Bearer {access_token}Content-Type: application/json
{ "parts": [ { "partNumber": 1, "etag": "\"a1b2…\"" }, { "partNumber": 2, "etag": "\"c3d4…\"" } ] }응답은 파일을 반환하고 세션 상태를 보고합니다.
{ "fileId": "c8c8cb0b-f476-4211-a3a4-72e4e04b91d5", "fileName": "scan.las", "type": "pointclouds", "size": 734003200, "status": "completed", "sessionStatus": "pending"}sessionStatus는 마지막 예상 파일이 마무리될 때까지 pending으로 유지되며, 그 시점에 completed가 됩니다.
만료된 URL 새로 고침
섹션 제목: “만료된 URL 새로 고침”사전 서명된 URL은 만료됩니다. 업로드가 오래 실행되어 PUT이 403을 반환하기 시작하면, 중단한 파트부터 URL을 재발급하고 계속하세요.
GET {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/0Authorization: Bearer {access_token}{ "startIndex": 0, "urls": [ { "partNumber": 1, "url": "https://s3…/part-1?…" } ] }파일을 다시 개시할 필요는 없습니다. 파트 URL만 갱신됩니다.
3단계: 진행 상황 추적 및 재개
섹션 제목: “3단계: 진행 상황 추적 및 재개”sessionId를 메모리에 유지하지 않고도 진행 상황을 확인하거나 중단된 배치를 재개하려면 번들의 세션을 나열하세요. 목록은 페이지 단위이며 정렬 가능하고, 선택적 status 필터를 제공합니다.
쿼리 매개변수: page(기본값 1), limit(기본값 20, 최대 20), sortBy(createdAt 또는 status, 기본값 createdAt), sortDir(ASC 또는 DESC, 기본값 ASC), status(선택적 필터: pending 또는 completed).
GET {api_url}/v1/bundles/{bundleId}/upload-sessions?page=1&limit=20&sortBy=createdAt&sortDir=DESCAuthorization: Bearer {access_token}{ "items": [ { "sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b", "status": "pending", "expectedFileCount": 28, "finalizedFileCount": 14 } ], "total": 1}finalizedFileCount와 expectedFileCount를 비교하면 남은 파일 수를 알 수 있습니다. 재개하려면 아직 완료되지 않은 파일만 개시하고 마무리하세요.
검증 및 제한
섹션 제목: “검증 및 제한”RealityConnect API를 사용하여 데이터를 업로드할 때 무엇에 대한 책임이 있는지 결정하므로 이 부분을 주의 깊게 읽으세요.
- 업로드 엔드포인트는 제공한
type아래에 각 파일을 기록하고 순수하게 개수로 세션을 완료합니다.finalizedFileCount가expectedFileCount와 같아지면 세션은completed가 됩니다. 업로드 시 서버는type이requirements중 하나인지, 파일 확장자가 허용되는지, 필수 유형이 존재하는지, 유형 간 종속성이 충족되는지를 검증하지 않습니다. - 모든 규칙(필수 유형,
allowMultiples, 유형 간dependencies)은 번들이 처리를 위해 전송된 후에 시행됩니다.requirements를 무시하는 배치는 성공적으로 업로드되지만 처리에 실패합니다. - 심층 콘텐츠 검증은 업로드 API에서 절대 수행되지 않습니다.
.las가 실제 ZF 포인트 클라우드인지,.json이 이미지를 올바르게 참조하는지는 처리 시에만 확인됩니다.
requirements를 계약으로 취급하세요. 업로드 배치에서 유형, 확장자, 선택성, 다중성, 종속성을 준수하여 업로드 후 번들을 처리할 수 있도록 하세요. 잘못된 업로드 구조로 인한 처리 실패는 환불되지 않습니다.
전체 예제
섹션 제목: “전체 예제”ZF 캡처를 위해 모두 조합합니다. files는 로컬 데이터셋에서 만드는 목록으로, 각 항목은 경로와 requirements의 type을 짝지어 줍니다.
import math, requests
API_URL = "https://<your-regional-api-url>"TOKEN = "<access_token>"BUNDLE_ID = "<bundleId>"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# (local path, schema type) for every file in the batchfiles = [ ("scan.las", "pointclouds"), ("2024-07-scan.laser_laser_0.txt", "trajectories"), ("panorama/2024-07-scan-idx40.jpg", "pictures"), ("panorama/2024-07-scan-idx40.json", "picturesMetadata"), # … remaining image / metadata pairs …]
def upload_one(session_id, path, file_type): size = __import__("os").path.getsize(path) base = f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions/{session_id}/files"
initiated = requests.post( base, headers=HEADERS, json={"fileName": path.split("/")[-1], "fileSize": size, "type": file_type}, ).json()
part_size = math.ceil(size / initiated["parts"]) parts = [] with open(path, "rb") as handle: for entry in sorted(initiated["urls"], key=lambda u: u["partNumber"]): res = requests.put(entry["url"], data=handle.read(part_size)) res.raise_for_status() parts.append({"partNumber": entry["partNumber"], "etag": res.headers["ETag"]})
return requests.post( f"{base}/{initiated['fileId']}/finalize", headers=HEADERS, json={"parts": parts}, ).json()
# 1. Open the session for the exact number of files we will send.session = requests.post( f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions", headers=HEADERS, json={"expectedFileCount": len(files)},).json()
# 2. Upload every file under its type.result = Nonefor path, file_type in files: result = upload_one(session["sessionId"], path, file_type)
# 3. The last finalize reports the session as completed.assert result["sessionStatus"] == "completed"다음 단계
섹션 제목: “다음 단계”모든 파일이 업로드되고 세션이 completed가 되면, 원시 입력을 볼 수 있는 출력물로 변환하려면 처리 및 모니터링으로 계속 진행하세요.