Introduction & Requirements
사용자가 여행 중 사진 여러 장과 긴 동영상을 촬영합니다.
숙소의 Wi-Fi에 연결하자 사진 앱에 업로드 중이라는 표시가 나타납니다. 사용자는 화면을 끄고 잠자리에 듭니다. 다음 날 앱을 열어보니 일부 사진은 백업되었지만, 동영상은 여전히 대기 중입니다.
진행률이 거의 끝났던 동영상이 왜 다시 처음부터 올라갈까요.
사진 한 장은 백업되었다고 표시되는데, 다른 기기에서는 움직이는 장면이 사라지고 정지 이미지로만 보이는 이유는 무엇일까요.
또 다른 사용자는 사진 접근 권한을 전체 허용에서 선택한 항목만 허용으로 바꿉니다. 앱이 이전에 보던 사진을 더 이상 조회하지 못하게 되었습니다. 이것을 사진 삭제로 해석해 서버의 사진까지 지우면 안 됩니다.
카메라 롤의 백그라운드 업로드에서는 다음 사실이 서로 다릅니다.
라이브러리에서 항목을 발견함
|
해당 항목의 원본 자원을 읽을 수 있음
|
전송할 바이트를 일관된 상태로 준비함
|
서버가 일부 바이트를 보관함
|
필수 자원 전체를 검증함
|
서버 라이브러리에 미디어를 확정함
|
미리보기와 검색용 파생 데이터가 준비됨썸네일이 보인다고 원본을 읽을 수 있는 것은 아닙니다. 네트워크 라이브러리가 바이트를 모두 보냈다고 서버 백업이 확정된 것도 아닙니다. 사진과 동영상이 함께 구성하는 미디어라면 파일 하나의 성공만으로 항목 전체를 완료 처리할 수도 없습니다.
Google Photos의 공개 도움말도 기기에 저장된 항목이 앱에 표시된다는 사실과 특정 Google 계정에 백업되었다는 사실을 구분합니다. 백업 대상 계정과 항목별 상태를 확인하는 기능이 필요한 이유입니다. Google Help
이번 설계는 iOS와 Android의 카메라 롤을 읽고, 사용자가 허용한 사진과 동영상을 클라우드에 백업하는 모바일 앱을 대상으로 합니다.
아래의 업로드 프로토콜, 데이터 모델, 우선순위와 보존 정책은 이 글에서 제안하는 설계입니다. Google Photos의 비공개 내부 구현을 재현하는 것은 아닙니다.
폴더의 경로와 파일 이름을 양쪽에서 같게 만드는 시스템을 설계하지 않겠습니다. 이 앱은 운영체제의 사진 라이브러리를 원본으로 읽습니다. 사진 접근 권한, 원본의 실제 위치, 촬영 후 편집과 복합 미디어의 구성 요소가 전송 상태와 함께 관리되어야 합니다.
Functional Requirements
사용자는 백업 대상 계정과 라이브러리 접근 범위, 원본 또는 용량 절약 정책을 선택할 수 있습니다.
앱은 접근 가능한 라이브러리의 신규 항목과 변경을 발견하고, 앱 종료 후 놓친 변경도 다시 확인합니다.
사용자가 직접 선택한 항목을 우선 처리하면서 최신 촬영분과 기존 미완료 항목을 백그라운드에서 업로드합니다.
앱은 Wi-Fi 전용, 셀룰러 허용, 로밍 제한과 일일 셀룰러 예산에 따라 업로드를 실행하거나 대기시킵니다.
네트워크 전환과 프로세스 종료 이후에는 서버에 저장된 부분을 확인하고, 같은 원본 버전의 미완료 전송을 재개합니다.
Live Photo와 같은 복합 미디어는 필수 구성 요소를 함께 보관하고, 일부 파일만 성공한 상태를 전체 백업 완료와 구분합니다.
같은 항목의 재시도는 기존 작업에 연결하고, 이미 보관한 동일 바이트는 안전한 범위에서 재사용합니다.
사용자는 대기, 원본 준비, 전송, 서버 확인과 실패 상태를 구분해 볼 수 있습니다.
앱은 로컬 삭제, 클라우드 휴지통 이동, 보관 처리와 사진 접근 권한 철회를 서로 다른 변경으로 처리합니다.
사용자가 백업을 중지하거나 계정을 전환하면 이전 작업의 늦은 결과가 다른 계정의 상태에 반영되지 않아야 합니다.
Target & Scale, Traffic & Capacity Assumptions
사진 개수와 전송 바이트는 다른 부하를 만듭니다
일간 활성 사용자를 500만 명으로 가정하겠습니다. 이 가운데 60퍼센트가 백업을 사용하고, 백업 사용자 한 명에게 하루 평균 12개의 신규 항목이 생긴다고 두겠습니다.
하루 백업 대상은 3,600만 항목입니다. 평균은 초당 약 417개이며, 피크를 평균의 5배로 두면 초당 약 2,083개입니다.
기본 계산에서는 사진과 동영상을 각각 자원 하나로 단순화하겠습니다. Live Photo와 RAW 조합의 추가 자원, 편집본과 재시도는 이후 비용에 더해야 합니다.
사진이 90퍼센트이고 평균 크기가 4MB라면 하루 사진 데이터는 약 129.6TB입니다. 동영상이 10퍼센트이고 평균 크기가 80MB라면 하루 동영상 데이터는 약 288TB입니다.
전체는 하루 약 417.6TB입니다. 십진 단위로 계산한 평균 유입 대역폭은 약 38.7Gbps이며, 같은 피크 계수를 적용하면 약 193.3Gbps입니다.
항목 수에서는 동영상이 적지만 전송량에서는 더 큰 비중을 차지합니다. 따라서 사진과 동영상을 같은 개수 기준으로만 예약하면 큰 동영상이 다른 업로드를 오래 기다리게 만들 수 있습니다.
여기에서 사용한 수치는 실제 Google Photos 트래픽이 아닙니다. 저장 복제본, 프로토콜 부가 정보와 파생 미디어 생성량도 포함하지 않았습니다.
청크 크기는 요청 수와 재시도 비용을 바꿉니다
이번 초기 전송 단위를 8MiB로 가정하겠습니다.
평균 4MB 사진은 하나의 전송 단위에 들어갑니다. 평균 80MB 동영상은 8MiB 단위로 나누면 약 10개의 부분 요청이 필요합니다.
앞선 가정에서는 하루 부분 요청이 약 6,840만 건입니다. 평균은 초당 약 792건입니다. 세션 생성과 완료 확인, 실패 재시도 요청은 별도입니다.
사진 부분 요청
3,240만 항목 x 1
동영상 부분 요청
360만 항목 x 10
합계
6,840만 부분 요청청크를 작게 하면 중단 시 다시 보낼 수 있는 범위를 줄이기 쉽습니다. 대신 요청 수와 파일 준비, 상태 저장 비용이 늘어납니다.
큰 청크는 처리량에 유리할 수 있지만 실행 시간과 재시도 비용이 커집니다. 모든 사진을 잘게 나누기보다 작은 사진은 한 번에 보내고, 큰 자원만 나누는 정책으로 시작하겠습니다.
초기 라이브러리 백업은 일상적인 신규 업로드와 다릅니다
새로 촬영한 사진 12개를 처리하는 것과 기존 사진 5만 개를 처음 확인하는 것은 다른 작업입니다.
초기 인덱싱에서는 썸네일과 원본을 모두 읽지 않습니다. 먼저 식별자와 필요한 메타데이터를 제한된 페이지로 확인하고, 업로드할 항목의 원본만 준비합니다.
촬영 날짜가 오래되었다는 이유로 이미 백업된 것으로 판단하지도 않습니다. 오늘 가져온 오래된 사진이나 다른 앱이 저장한 동영상이 새 항목일 수 있기 때문입니다.
대량 초기 백업에서는 신규 촬영분을 우선하되 기존 항목이 영원히 대기하지 않도록 오래 기다린 작업의 우선순위를 높이는 정책을 함께 둡니다.
Non-Functional Requirements
아래 목표와 예산은 제안하는 제품의 초기 시험 기준입니다.
수동 백업 선택부터 로컬 대기 상태 표시까지 p95 100ms 이내를 목표로 합니다. 이 표시는 서버 백업 완료를 의미하지 않습니다.
앱이 전경에 있고 접근 권한과 로컬 저장소가 정상인 시험 조건에서 최근 변경 200개를 인덱스에 반영하는 시간을 p95 2초 이내로 목표로 합니다.
이미 실행 중인 앱에서 모든 정책 조건을 만족한 작업은 p95 1초 이내에 실행기로 전달하는 것을 목표로 합니다. 운영체제가 백그라운드 실행을 허용하기까지의 시간은 별도로 측정합니다.
서버가 확인한 자원과 로컬 원본 버전의 연결이 유지되어야 하며, 전송 중 원본이 바뀌면 이전 부분과 새로운 바이트를 섞지 않습니다.
재시도와 완료 응답 유실이 중복 미디어 생성이나 중복 저장 공간 차감으로 이어지지 않아야 합니다.
원본 전송을 위해 사진을 전체 해상도의 비트맵으로 디코딩하지 않고, 메모리와 임시 저장 공간에 명시적인 상한을 둡니다.
백업 완료는 필수 자원의 검증과 서버 미디어 확정 이후에만 표시합니다. 미리보기와 검색 준비 상태는 별도로 관리합니다.
접근 권한을 확인하지 못한 항목을 삭제된 항목으로 추정하지 않으며, 계정과 권한이 바뀐 뒤의 늦은 응답을 현재 상태에 적용하지 않습니다.
배터리와 발열, 네트워크 예산 때문에 멈춘 작업은 실패 반복 대신 대기 이유와 복구 위치를 유지해야 합니다.
Out of Scope
임의의 디렉터리 구조와 파일 이름을 여러 기기에서 동일하게 유지하는 범용 파일 동기화는 다루지 않습니다.
사진 편집기의 전체 기능과 서로 다른 편집 결과를 하나의 이미지로 자동 병합하는 기능은 구현하지 않습니다.
얼굴 인식, 사진 추천과 자연어 검색 모델의 학습 및 서빙은 제외합니다. 업로드 이후 파생 처리 경계만 포함합니다.
운영체제의 사진 접근 권한을 우회하거나 사용자가 허용하지 않은 항목을 자동으로 수집하지 않습니다.
백업 완료 직후 카메라 롤의 원본을 자동 삭제하는 기능은 기본안에서 제외합니다. 저장 공간 확보는 별도의 사용자 승인 동작으로 둡니다.
Architecture
사진 라이브러리와 앱의 전송 파일을 분리합니다
사진 라이브러리는 앱이 독점적으로 소유하는 디렉터리가 아닙니다.
사용자가 다른 앱에서 편집하거나 삭제할 수 있고, 실제 원본이 기기에 없을 수도 있습니다. 따라서 사진 식별자를 저장한 뒤 나중에 같은 바이트를 언제든 읽을 수 있다고 가정하지 않습니다.
이번 구조에서는 라이브러리를 관측하는 계층과 업로드할 자원을 준비하는 계층을 나눕니다.
Library Indexer는 어떤 항목을 발견했고 어떤 변경을 다시 확인해야 하는지 관리합니다.
Resource Exporter는 특정 항목의 특정 버전을 전송 가능한 자원으로 준비합니다. 임시 원본이 아니라 전송 중 변경되지 않을 파일이나 그와 동등한 읽기 계약이 필요합니다.
앱이 준비한 파일은 카메라 롤의 원본을 대체하지 않습니다. 업로드를 일관되게 수행하기 위한 작업 사본입니다.
서버의 미디어 생성과 바이트 수신을 나눕니다
원본은 업로드 전용 경로를 통해 객체 저장소로 전달합니다. 라이브러리 조회 API가 모든 영상 바이트를 중계하도록 만들지 않겠습니다.
그러나 객체 저장소에 파일이 생겼다고 사용자 라이브러리에 항목을 확정하지도 않습니다.
바이트 수신
부분 데이터 보관과 무결성 확인
미디어 확정
필수 자원 연결과 계정 권한, 저장 공간 반영
파생 처리
썸네일, 재생용 사본과 검색 데이터 생성이 경계를 나누면 긴 동영상의 썸네일 생성이 지연되어도 원본 보관 여부를 확인할 수 있습니다.
반대로 이미지 미리보기 하나만 만들어졌는데 원본 백업까지 끝난 것으로 표시하지 않습니다.
다른 기기의 변경은 카메라 롤 원본과 직접 합치지 않습니다
클라우드 미디어의 보관과 휴지통 상태는 서버 라이브러리에서 관리합니다.
기기에서는 서버 미디어 ID와 로컬 자산의 연결을 유지합니다. 다른 기기에서 삭제한 항목을 발견하면 해당 클라우드 연결의 상태를 갱신합니다.
그 사실만으로 현재 기기의 운영체제 사진 라이브러리를 무조건 삭제하지는 않습니다. 클라우드 변경과 로컬 원본 변경은 별도의 권한과 제품 정책을 갖습니다.
UI & State Model
업로드 중 하나로 모든 대기를 표현하지 않습니다
사용자에게 같은 진행 표시가 보여도 내부 상태는 다를 수 있습니다.
Source availability
accessible / cloud_only / permission_missing / removed / unknown
Preparation
not_started / exporting / ready / source_changed
Transfer
queued / uploading / paused / retryable_failure
Wait reason
network / cellular_budget / battery / thermal / system / storage
Cloud preservation
not_committed / verifying / committed / trashed
Presentation
preview_pending / preview_ready원본이 다른 클라우드에 있어 내려받는 중이라면 업로드 전송률만으로 진행을 설명할 수 없습니다.
운영체제가 실행 시간을 주지 않은 상황과 서버가 오류를 반환한 상황도 다릅니다. 대기 상태를 실패로 표시하면 사용자는 효과가 없는 재시도를 반복하게 됩니다.
전체 진행률과 항목별 완료를 구분합니다
동영상의 바이트 전송이 끝났다면 서버 확인 중으로 전환합니다. 아직 미디어가 확정되지 않았다면 완료라고 표시하지 않습니다.
또한 항목 수 기준 진행률과 바이트 기준 진행률은 다릅니다. 작은 사진 99장과 큰 동영상 한 개 중 사진만 완료되었다면 항목 수는 거의 끝났어도 대부분의 바이트가 남아 있을 수 있습니다.
제한된 접근 권한에서는 제한된 완료를 표시합니다
앱이 사진 20개에만 접근할 수 있다면 그 20개를 모두 백업했다고 전체 카메라 롤을 백업했다고 표현해서는 안 됩니다.
완료 상태에는 대상 계정과 접근 가능한 범위가 포함되어야 합니다.
Selected items backed up
Not equivalent to
Entire device library backed up새 사진이 계속 생기므로 전체 완료 역시 특정 관측 시점까지의 상태입니다. 최근 라이브러리 확인 시점과 아직 인덱싱하지 못한 변경이 있는지를 구분합니다.
Core Entities & Data Modeling
로컬 항목과 클라우드 미디어를 같은 ID로 취급하지 않습니다
asset_key
앱의 로컬 인덱스가 사용하는 자산 식별자입니다.
platform_locator
PhotoKit 식별자나 MediaStore URI처럼 운영체제의 자산을 다시 찾는 참조입니다.
library_epoch
현재 사진 라이브러리 또는 MediaStore 인덱스의 식별 범위를 구분합니다. 라이브러리 재구성 이후 과거 참조를 그대로 신뢰하지 않기 위해 사용합니다.
source_revision
앱이 관측한 콘텐츠 변경 세대입니다. 파일 바이트의 해시를 대신하는 값은 아닙니다.
representation_profile
원본 보존, 현재 편집본 포함 여부와 변환 규칙을 나타냅니다.
ingestion_id
서버가 관리하는 한 미디어 반입 작업입니다.
upload_session_id
개별 자원의 부분 전송과 재개 상태를 관리하는 서버 식별자입니다.
media_id
서버 라이브러리에 확정된 논리 미디어 항목입니다.
media_version
클라우드 미디어의 현재 내용과 보관 상태를 비교하는 버전입니다.
하나의 로컬 자산이 여러 파일을 가질 수 있고, 하나의 서버 미디어가 여러 기기의 로컬 자산과 연결될 수도 있습니다.
로컬 ID는 그 기기에서 항목을 찾는 근거로 사용합니다. 다른 기기에 같은 사진이 존재한다는 사실은 별도의 연결 정보와 콘텐츠 확인으로 판단합니다.
로컬 스키마는 재개할 대상을 보존합니다
아래는 제안하는 SQLite 스키마의 일부입니다.
PRAGMA foreign_keys = ON;
CREATE TABLE local_assets (
asset_key TEXT PRIMARY KEY,
library_epoch TEXT NOT NULL,
platform_locator TEXT NOT NULL,
source_revision INTEGER NOT NULL CHECK (source_revision > 0),
metadata_revision INTEGER NOT NULL CHECK (metadata_revision > 0),
access_state TEXT NOT NULL,
UNIQUE (library_epoch, platform_locator)
);
CREATE TABLE backup_jobs (
job_id TEXT PRIMARY KEY,
account_id TEXT NOT NULL,
account_generation INTEGER NOT NULL,
asset_key TEXT NOT NULL REFERENCES local_assets(asset_key),
source_revision INTEGER NOT NULL,
representation_profile TEXT NOT NULL,
intent_revision INTEGER NOT NULL,
idempotency_key TEXT NOT NULL,
ingestion_id TEXT,
cloud_media_id TEXT,
state TEXT NOT NULL,
wait_reason TEXT,
priority INTEGER NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
UNIQUE (account_id, idempotency_key),
UNIQUE (
account_id, asset_key, source_revision,
representation_profile, intent_revision
)
);
CREATE TABLE resource_uploads (
resource_upload_id TEXT PRIMARY KEY,
job_id TEXT NOT NULL REFERENCES backup_jobs(job_id),
resource_role TEXT NOT NULL,
resource_ordinal INTEGER NOT NULL,
expected_bytes INTEGER NOT NULL CHECK (expected_bytes >= 0),
sha256 TEXT NOT NULL,
staging_locator TEXT,
server_session_id TEXT,
session_generation INTEGER NOT NULL DEFAULT 1,
UNIQUE (job_id, resource_role, resource_ordinal)
);
CREATE TABLE uploaded_parts (
resource_upload_id TEXT NOT NULL
REFERENCES resource_uploads(resource_upload_id),
session_generation INTEGER NOT NULL,
part_number INTEGER NOT NULL CHECK (part_number >= 0),
stored_bytes INTEGER NOT NULL CHECK (stored_bytes > 0),
part_sha256 TEXT NOT NULL,
storage_receipt TEXT NOT NULL,
PRIMARY KEY (
resource_upload_id, session_generation, part_number
)
);created_at_ms는 작업이 처음 대기한 시점입니다. 재시도할 때마다 바꾸면 얼마나 오래 기다렸는지 알기 어렵습니다.
intent_revision은 사용자가 취소한 작업을 명시적으로 다시 시작하는 등 새로운 의도를 구분합니다. 단순 네트워크 재시도에서는 변경하지 않습니다.
session_generation은 만료된 업로드 세션과 새 세션을 구분합니다. 이전 세션에서 확인한 부분 목록을 새 세션에 그대로 적용하지 않도록 합니다.
이 스키마에는 실제 이미지 바이트를 넣지 않습니다. 전송 상태와 바이트를 찾는 참조를 보관합니다.
발견한 항목과 대기 작업은 함께 기록합니다
라이브러리에서 신규 사진을 발견한 뒤 인덱스만 갱신하고 앱이 종료될 수 있습니다.
변경 확인 위치는 앞으로 갔는데 업로드 작업이 없다면 이후 그 사진을 다시 발견하지 못할 수 있습니다.
따라서 확인한 변경에 대한 인덱스 갱신, 필요한 작업 생성과 라이브러리 체크포인트를 같은 로컬 저장 경계에서 처리합니다.
이 체크포인트는 라이브러리 변경을 처리한 위치입니다. 업로드 완료 위치가 아닙니다.
라이브러리 인덱싱이 끝났다고 서버에 사진이 저장되었다는 뜻으로 사용하지 않습니다.
RDBMS에서 제외할 데이터
원본 사진과 동영상은 일반 업무 테이블에 큰 바이너리로 넣지 않겠습니다. 단말에서는 보호된 작업 파일, 서버에서는 객체 저장소를 사용합니다.
썸네일은 다시 만들 수 있는 표시 캐시입니다. 반면 업로드 중인 작업 사본은 동일한 바이트로 재개하는 데 필요할 수 있습니다. 둘을 같은 LRU 삭제 정책에 넣지 않습니다.
사진 전체를 디코딩한 비트맵과 영상 프레임도 전송 상태의 원본으로 보관하지 않습니다. 원본을 전송하는 경로는 바이트 스트림과 파일을 사용하고, 화면에는 필요한 크기의 썸네일만 준비합니다.
높은 빈도의 전송 진행률은 분석과 UI 표시용입니다. 복구에 필요한 서버 수신 확인과 미디어 확정 결과는 영속적으로 저장하되, 네트워크 콜백 하나마다 업무 DB를 갱신할 필요는 없습니다.
API Signatures
아래 API는 이 글에서 제안하는 반입 계약입니다. Google Photos의 비공개 업로드 API가 아닙니다.
복합 미디어의 업로드 계획을 만듭니다
다음 예시는 정지 이미지 4MiB와 동영상 12MiB로 구성된 Live Photo를 가정합니다.
POST /v1/media-ingestions
Authorization: Bearer <access-token>
Idempotency-Key: ingest-asset81-revision7
Content-Type: application/json
{
"client_asset_key": "asset-81",
"source_revision": 7,
"representation_profile": "original_bundle_v1",
"resources": [
{
"client_resource_id": "still-7",
"role": "original_photo",
"byte_length": 4194304,
"sha256": "<sha256-of-still-resource>"
},
{
"client_resource_id": "motion-7",
"role": "paired_video",
"byte_length": 12582912,
"sha256": "<sha256-of-motion-resource>"
}
]
}{
"ingestion_id": "ingestion-81",
"state": "prepared",
"part_size_bytes": 8388608,
"resource_sessions": [
{
"client_resource_id": "still-7",
"upload_session_id": "upload-still-81"
},
{
"client_resource_id": "motion-7",
"upload_session_id": "upload-motion-81"
}
]
}서버는 계정과 저장 한도, 지원 자원 형식과 요청의 멱등성을 확인합니다.
같은 키와 같은 자원 목록이면 기존 반입 작업을 반환합니다. 같은 키로 다른 해시나 보존 정책을 보내면 기존 작업에 섞지 않고 충돌로 처리합니다.
이 단계의 prepared는 전송 계획이 준비되었다는 뜻입니다. 아직 백업 완료가 아닙니다.
부분 데이터는 같은 세션과 위치에 연결합니다
PUT /v1/uploads/upload-motion-81/parts/0
Authorization: Bearer <scoped-upload-token>
Content-Type: application/octet-stream
Content-Length: 8388608
X-Part-SHA256: <sha256-of-part-0>
<exact bytes from the staged motion resource>이 설계에서는 같은 세션의 같은 부분 번호에 같은 바이트가 다시 오면 기존 수신 결과에 연결합니다.
같은 번호에 다른 바이트가 오면 덮어쓰지 않습니다. 원본 버전이 바뀌었거나 잘못된 파일을 연결했을 수 있기 때문입니다.
응답에는 해당 부분의 저장 크기와 검증 결과, 저장소 수신 확인값을 포함합니다. 구현에서 ETag를 사용할 수는 있지만 ETag가 항상 전체 파일의 콘텐츠 해시인 것은 아닙니다. S3도 업로드 방식 등에 따라 ETag와 체크섬의 의미를 구분하므로, 명시적인 무결성 필드를 사용해야 합니다. AWS Documentation
중단 후에는 서버가 보관한 부분을 조회합니다
GET /v1/uploads/upload-motion-81
Authorization: Bearer <access-token>{
"upload_session_id": "upload-motion-81",
"state": "active",
"expected_bytes": 12582912,
"part_size_bytes": 8388608,
"stored_parts": [
{
"part_number": 0,
"stored_bytes": 8388608,
"verified": true
}
]
}클라이언트가 마지막으로 보낸 위치가 아니라 서버가 확인한 부분을 기준으로 남은 전송을 정합니다.
큰 파일에서 목록이 길어지면 이 조회도 페이지로 나눌 수 있습니다. 일부 목록만 받았는데 전체 상태를 확인했다고 판단하지 않도록 응답 범위를 명시합니다.
필수 자원이 준비되면 미디어 확정을 요청합니다
POST /v1/media-ingestions/ingestion-81/commit
Authorization: Bearer <access-token>
Idempotency-Key: commit-ingestion-81
Content-Type: application/json
{
"resource_manifest_hash": "<hash-of-canonical-resource-manifest>",
"expected_ingestion_version": 3
}서버는 부분 수신 목록만 믿고 완료하지 않습니다. 필수 자원의 길이와 전체 해시, 구성 관계와 사용 권한을 확인합니다.
검증에 시간이 필요하면 처리 중 상태와 조회 경로를 반환합니다. 완료 응답이 유실되면 같은 반입 작업을 조회하거나 같은 완료 키로 재시도합니다.
최종 결과에는 다음 정보가 포함됩니다.
{
"ingestion_id": "ingestion-81",
"state": "committed",
"media_id": "media-81",
"media_version": 1,
"representation_profile": "original_bundle_v1",
"resource_count": 2,
"stored_bytes": 16777216,
"preview_state": "processing"
}미리보기가 처리 중이어도 원본 보관은 확정될 수 있습니다. 그러나 필수 자원 하나가 빠진 상태에서는 이 완료 결과를 반환하지 않습니다.



