사이트 이전

이 페이지

사이트 패키지는 EmDash 사이트의 콘텐츠 모델, 콘텐츠, 편집 이력, 사이트 표시, 설정, 미디어 파일을 담은 이식 가능한 복사본입니다. 사이트 패키지를 가져오면 사이트를 다른 EmDash 배포로 옮길 수 있으며, SQLite, PostgreSQL, Cloudflare D1처럼 다른 데이터베이스를 쓰는 배포도 가능합니다.

가져오기는 콘텐츠 영역이 비어 있는 새 사이트에 기록합니다. EmDash는 무엇을 쓰기 전에 패키지 전체를 검사하고, 재개 가능한 작은 단계로 가져오기를 실행하며, 가져온 사이트를 다시 읽어 결과가 패키지와 일치하면 영수증을 발급합니다.

사이트 패키지에는 사용자, 인증 정보, 시크릿이 포함되지 않습니다. 다만 사이트의 모든 항목과 댓글은 포함되며, 작성자와 댓글 작성자의 이메일 주소도 포함됩니다. 데이터베이스 백업과 같은 주의로 보관하고 전달하세요.

알맞은 복사 방식 고르기

메커니즘목적가져오기 가능미디어 파일사용자와 시크릿
시드 파일콘텐츠 모델과 샘플 콘텐츠 초기 구축예, 시드 의미론으로아니요아니요
미리보기 스냅샷격리된 미리보기 렌더링 채우기미리보기만아니요아니요
JSON 백업선택한 데이터베이스 형태 상태 검사아니요아니요아니요
원시 데이터베이스 및 미디어 백업한 배포 복구같은 종류의 데이터베이스로 복원별도 복사예
사이트 패키지사이트를 다른 EmDash 사이트로 이동예, 빈 사이트로예아니요. 작성자 이름과 이메일만

데이터 손실 후 배포를 복구하려면 원시 데이터베이스 백업을 사용하세요. 다른 곳에 사이트의 새 복사본을 만들려면 사이트 패키지를 사용하세요.

사이트 패키지에 포함되는 내용

사이트 패키지에는 다음이 포함됩니다.

  • 컬렉션, 필드, 모든 버전의 블록 타입, 택소노미 정의, 관계 정의, 바이라인 필드 정의
  • 초안, 예약 항목, 휴지통 항목, 리비전 이력, 번역 그룹을 포함한 모든 로케일의 모든 콘텐츠 항목
  • 택소노미 용어와 용어 할당, 바이라인과 크레딧, 콘텐츠 참조, SEO 기록
  • 메뉴와 메뉴 항목, 위젯 영역과 위젯, 섹션, 리디렉트
  • 내보내기에서 댓글을 끄지 않은 경우 댓글과 댓글 반응
  • 미디어 폴더, 미디어 메타데이터, 준비된 모든 미디어 파일의 바이트
  • 아래에 나열된 이식 가능한 사이트 설정

패키지는 JSON 필드와 Portable Text 같은 JSON 값을 객체 키를 정렬한 상태로 저장합니다. 따라서 가져온 값은 원본과 키 순서가 다를 수 있습니다. 그 외 값은 변경되지 않습니다.

이식 가능한 설정

다음 설정만 내보내집니다: site:title, site:tagline, site:logo, site:favicon, site:postsPerPage, site:dateFormat, site:timezone, site:social, site:seo, emdash:site_title, emdash:site_tagline, emdash:locale.

대상 사이트는 자체 사이트 URL(site:url과 emdash:site_url), 사이트 ID, 설정 상태, 백업 설정을 유지합니다. 가져오기는 이를 덮어쓰지 않습니다.

가져오기 계획은 설정 마법사가 쓴 대상의 제목과 태그라인을 유지할지, 패키지 값을 사용할지 묻습니다. 기본값은 패키지 값입니다.

프린시펄

사용자 계정은 패키지와 함께 이동하지 않습니다. 콘텐츠, 리비전, 미디어, 바이라인, 댓글이 참조하는 각 원본 사용자에 대해 패키지는 프린시펄을 담습니다: 사용자 ID, 표시 이름, 이메일 주소. 프린시펄에는 역할, 비밀번호, 패스키, 세션, 토큰이 없습니다.

가져오기 중에 각 프린시펄을 대상 사이트의 사용자에 매핑하거나 매핑하지 않은 채로 둘 수 있습니다. 작성자를 대상 사용자에 매핑을 참고하세요.

댓글

댓글에는 작성자 이름과 이메일, 본문, 상태, 스레딩, 타임스탬프, 모더레이션 메타데이터가 포함됩니다. IP 주소 해시와 사용자 에이전트는 내보내지 않습니다.

반응은 카운트를 유지합니다. 내보내기는 각 투표자 해시를 새 난수로 바꿔, 대상이 반응을 해당 방문자와 연결할 수 없게 합니다.

사이트 패키지에서 제외되는 내용

사이트 패키지에는 절대 포함되지 않습니다.

  • 사용자, 세션, 패스키, OAuth 계정, 허용 도메인, API 토큰, OAuth 클라이언트, 인가 코드, 디바이스 코드
  • 플러그인 스토리지, 플러그인 상태, 플러그인 시크릿을 포함한 플러그인 설정
  • 미리보기 서명 시크릿처럼 이식 가능한 설정 외의 설정
  • 감사 로그, 속도 제한, 편집 잠금, 예약 작업 상태, 404 로그, 마이그레이션 이력
  • 가져오기가 다시 구축하는 미디어 사용 기록과 검색 인덱스
  • 원본 스토리지 키, 버킷 이름, 데이터베이스 이름, 바인딩 이름
  • 미완료 업로드처럼 준비되지 않은 미디어

외부 미디어 제공자의 미디어는 외부에 남습니다. 패키지는 참조를 유지하지만 제공자의 파일은 복사하지 않습니다.

대상 사이트 준비

아래 요구 사항을 모두 충족하는 사이트에 가져오세요. 대상의 콘텐츠, 로케일, 업로드 한도, 지원 형식이 패키지에 맞지 않으면 분석이 차단 항목을 보고합니다.

  • 관리자 계정. 가져오기는 로그인한 관리자 또는 API 토큰으로 실행됩니다. 설정 중에 대상의 관리자를 만드세요.
  • 스토리지 백엔드. 원본과 대상 모두 스토리지가 구성되어 있어야 합니다. EmDash는 패키지 파일을 여기에 스테이징합니다.
  • 콘텐츠 없음. 대상에는 항목(휴지통 포함), 리비전, 미디어나 미디어 폴더, 바이라인이나 바이라인 필드, 댓글, 리디렉트, 용어 할당, 관계, SEO 기록, 관리자에서 만든 섹션, 설정 이후 만든 컬렉션이나 블록 타입이 없어야 합니다. 공식 템플릿으로 설정한 사이트는 적합합니다. 설정이 만든 것은 설정 스캐폴드입니다: 시드된 컬렉션과 블록 타입, 택소노미 정의와 미할당 용어, 메뉴와 항목, 위젯 영역과 위젯, 테마 섹션. 계획에 스캐폴드가 나열되며, 계획을 확인한 뒤 가져오기가 이를 제거합니다.
  • 패키지가 쓰는 모든 로케일. 패키지의 각 로케일을 대상의 i18n 구성에 추가하세요. i18n 구성이 없는 사이트는 en만 받습니다. 로케일은 대소문자 구분 없이 매칭되며, 가져오기는 대상에 구성된 대소문자로 쓰고 locale_recased로 선언합니다.
  • 충분한 업로드 한도. 모든 미디어 파일이 대상의 maxUploadSize에 맞아야 하며, 기본값은 50 MiB입니다.
  • 형식 버전 1. 대상이 패키지 형식 버전과 필요한 모든 기능을 지원해야 합니다.

다음 요청은 지원 형식 버전, 기능, 한도를 반환합니다. portableDomain 객체는 사이트가 가져오기를 받을 수 있는지, 불가 시 이유를 보고합니다.

curl https://new.example.com/_emdash/api/admin/transfer/capabilities \
  -H "Authorization: Bearer $EMDASH_TOKEN"

사이트 내보내기

내보내기는 사이트를 제한된 단계로 읽고 패키지를 사이트의 스토리지에 씁니다. 내보내기가 끝나기 전에, 내보내기는 가져오기와 같은 방식으로 완성된 패키지를 검증합니다. 내보내기 중 사이트에 쓰기가 성공하면 내보내기는 다시 시작합니다. 항목 편집 잠금을 잡거나 갱신하는 것은 쓰기로 치지 않습니다. 세 번 시도한 뒤에는 TRANSFER_EXPORT_CONCURRENT_WRITES로 실패합니다.

내보내기 파일은 내보내기 생성 후 7일간 사용할 수 있습니다. 그 이후 다운로드는 TRANSFER_EXPIRED를 반환합니다.

관리자에서 내보내기

  1. Settings → Transfer를 엽니다. 관리자에게만 표시됩니다.

  2. Export 섹션에서 Include comments를 끄면 댓글과 반응이 제외됩니다.

  3. Export site를 선택합니다. 페이지에 진행 상황이 표시됩니다. 페이지를 열어 두세요. 떠나더라도 돌아오면 내보내기가 이어집니다.

  4. Export ready가 나타나면 Download package를 선택하고 .emdash 파일을 저장할 위치를 고릅니다. 다운로드된 파일 수와 바이트가 표시되며, Stop은 다운로드를 취소합니다.

이 섹션에는 패키지 다이제스트, 종류별 레코드 수, 사이트의 최근 내보내기도 표시되며, 만료 전까지 각각 다운로드 버튼이 있습니다.

Download package는 내보내기를 파일 단위로 가져와 각 파일의 크기와 SHA-256 다이제스트를 매니페스트와 대조하고, 브라우저에서 .emdash 파일을 만듭니다. 그래서 Cloudflare Workers에서도 사이트 크기에 관계없이 동작합니다. 파일이 맞지 않으면 오류로 중단됩니다. Chrome, Edge 등 Chromium 기반 브라우저는 디스크에 바로 씁니다. 다른 브라우저는 다운로드가 끝날 때까지 패키지 전체를 메모리에 둡니다. 약 500 MB보다 큰 내보내기에는 Chromium 기반 브라우저나 CLI를 권장합니다.

Download as one file는 서버에 단일 응답으로 아카이브를 요청합니다. 작은 사이트에 적합합니다. Cloudflare Workers에서는 큰 사이트가 단일 요청 한도를 넘을 수 있습니다.

CLI로 내보내기

원본 사이트에 로그인한 뒤 패키지 파일로 내보내세요.

npx emdash login --url https://example.com
npx emdash site export --url https://example.com --output site.emdash

명령은 내보내기를 완료까지 진행하고, 패키지를 파일 단위로 다운로드하며, 모든 파일의 크기와 다이제스트를 검사한 뒤 site.emdash를 씁니다. --no-comments를 추가하면 댓글과 반응이 제외됩니다. 중단되면 같은 옵션으로 다시 실행해 같은 내보내기를 재개하세요. emdash site export 참고를 보세요.

REST API로 내보내기

advance를 호출할 때마다 한 단계가 실행되고, 다음 호출까지 대기할 nextRequestInMs가 반환됩니다. nextRequestInMs가 null이면 내보내기가 끝난 것입니다.

이 예시는 transfer:export 스코프가 있는 개인 액세스 토큰을 사용합니다. 토큰 스코프를 참고하세요.

  1. 내보내기를 시작합니다. 댓글과 반응을 제외하려면 본문으로 { "comments": false }를 보냅니다. Idempotency-Key 헤더는 재시도 시 새 내보내기를 시작하지 않고 같은 내보내기를 반환합니다. 다른 옵션으로 같은 키를 재사용하면 409 TRANSFER_IDEMPOTENCY_CONFLICT로 실패합니다.

    curl -X POST https://example.com/_emdash/api/admin/transfer/exports \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      -H "Idempotency-Key: move-to-new-host"
  2. nextRequestInMs가 null이 될 때까지 내보내기를 진행합니다. 반환된 밀리초만큼 기다린 뒤 호출하세요. operation.progress는 done과 total 단계, 지금까지 쓴 records, 패키지 크기를 알면 bytesDone과 bytesTotal을 보고합니다.

    curl -X POST https://example.com/_emdash/api/admin/transfer/exports/$EXPORT_ID/advance \
      -H "Authorization: Bearer $EMDASH_TOKEN"
  3. operation.state가 complete인지 확인합니다. failed 내보내기는 operation.errorCode에 이유를 담습니다.

  4. 패키지를 하나의 .emdash 파일로 다운로드합니다.

    curl -o site.emdash \
      https://example.com/_emdash/api/admin/transfer/exports/$EXPORT_ID/archive \
      -H "Authorization: Bearer $EMDASH_TOKEN"

.emdash 파일은 첫 항목이 manifest.json인 비압축 tar 아카이브입니다. 아카이브는 모든 파일을 한 응답으로 스트리밍합니다. Cloudflare Workers에서는 큰 사이트가 단일 요청 한도를 넘을 수 있습니다. 대신 exports/{id}/manifest에서 manifest.json을, exports/{id}/files/{path}에서 각 파일을 다운로드하세요. 다운로드되는 모든 파일은 스트리밍 중 기록된 다이제스트와 대조됩니다. 내보내기 이후 저장된 바이트가 바뀌면 완료 대신 오류로 끝납니다.

사이트 가져오기

가져오기는 패키지에서 생성되고, 계획으로 분석되며, 해당 다이제스트로 계획을 확인한 뒤에만 실행됩니다. 실행을 시작하지 않은 가져오기는 생성 후 24시간이 지나면 만료됩니다.

관리자, CLI, REST API로 모든 단계를 실행할 수 있습니다. AI 에이전트는 이미 업로드된 가져오기를 MCP 도구로 분석하고 시작할 수 있습니다.

관리자에서 가져오기

  1. 대상 사이트에서 Settings → Transfer를 엽니다. 사이트가 가져오기를 받을 수 있으면 Import 섹션이 나타납니다. 그렇지 않으면 가져오기를 막는 기존 내용이 나열됩니다.

  2. Choose package file을 선택하고 .emdash 파일을 고릅니다. 브라우저가 패키지를 검사하고 분할 업로드합니다. 업로드 중에는 사이트가 바뀌지 않습니다. 업로드가 멈추면 같은 파일을 다시 선택해 이어서 올리세요.

  3. 업로드가 끝나면 사이트가 패키지를 분석합니다. 페이지를 떠나도 다시 올 수 있습니다.

  4. 가져오기를 검토하세요: 원본 사이트, 내보내기 날짜와 EmDash 버전, 크기, 패키지 다이제스트, 종류별 레코드 수. Blockers와 Warnings, 계획의 변환을 나열하는 Differences from the source site, 유형별로 묶인 Starter content that will be removed를 읽으세요. 가져오기 계획 검토를 참고하세요.

  5. Authors에서 각 작성자 콘텐츠를 소유할 이 사이트의 사용자를 고르거나 Don’t map을 선택하세요. 사용자 이메일과 일치하는 작성자는 Matched by email로 표시됩니다. 작성자를 대상 사용자에 매핑을 참고하세요.

  6. Site identity에서 패키지의 사이트 제목과 태그라인을 쓸지, 이 사이트의 값을 유지할지 선택하세요.

  7. Start import를 선택하고 확인합니다. 계획에 차단 항목이 있으면 버튼이 비활성화됩니다. 가져오기가 끝날 때까지 사이트 편집이 일시 중지됩니다.

  8. 진행 상황을 따르세요. 가져오기가 완료되면 Verified 배지와 영수증, 패키지, 계획, 콘텐츠 다이제스트가 있는 영수증이 표시됩니다. Copy receipt로 영수증 JSON 복사본을 보관하세요.

페이지는 업로드부터 가져오기 완료까지 Cancel import를, 쓰기를 시작한 가져오기가 실패하거나 취소된 뒤에는 Abandon import도 제공합니다. 둘 다 확인을 요청합니다. 가져오기 취소와 미완료 가져오기 포기를 참고하세요.

CLI로 가져오기

대상 사이트에 로그인한 뒤 패키지를 분석하세요.

npx emdash login --url https://new.example.com
npx emdash site import site.emdash --url https://new.example.com --analyze

명령은 패키지 파일 전체를 로컬에서 검사하고, 업로드하며, 분석한 뒤 계획 다이제스트와 함께 계획을 출력합니다. 계획에 차단 항목이 있으면 종료 코드 2로 끝납니다. 가져오기 계획 검토에 따라 계획을 검토하세요.

계획의 결정을 바꾸려면 결정 플래그와 함께 --analyze를 다시 실행하세요. --map-principal은 ID나 이메일로 프린시펄을 대상 사용자(ID 또는 이메일) 또는 none에 매핑합니다. --use-target-title과 --use-target-tagline은 대상의 제목과 태그라인을 유지합니다.

npx emdash site import site.emdash --url https://new.example.com --analyze \
  --map-principal [email protected][email protected] \
  --map-principal 01J8ZQ4Y6T2N0D3VJ5R7K9M1PX=none \
  --use-target-title

검토한 계획의 다이제스트를 넘겨 실행하세요.

npx emdash site import site.emdash --url https://new.example.com \
  --plan sha256:3f1c… --confirm

명령은 가져오기를 완료까지 실행하고 영수증을 출력합니다. 중단되면 emdash site import resume <operation-id>로 이어가세요. emdash site import status <operation-id>는 상태를, emdash site import receipt <operation-id>는 영수증을 다시 출력합니다. emdash site import 참고를 보세요.

REST API로 가져오기

서버는 .emdash 아카이브가 아니라 패키지 안의 파일로 작업합니다. 먼저 아카이브를 푸세요. manifest.json, index/ 아래 인덱스 파일, records/ 아래 레코드 파일, media/ 아래 미디어 파일이 있습니다. 매니페스트는 모든 파일의 크기와 SHA-256 다이제스트를 고정하므로, 패키지 다이제스트가 전체 패키지를 식별합니다.

이 예시는 transfer:analyze와 transfer:execute 스코프가 있는 토큰을 사용합니다.

  1. 가져오기를 만듭니다. manifest.json의 변경되지 않은 바이트를 요청 본문으로 보냅니다. 응답에는 작업과 서버가 아직 필요로 하는 파일의 첫 페이지가 있습니다.

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      -H "Idempotency-Key: move-to-new-host" \
      --data-binary @site/manifest.json
  2. 누락된 모든 파일을 imports/{id}/files/{path}에 업로드합니다. Content-Length 헤더는 선언된 크기와 같아야 하고, 바이트는 선언된 다이제스트와 일치해야 합니다.

    curl -X PUT \
      https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/files/index/000000.ndjson \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      --data-binary @site/index/000000.ndjson

    인덱스 파일을 업로드하면 그에 나열된 레코드와 미디어 파일이 선언됩니다. 업로드 배치마다 imports/{id}/missing을 다시 요청하고, 항목이 없을 때까지 계속하세요.

    이미 저장된 파일을 다시 업로드하면 다시 검사합니다. 저장된 복사본이 더 이상 맞지 않으면 교체되고 응답은 alreadyVerified: false를 보고합니다.

  3. 패키지를 분석합니다. nextRequestInMs가 null이 될 때까지 imports/{id}/analyze를 호출하세요. 최종 응답에 plan과 planDigest가 있습니다.

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/analyze \
      -H "Authorization: Bearer $EMDASH_TOKEN"
  4. 계획을 검토하고 모든 차단 항목, 경고, 변환을 읽으세요. 가져오기 계획 검토를 참고하세요.

  5. 기본값이 원하지 않는 경우 결정을 제출하세요. 각 제출은 새 계획과 계획 다이제스트를 반환합니다. 실행이 요청되면 계획은 고정되고, 결정 제출은 409 TRANSFER_INVALID_STATE로 실패합니다.

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/analyze \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{ "decisions": { "principalMappings": { "01J8ZQ4Y6T2N0D3VJ5R7K9M1PX": null }, "siteTitle": "target" } }'
  6. 검토한 다이제스트로 가져오기를 시작합니다.

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/execute \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{ "packageDigest": "sha256:…", "planDigest": "sha256:…" }'
  7. nextRequestInMs가 null이 될 때까지 가져오기를 진행하며, 반환된 지연만큼 기다립니다.

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/advance \
      -H "Authorization: Bearer $EMDASH_TOKEN"
  8. operation.state가 complete인지 확인한 뒤 imports/{id}/receipt에서 영수증을 읽으세요.

다이제스트가 스테이징된 패키지나 현재 계획과 다르면 실행은 TRANSFER_PACKAGE_DIGEST_MISMATCH 또는 TRANSFER_PLAN_DIGEST_MISMATCH로 실패합니다. imports/{id}/plan에서 현재 계획을 읽고 다시 검토한 뒤 그 다이제스트로 재시도하세요.

작성자를 대상 사용자에 매핑

분석은 각 프린시펄의 표시 이름, 이메일, 참조하는 패키지 레코드 수를 나열합니다. 대소문자 구분 없이 같은 이메일을 가진 대상 사용자가 정확히 하나이면, 계획이 그 사용자를 제안하고 기본적으로 프린시펄을 매핑합니다. 제안이 없는 프린시펄은 매핑되지 않은 상태로 시작합니다.

매핑을 바꾸려면 emdash site import --analyze에 --map-principal을 넘기거나, analyze 엔드포인트에 principalMappings를 제출하세요. 각 매핑은 대상 사용자를 지정하거나 프린시펄을 매핑하지 않은 채로 둡니다(CLI에서는 none, API에서는 null). EmDash는 각 매핑을 항목 작성자, 리비전 작성자, 미디어 업로더, 바이라인 사용자 링크, 댓글 작성자에 적용합니다.

매핑되지 않은 프린시펄의 참조는 제거됩니다. 매핑되지 않은 작성자에게 계정에 연결된 바이라인도 있었다면, 가져오기는 명시적 바이라인 크레딧이 없고 해당 로케일에 작성자 바이라인이 있는 각 작성자 항목에 그 바이라인을 명시적으로 크레딧합니다. 따라서 작성자 크레딧이 페이지에 남습니다.

다음 두 매핑은 principal_conflict 차단 항목을 만듭니다.

  • 같은 로케일에 바이라인이 둘 이상인 프린시펄을 사용자에 매핑하는 경우
  • 같은 로케일에 바이라인이 있는 두 프린시펄을 같은 사용자에 매핑하는 경우

대상 사용자는 로케일당 바이라인을 하나만 가질 수 있습니다. 한 프린시펄을 매핑하지 않거나, 서로 다른 사용자에 매핑하세요.

가져오기 계획 검토

계획은 가져오기가 만들 것, 적용할 결정, 세 종류의 발견 항목을 나열합니다.

  • 차단 항목은 실행을 막습니다. 계획이 비워질 때까지 실행은 TRANSFER_PLAN_BLOCKED를 반환합니다. principal_conflict는 프린시펄 매핑을 바꿔 해결하세요. 다른 차단 항목은 패키지나 대상의 변경이 필요합니다. 가져오기를 취소하고 변경한 뒤 새 가져오기를 만드세요.
  • 경고는 가져오기를 막지 않는 패키지 문제를 설명합니다. 영수증에 복사됩니다.
  • 변환은 원본 사이트와 가져온 사이트 사이의 정확하고 선언된 차이입니다. 내보내기의 변경이 먼저, 그다음 가져오기의 변경이 나열됩니다. 검증은 가져온 사이트를 패키지와 비교할 때 가져오기 변환을 적용합니다.

계획은 차단 항목과 경고를 최대 500개까지 나열합니다. issues_truncated 경고는 추가로 발견된 수를 보고합니다.

차단 항목

코드의미
package_invalid패키지 파일이나 경로가 검증에 실패합니다.
unsupported_format대상이 패키지 형식이나 형식 버전을 지원하지 않습니다.
unsupported_feature패키지가 대상이 지원하지 않는 기능을 요구합니다.
limit_exceeded패키지 파일이나 레코드가 한도를 초과합니다.
file_missing선언된 패키지 파일이 업로드되지 않았습니다.
file_mismatch패키지 파일의 크기나 다이제스트가 선언과 다릅니다.
record_invalid레코드가 잘못되었거나 정규 JSON이 아닙니다.
record_count_mismatch어떤 종류의 레코드 수가 매니페스트와 다릅니다.
record_order_invalid레코드 순서가 잘못되었거나 부모가 자식 뒤에 나타납니다.
duplicate_id같은 종류의 두 레코드가 ID를 공유합니다.
dangling_reference레코드가 패키지에 없는 레코드를 참조합니다. 패키지에 없는 블록 타입을 가리키는 blocks 필드, 현재 버전이 패키지에 없는 블록 타입도 포함합니다.
reference_cycle용어, 댓글, 메뉴 항목이 자기 자신의 부모입니다.
media_ref_invalid콘텐츠가 패키지에 없는 미디어 레코드를 참조합니다.
media_blob_missing미디어 레코드의 파일이 패키지에 없습니다.
media_blob_too_large미디어 파일이 대상의 maxUploadSize보다 큽니다.
target_not_empty대상에 이미 콘텐츠가 있습니다. 차단 항목의 detail이 발견한 내용을 이름 짓습니다.
locale_not_configured패키지가 대상 i18n 구성에 없는 로케일을 사용합니다.
field_type_unknown필드나 바이라인 필드가 대상이 지원하지 않는 타입을 사용합니다.
principal_conflict프린시펄 매핑이 한 사용자에게 같은 로케일의 바이라인을 둘 줍니다.
integer_out_of_range정수가 대상 데이터베이스의 정수 범위를 벗어납니다. PostgreSQL은 정수를 32비트로 저장합니다.
value_constraint_violation관리자 API가 거부할 값입니다. 아래 목록을 보세요.
unique_violation레코드가 대상에서 다른 레코드의 고유 키를 중복합니다.

가져오기는 레코드를 직접 쓰므로, 분석은 해당 레코드를 저장할 때 관리자 API가 적용하는 것과 같은 검사를 적용합니다. 다음 값은 각각 value_constraint_violation입니다.

  • 필드 열에 맞지 않는 항목 값, 값이 없는 필수 필드, 컬렉션에 없는 필드에 대한 값
  • 소스나 대상이 사이트의 경로가 아니거나, 타입이 지원되지 않거나, 소스 패턴이 잘못되었거나, 대상이 소스가 캡처하지 않는 매개변수를 쓰는 리디렉트
  • http나 https URL이 아닌 바이라인 웹사이트, 필드 타입이나 선택지에 맞지 않는 바이라인 필드 값, 사이트가 지원하는 것보다 많은 선택지가 있는 바이라인 필드
  • 잘못된 컬렉션 URL 패턴
  • 예약된 슬러그, 비어 있거나 200자를 넘는 레이블, 블록 타입 편집기가 거부할 필드 정의가 있는 블록 타입
  • http나 https URL도 사이트 경로도 아닌 SEO 정규 URL
  • 메뉴가 허용하지 않는 스킴의 메뉴 항목 URL

경고

코드의미
media_provider_external콘텐츠가 외부 제공자 미디어를 사용합니다. 참조는 유지되고 파일은 복사되지 않습니다.
media_row_missing설정이 패키지에 없는 미디어를 참조합니다.
soft_reference_dangling선택적 참조가 패키지의 레코드로 해석되지 않습니다.
redirect_loops_unchecked패키지에 리디렉트가 너무 많아 가져오기 전 루프를 검사하지 않습니다. 루프를 닫는 리디렉트는 비활성으로 가져옵니다.
issues_truncated계획이 나열하는 것보다 많은 차단 항목이나 경고가 발견되었습니다.

변환

내보내기는 원본 사이트 데이터에 적용한 변경을 선언합니다. 다음 변환은 각각 레코드 종류와 개수를 담습니다.

코드의미
orphan_dropped원본 사이트에 부모가 더 이상 없는 레코드가 제외됨. 예: 삭제된 항목의 리비전.
soft_orphan_dropped누락된 레코드로의 링크가 제외됨. 예: 삭제된 용어에 대한 용어 할당, 삭제된 항목을 가리키는 메뉴 항목.
orphan_reference_nulled누락된 레코드에 대한 참조가 제거됨. 예: 미디어 파일의 삭제된 폴더.
avatar_nulled패키지에 없는 미디어를 가리키는 바이라인 아바타나 섹션 미리보기 이미지가 제거됨.
media_not_ready_dropped미완료 업로드처럼 준비되지 않은 미디어가 제외됨.
media_ref_unlinked패키지에 없는 미디어에 대한 참조가 콘텐츠에서 제거됨.
media_url_relativized원본 사이트 자체 미디어 파일의 절대 URL이 대상에서 해석되는 사이트 상대 URL로 변환됨.
redirect_duplicate_dropped같은 소스 경로의 중복 리디렉트가 제외됨. 소스 경로당 하나만 유지됨.
unknown_storage_key원본 사이트에 없는 미디어 파일을 여전히 참조하는 레코드. 변경 없이 내보내짐.

가져오기는 자체 변경을 선언합니다.

코드의미
principal_mapped프린시펄 참조가 매핑된 대상 사용자로 다시 쓰임.
principal_unmapped매핑되지 않은 프린시펄에 대한 참조가 제거됨.
seeded_scaffold_removed가져오기가 쓰기 전에 대상의 설정 스캐폴드가 삭제됨. 계획이 각 항목을 나열함.
redirect_loop_disabled루프를 형성하는 리디렉트가 비활성으로 가져와짐.
search_unsupported대상이 PostgreSQL을 쓰므로 나열된 컬렉션의 검색이 꺼짐.
float4_rounded소수 값이 대상 PostgreSQL real 열의 정밀도로 반올림됨.
locale_recased로케일이 대상에 구성된 대소문자로 쓰임. 예: pt-br → pt-BR.

가져오기 실행

실행은 다음 단계를 순서대로 진행합니다.

  1. 대상을 예약하고 비어 있는지 다시 확인합니다.
  2. 계획에 나열된 설정 스캐폴드를 제거합니다.
  3. 블록 타입, 컬렉션, 필드, 택소노미 정의, 관계 정의, 바이라인 필드를 만듭니다.
  4. 미디어 파일을 대상 스토리지에 복사하고 미디어 레코드를 만듭니다.
  5. 용어와 바이라인을 씁니다.
  6. 리비전과 항목을 씁니다.
  7. 용어 할당, 바이라인 크레딧, 콘텐츠 참조, SEO 기록을 씁니다.
  8. 메뉴, 위젯, 섹션, 리디렉트, 댓글, 반응, 설정을 씁니다.
  9. 검색 인덱스와 캐시를 다시 구축하고 미디어 사용 재인덱싱을 큐에 넣습니다.
  10. 결과를 검증합니다.

각 advance 호출은 D1의 Cloudflare Workers 요청 한도에 맞는 하나의 제한된 단계를 실행합니다. 진행 상황은 서버에 저장됩니다. 중단된 요청은 진행 중인 단계만 잃을 수 있으며, 모든 쓰기는 멱등이므로 단계를 다시 실행해도 레코드가 중복되지 않습니다.

다른 요청이 단계를 실행 중이거나 단계 중에 작업을 가져가면, advance는 짧은 nextRequestInMs와 함께 작업을 반환합니다. 스토리지나 데이터베이스 오류는 재시도됩니다. 작업이 오류를 기록하고 nextRequestInMs는 연속 실패마다 커집니다. 진행 없이 반복 실패하면 가져오기가 실패합니다.

가져오기 중 쓰기 차단

첫 실행 단계부터 가져오기 완료까지 EmDash는 API 쓰기 요청을 503 TRANSFER_IMPORT_IN_PROGRESS로 거부합니다. 관리자, REST API, 플러그인 라우트, 공개 댓글 제출, 예약 게시, 플러그인 콘텐츠 쓰기가 해당됩니다. 로그인, 사용자 및 API 토큰 관리, 항목 편집 잠금, 전송 API 자체는 계속 사용할 수 있습니다. 읽기 요청은 차단되지 않습니다.

플러그인 MCP 도구를 포함한 MCP 쓰기 도구는 일반적인 도구 오류로 TRANSFER_IMPORT_IN_PROGRESS와 함께 실패합니다. 읽기 전용 MCP 도구와 site_* 전송 도구는 계속 동작하므로, MCP로 시작한 가져오기를 MCP로 재개·검사·완료할 수 있습니다.

중단 후 재개

관리자 페이지는 열려 있는 동안에만 가져오기를 진행합니다. 재개하려면 Settings → Transfer를 다시 열거나, emdash site import resume <operation-id>를 실행하거나, 같은 작업에 advance를 다시 호출하세요. 서버는 마지막으로 완료된 단계부터 이어갑니다. 중단된 요청이 아직 작업을 잡고 있으면, 다음 호출은 그 홀드가 만료될 때까지 기다리며 최대 5분입니다.

실패하거나 취소된 가져오기는 재개할 수 없습니다.

가져오기 취소

Settings → Transfer에서 Cancel import를 선택하거나, emdash site import cancel <operation-id>를 실행하거나, POST imports/{id}/cancel을 보내세요. 진행 중인 단계는 현재 배치 후에 멈춥니다. 취소는 이미 쓴 레코드를 제거하지 않습니다.

미완료 가져오기 포기

쓰기를 시작한 실패·취소된 가져오기는 쓰기를 계속 막아, 미완료 사이트가 실수로 편집되지 않게 합니다. 차단을 해제하려면 Settings → Transfer에서 Abandon import를 선택하거나, emdash site import abandon <operation-id>를 실행하거나, POST imports/{id}/abandon을 보내세요. 포기는 가져온 데이터를 유지합니다.

포기 후에는 사이트가 더 이상 비어 있지 않아 다른 가져오기를 받을 수 없습니다. 대신 새로 설정한 사이트에 가져오세요.

쓰기를 시작하지 않은 실패·취소된 가져오기는 쓰기를 막지 않으며 포기할 필요가 없습니다.

결과 검증

검증은 내보내기가 쓰는 것과 같은 코드로 가져온 모든 레코드를 다시 읽고, 계획의 선언된 변환을 패키지 레코드에 적용한 뒤 둘을 비교합니다. 또한 모든 종류의 레코드 수를 확인하고, 가져온 모든 미디어 파일을 다시 다운로드해 다이제스트를 검사합니다. 차이가 있으면 TRANSFER_VERIFICATION_FAILED로 가져오기가 실패합니다. 작업의 errorDetail은 차이를 최대 50개까지 나열합니다.

성공한 가져오기는 영수증을 만듭니다.

{
	"operationId": "01J8ZR2C4S6D8F0G2H4J6K8M0N",
	"packageDigest": "sha256:…",
	"planDigest": "sha256:…",
	"targetSiteId": "01J8ZR0A2B4C6D8E0F2G4H6J8K",
	"originSiteId": "01J1A3C5E7G9J1L3N5Q7S9U1W3",
	"formatVersion": "1",
	"importerEmDashVersion": "0.38.0",
	"completedAt": "2026-09-23T10:15:00.000Z",
	"logicalDigest": "sha256:…",
	"counts": { "entry": 412, "media": 96 },
	"warnings": [],
	"verification": "verified",
	"receiptDigest": "sha256:…"
}

영수증은 검증이 끝났을 때 targetSiteId로 식별된 대상 사이트가 planDigest로 식별된 계획 적용 후 packageDigest로 식별된 패키지의 콘텐츠를 정확히 가지고 있었음을 기록합니다. logicalDigest는 검증된 레코드를 요약합니다.

receiptDigest는 receiptDigest 속성을 제거한 영수증 정규 JSON의 SHA-256 다이제스트입니다. 발급 후 변경된 영수증을 감지합니다. 영수증은 서명되지 않으므로 어느 서버가 발급했는지 증명하지 않습니다. 그 점이 중요하면 인증된 연결로 대상에서 영수증을 가져오세요.

영수증은 검증이 끝난 순간의 사이트를 설명합니다. 이후 편집에 대해서는 말하지 않습니다.

데이터베이스 간 이동

패키지는 원본 데이터베이스에 의존하지 않습니다. SQLite, PostgreSQL, D1에서 내보내 그중 어디에든 가져올 수 있습니다. 대상이 PostgreSQL일 때 다음 차이를 계획하세요.

  • PostgreSQL은 정수를 32비트로 저장합니다. 그 범위를 벗어난 정수는 integer_out_of_range 차단 항목입니다.
  • PostgreSQL은 number 필드와 미디어 초점점을 32비트 부동소수점으로 저장합니다. 바뀌는 값은 float4_rounded로 선언되며, 검증은 반올림된 값을 비교합니다.
  • 전문 검색은 SQLite와 D1에서만 사용할 수 있습니다. 검색이 켜진 컬렉션은 검색이 꺼진 채로 가져와지고 search_unsupported로 선언됩니다.

가져오기는 미디어를 대상 스토리지 백엔드에 새 스토리지 키로 쓰고, 콘텐츠·설정·SEO 기록의 미디어 참조를 그에 맞게 다시 씁니다. 원본에 없는 미디어 파일에 대한 참조는 변경 없이 내보내지고 unknown_storage_key로 선언됩니다.

보안

  • 패키지를 민감한 데이터로 취급하세요. 초안과 휴지통을 포함한 모든 콘텐츠, 작성자와 댓글 작성자 이메일이 들어 있습니다. 공개 버킷과 공유 폴더에 두지 말고, 더 이상 필요 없는 복사본은 삭제하세요.
  • 패키지를 신뢰할 수 없는 입력으로 취급하세요. 가져오기는 쓰기 전에 경로, 크기, 다이제스트, 레코드 스키마, 참조, 한도를 검사합니다. 패키지의 코드나 SQL을 실행하지 않으며 패키지에서 URL을 가져오지 않습니다.
  • 전송 접근 권한을 신중히 부여하세요. 전송에는 관리자 역할이 필요합니다. admin 스코프 토큰은 모든 전송 작업을 실행할 수 있으므로, 에이전트 토큰에는 필요한 전송 스코프만 주세요.
  • 감사 로그를 검토하세요. EmDash는 사이트의 감사 로그에 전송 작업을 기록합니다: transfer_export_create, transfer_import_create, transfer_import_execute, transfer_import_cancel, transfer_import_abandon, transfer_import_complete, transfer_import_fail, transfer_approval_approve, transfer_approval_deny. 각 항목은 행위 사용자와 작업 또는 승인(리소스 타입 transfer_operation 또는 transfer_approval)을 이름 짓습니다. 세부 정보에는 ID, 다이제스트, 레코드 수, 오류 코드만 있고 패키지 콘텐츠는 없습니다. 전송 오류 세부 정보에도 패키지 콘텐츠는 포함되지 않습니다.
  • 스테이징을 비공개로 유지하세요. EmDash는 스토리지 버킷의 transfers/ 접두사 아래에 패키지 파일을 스테이징하고, 미디어 라우트를 통해 그 접두사를 제공하지 않습니다. 버킷에 공개 도메인이 있으면 백업처럼 미디어로 범위를 한정하세요. 스테이징된 파일은 작업이 끝나거나 만료되면 삭제됩니다.

토큰 스코프

전송은 세 가지 API 토큰 스코프를 사용합니다.

스코프허용
transfer:export내보내기 시작, 진행, 다운로드.
transfer:analyze가져오기 생성, 패키지 파일 업로드, 분석, 계획 읽기.
transfer:execute가져오기 시작, 진행, 취소, 포기.

admin 스코프는 세 가지를 모두 포함하므로 emdash login이 저장하는 토큰으로 모든 전송을 실행할 수 있습니다. 각 전송 스코프는 자체 작업만 허용하며, 관리자만 발급할 수 있습니다. 예를 들어 패키지를 분석만 하고 내보내기·가져오기는 못 하는 에이전트처럼 admin보다 좁은 접근에 사용하세요. 스코프 참고를 보세요.

에이전트 승인

AI 에이전트는 site_* MCP 도구로 전송을 구동합니다. 도구는 작업을 시작하고 진행하며 보고합니다. 패키지 바이트는 전달하지 않으므로, 에이전트의 사용자가 CLI나 REST API로 내보내기를 다운로드하고 패키지를 업로드합니다. 모든 도구에 Admin 역할이 필요합니다.

토큰에 admin도 해당 전송 스코프도 없는 MCP 클라이언트(예: transfer:analyze만 부여된 에이전트)는 혼자 내보내기나 가져오기를 시작할 수 없습니다. site_export_start나 site_import_start 호출은 대기 중인 승인 요청을 만들고 TRANSFER_APPROVAL_REQUIRED와 승인 ID로 실패합니다. 관리자는 Settings → Transfer의 Approval requests에서 요청을 승인하거나 거부하며, 각 대기 요청의 요청자, 작업, 만료 시간이 나열됩니다. 세션 전용 POST /_emdash/api/admin/transfer/approvals/{id}/approve와 …/deny 엔드포인트도 동일합니다. API 토큰은 요청을 승인할 수 없습니다. 클라이언트는 승인 ID로 호출을 반복합니다. 승인은 이 MCP 도구에만 적용되며 REST API에는 승인 매개변수가 없습니다.

승인은 요청한 사용자에게, 같은 토큰과 같은 인자로, 한 번의 호출을 허용합니다. 내보내기 승인은 내보내기 옵션에 묶입니다. 가져오기 승인은 작업과 두 다이제스트에 묶이므로, 계획이 바뀌면 새 승인이 필요합니다. 대기 요청은 15분 후 만료되고, 승인된 것은 승인 후 15분입니다. 작업을 시작하는 재시도가 이를 소모하며, 시작에 실패하면 만료 전까지 같은 승인을 재시도할 수 있습니다. 같은 사용자와 토큰은 이후 스코프 없이 그 한 작업을 확인하고 진행할 수 있습니다.

사람이 매번 승인하지 않고 에이전트가 전송을 실행해야 할 때만 에이전트 토큰에 transfer:export, transfer:execute, 또는 admin을 부여하세요.

한도

한도값
manifest.json8 MiB
레코드 하나1,900,000 바이트
레코드 또는 인덱스 파일 하나4 MiB 및 1,000 레코드
패키지당 레코드5,000,000
패키지당 파일1,000,000
JSON 중첩 깊이64
미디어 파일 하나대상의 maxUploadSize, 기본 50 MiB

capabilities 엔드포인트가 사이트가 적용하는 값을 보고합니다.

호스팅 제공자용

호스팅 컨트롤 플레인은 REST API만으로 고객 사이트를 프로덕션으로 옮길 수 있습니다.

  1. 스토리지, 로케일, maxUploadSize가 있는 새 EmDash 사이트를 프로비저닝하고 설정을 완료하세요. capabilities가 portableDomain.empty를 true로 보고하는지 확인하세요.

  2. 컨트롤 플레인용으로 transfer:analyze와 transfer:execute가 있는 토큰을 발급하세요. 에이전트나 사이트 구축 도구에 두지 마세요.

  3. 가져오기를 실행하고, 실행 전에 계획의 경고에 자체 정책을 적용하세요. 차단 항목이 있는 계획은 거부하세요.

  4. 영수증을 가져와 사이트를 승격하기 전에 확인하세요.

    • verification이 verified
    • packageDigest가 게시하려던 패키지의 다이제스트
    • planDigest가 수락한 계획
    • targetSiteId가 승격하려는 사이트
    • receiptDigest가 영수증의 정규 JSON과 일치
  5. 예를 들어 도메인을 라우팅해 사이트를 승격하세요.

4단계가 성공할 때까지 대상을 도달 불가 상태로 두세요. EmDash는 부분적으로 가져온 사이트를 방문자에게 숨기지 않습니다.

문제 해결

전송 오류는 안정적인 코드를 사용합니다. HTTP 상태는 각 코드와 함께 표시됩니다.

코드상태조치
TRANSFER_TARGET_NOT_EMPTY409대상에 이미 콘텐츠가 있습니다. 새로 설정한 사이트에 가져오세요. Settings → Transfer와 capabilities가 부적격 사유를 나열합니다.
TRANSFER_IMPORT_IN_PROGRESS503이 사이트에서 가져오기가 실행 중이거나, 미완료 가져오기가 쓰기를 막고 있습니다. 끝날 때까지 기다리거나 실패·취소된 가져오기를 포기하세요.
TRANSFER_FENCE_CHECK_FAILED503EmDash가 가져오기 실행 여부를 확인할 수 없습니다. 쓰기를 재시도하세요.
TRANSFER_EXPORT_CONCURRENT_WRITES409내보내기 중 사이트가 계속 변경되었습니다. 편집이 조용할 때 다시 내보내세요.
TRANSFER_EXPIRED410내보내기 파일이 7일 후 삭제되었거나, 가져오기가 24시간 내에 실행되지 않았습니다. 다시 시작하세요.
TRANSFER_FILE_MISSING422선언된 일부 파일이 업로드되지 않았습니다. imports/{id}/missing이 나열하는 모든 것을 업로드하세요.
TRANSFER_FILE_NOT_DECLARED422업로드 경로가 패키지에 없습니다. 나열된 경로만 업로드하세요.
TRANSFER_FILE_SIZE_MISMATCH422Content-Length 또는 업로드된 바이트가 선언된 크기와 다릅니다. 파일을 변경 없이 업로드하세요.
TRANSFER_FILE_DIGEST_MISMATCH422업로드된 바이트가 선언된 다이제스트와 다르거나, 내보내기 후 파일이 변경되었습니다. 원본 파일을 업로드하거나 다시 내보내세요.
TRANSFER_LIMIT_EXCEEDED413파일이 한도를 초과합니다. 미디어는 대상의 maxUploadSize를 높이세요.
TRANSFER_MANIFEST_INVALID422요청 본문이 유효한 매니페스트가 아닙니다. manifest.json을 바이트 그대로 보내세요.
TRANSFER_UNSUPPORTED_FORMAT422대상의 EmDash를 업그레이드하세요.
TRANSFER_UNSUPPORTED_FEATURE422대상의 EmDash를 업그레이드하세요.
TRANSFER_CONTAINER_INVALID422.emdash 파일이 유효한 패키지 아카이브가 아닙니다. 다시 다운로드하세요.
TRANSFER_PLAN_BLOCKED409계획에 차단 항목이 있습니다. 가져오기 계획 검토를 보세요.
TRANSFER_PACKAGE_DIGEST_MISMATCH409다이제스트가 스테이징된 패키지와 맞지 않습니다. 작업의 packageDigest를 사용하세요.
TRANSFER_PLAN_DIGEST_MISMATCH409검토 이후 계획이 변경되었습니다. 현재 계획을 읽고 다시 검토하세요.
TRANSFER_DECISIONS_INVALID422결정이 알 수 없는 프린시펄이나 존재하지 않는 대상 사용자를 가리킵니다. 매핑을 수정하세요.
TRANSFER_INVALID_STATE409작업이 요청을 허용하는 상태가 아닙니다. 작업을 읽고 state를 따르세요.
TRANSFER_LEASE_ACTIVE409다른 요청이 단계를 실행 중입니다. 기다렸다가 재시도하세요.
TRANSFER_IDEMPOTENCY_CONFLICT409Idempotency-Key가 다른 옵션의 내보내기나 다른 패키지의 가져오기에 이미 사용되었습니다. 새 키를 사용하세요.
TRANSFER_RUNTIME_MISMATCH409호환되지 않는 EmDash 버전이 작업을 시작했습니다. 시작한 버전으로 끝내거나 새로 시작하세요.
TRANSFER_VERIFICATION_FAILED422가져온 사이트가 패키지와 맞지 않습니다. errorDetail의 차이를 읽고 가져오기를 포기한 뒤 새 사이트에 가져오세요.
TRANSFER_APPROVAL_REQUIRED403관리자가 요청을 승인해야 합니다. 에이전트 승인을 보세요.
TRANSFER_APPROVAL_INVALID403승인이 알 수 없거나, 거부되었거나, 만료되었거나, 사용되었거나, 다른 매개변수에 묶여 있습니다. 새로 요청하세요.
TRANSFER_SCHEMA_UNCLASSIFIED500데이터베이스에 내보내기가 인식하지 못하는 테이블이나 열이 있습니다. 데이터베이스 마이그레이션과 맞는 EmDash 버전을 실행하세요.
INSUFFICIENT_SCOPE403토큰에 admin도 요청에 필요한 전송 스코프도 없습니다. 해당 스코프의 토큰을 발급하세요.