멀티파트 업로드 완료 및 중단

CloudflareBeginner
지금 연습하기

소개

백업 업로더는 성공한 업로드를 완료하고, 중단 후 남은 미완료 파트를 해제해야 합니다. 이 실습에서는 크기가 제한된 합성 파일 하나를 여러 파트로 나누어 멀티파트 세션을 완료합니다. 그런 다음 별도의 미완료 세션을 확인하고 중단하되, 이미 완료된 객체에는 영향을 주지 않습니다.

먼저 이전 R2 객체 및 범위가 지정된 자격 증명 실습을 완료합니다. 이 새 VM 에는 Node.js 22.22.0, Wrangler 4.131.1, AWS SDK 3.888.0 이 설치되어 있습니다. 새로운 비공개 Standard 버킷 하나와 해당 버킷에서만 사용할 단기 자격 증명을 생성합니다. R2 가 활성화되어 있어야 합니다. 멀티파트 제한요금을 확인합니다. 완료되지 않은 파트도 스토리지 용량을 차지합니다. 이 실습에서는 작은 합성 파일만 전송하며 도메인은 필요하지 않습니다. 이전 업로드나 버킷을 재사용하지 않습니다.

비공개 문서 버킷 생성

이 단계에서는 이 VM 을 인증하고 일회성으로 사용할 버킷 하나를 생성합니다. 디바이스 인증은 학습 계정을 확인합니다. R2 버킷 관리는 해당 계정으로 제한된 별도의 API 토큰을 사용합니다.

아래 명령 구문에 맞게 Bash 를 시작한 다음 준비된 프로젝트로 이동하여 도구를 확인합니다. 리소스 이름 변수에 계속 액세스할 수 있도록 같은 터미널을 열어 둡니다.

bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version

표시된 디바이스 코드를 본인의 브라우저에서 인증합니다. 동의를 승인하기 전에 학습 계정과 요청된 account 및 user 읽기 범위를 확인합니다.

npx wrangler login --device --browser=false --scopes account:read user:read
npx wrangler whoami --json

loggedIn: true인지 확인합니다. 계정이 하나만 표시되더라도 계정 이름을 읽습니다. 아래의 YOUR_ACCOUNT_ID를 해당 계정의 실제 32 자 ID 로 바꿉니다. openssl rand -hex 6은 12 개의 무작위 16 진수 문자를 생성하므로 이전 실행과 이름이 충돌하지 않습니다. 이 here-document 는 표준 구성 파일을 작성하며, 셸이 변수 값을 파일에 삽입합니다.

ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r06-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON

버킷을 관리하려면 Cloudflare 프로필의 API Tokens 페이지를 열고 이 실습 이름을 포함한 사용자 지정 토큰을 생성합니다. Account → Workers R2 Storage → Edit 권한을 부여하고, Account Resources를 ID 를 저장한 학습 계정으로 제한합니다. 만료 기간은 짧게 설정합니다. 다른 계정이나 관련 없는 권한은 포함하지 않습니다. 이 관리 토큰은 생성과 삭제를 포함한 버킷 관리에 사용합니다. 이 단계의 뒷부분에서는 S3 SDK 의 객체 작업을 위해 이 버킷으로 권한이 제한된 별도의 객체 토큰을 생성합니다.

토큰을 한 번만 이 숨겨진 VM 프롬프트에 입력합니다. umask 077은 파일을 본인 사용자만 읽을 수 있도록 제한하고, read -s는 입력을 화면에 표시하지 않습니다. 이 파일은 Wrangler 의 표준 토큰 변수를 사용하며 Git 에서 제외됩니다.

umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN

R2 관리 명령에만 --env-file=.env.management를 사용합니다. 일반적인 whoami 명령은 계속 VM 의 디바이스 인증 상태를 확인합니다.

--env-file을 각 Wrangler 명령의 끝에 배치하여 파일 인수 목록에 명령 이름까지 포함되지 않게 합니다. 각 버킷을 만든 후 Wrangler 가 구성에 바인딩을 추가할지 물으면 n을 입력하고 Enter 를 누릅니다. 필요한 바인딩은 이미 구성에 포함되어 있습니다.

npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management

버킷 목록을 확인하고 방금 생성한 정확한 이름을 찾습니다. 다른 버킷은 다른 작업에 사용되므로 변경하지 않습니다.

npx wrangler r2 bucket list --env-file=.env.management

Dashboard 에서 Storage & databases → R2 → Overview를 열고 이 버킷을 선택한 다음, 비어 있는 객체 목록을 확인합니다. 버킷 설정에서 public development URL 과 custom domains 는 비활성화된 상태로 둡니다. Dashboard 에 표시되는 버킷 이름은 대상 버킷이 맞는지 확인하는 데 사용합니다. 나중에 다운로드 확인을 통해 저장된 바이트가 올바른지 검증합니다.

S3 호환 API 를 사용하면 표준 스토리지 SDK 로 R2 에 액세스할 수 있습니다. 이 API 는 Wrangler 의 디바이스 토큰이 아니라 별도의 액세스 키 쌍을 사용합니다. R2 Overview 에서 Account Details → API Tokens → Manage를 선택한 다음, 이 실습에서 생성한 리소스 이름을 포함한 이름으로 User API token을 생성합니다. Object Read & Write를 선택하고, 정확히 새로 생성한 버킷 하나로 범위를 제한합니다. 양식에서 제공한다면 만료 기간을 짧게 설정합니다. 모든 버킷이나 Admin 액세스는 선택하지 않습니다. 일회성 시크릿을 저장할 때까지 이 토큰 페이지를 열어 둡니다.

VM 에서 다음 Bash 프롬프트를 사용합니다. read -s는 입력을 숨기고, umask 077은 자격 증명 파일을 본인 사용자만 읽을 수 있도록 설정합니다. 다음 이름은 AWS SDK 에서 사용하는 표준 환경 변수입니다. Access Key ID 와 Secret Access Key 를 각각 해당 프롬프트에 붙여 넣은 다음 Enter 키를 누릅니다. 일반 API 토큰 값은 붙여 넣지 않습니다.

umask 077
read -r -s -p 'Access Key ID: ' AWS_ACCESS_KEY_ID; printf '\n'
read -r -s -p 'Secret Access Key: ' AWS_SECRET_ACCESS_KEY; printf '\n'
printf 'AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n' "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" > .env.s3
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY

재사용할 수 있는 표준 SDK 클라이언트를 작성합니다. SDK 에는 리전 문자열이 필요하며, R2 에서는 auto를 사용합니다. 기존 구성을 읽어 CLI 와 SDK 작업이 같은 계정과 버킷을 대상으로 하도록 합니다.

cat > storage.mjs <<'JS'
import { S3Client } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8"));
export const Bucket = config.r2_buckets[0].bucket_name;
export const s3 = new S3Client({
  region: "auto",
  endpoint: `https://${config.account_id}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});
JS

Wrangler 를 사용하여 보존할 합성 핸드북을 업로드합니다. 완료 및 중단 작업이 진행되는 동안 이 파일은 변경되지 않은 상태로 남아 있어야 합니다.

npx wrangler r2 object put "$BUCKET/retained/handbook.txt" --remote --file retained.txt --content-type text/plain --env-file=.env.management

크기가 제한된 멀티파트 업로드 시작

이 단계에서는 멀티파트 업로드를 시작합니다. 멀티파트 업로드는 최종 객체로 조립하기 전에 번호가 지정된 파트를 받아들이는 서버 측 업로드 세션입니다. 업로드된 파트는 아직 다운로드할 수 있는 객체가 아닙니다. 업로드 ID 를 저장하면 이 세션을 재개하거나 중단할 수 있습니다.

일반 Python 으로 6MiB 크기의 합성 바이너리 파일을 생성합니다. 첫 번째 파트는 5MiB 이고 마지막 파트는 1MiB 입니다. R2 에서 지원하는 파트 크기를 사용해야 합니다. 마지막 파트를 제외한 파트는 최소 5MiB 여야 하며, 파트 크기는 동일하게 사용합니다. 이 작은 파일로 대용량 전송 없이 프로토콜을 확인합니다.

python3 - <<'DATA'
from pathlib import Path
Path("archive.bin").write_bytes(bytes(range(256)) * (6 * 1024 * 1024 // 256))
DATA
cat > start.mjs <<'JS'
import { CreateMultipartUploadCommand } from "@aws-sdk/client-s3";
import { writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const Key = "exports/archive.bin";
const result = await s3.send(new CreateMultipartUploadCommand({ Bucket, Key, ContentType: "application/octet-stream" }));
writeFileSync("upload.json", JSON.stringify({ Key, UploadId: result.UploadId }));
console.log("Started multipart upload for", Key);
JS
node --env-file=.env.s3 start.mjs

upload.json을 보관합니다. 이 파일은 작업을 식별할 뿐, 성공을 나타내는 표식이 아닙니다. start를 불필요하게 다시 실행하지 않습니다. 실행할 때마다 정리해야 하는 또 하나의 미완료 업로드가 생성됩니다. 생성 요청에서 저장한 업로드 ID 를 계속 사용합니다. 테스트한 R2 엔드포인트의 목록 응답은 다른 불투명 ID 문자열을 반환했습니다. 정확한 객체 키를 비교하고 저장한 ID 로 ListParts 를 호출하여 활성 세션을 확인합니다.

파트를 순서대로 업로드하고 객체 완료

이 단계에서는 두 파트를 전송하고, 반환된 파트 식별자로 최종 객체를 구성하도록 R2 에 알립니다. 파트 번호는 1 부터 시작합니다. 완료 요청에는 각 파트 업로드에서 반환된 ETag 를 그대로 포함해야 합니다. 전체 원본 파일을 직접 해시한 값과는 다릅니다.

cat > complete.mjs <<'JS'
import { UploadPartCommand, CompleteMultipartUploadCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { readFileSync, writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const { Key, UploadId } = JSON.parse(readFileSync("upload.json", "utf8"));
const bytes = readFileSync("archive.bin");
const size = 5 * 1024 * 1024;
const Parts = [];
for (let offset = 0, PartNumber = 1; offset < bytes.length; offset += size, PartNumber++) {
  const result = await s3.send(new UploadPartCommand({ Bucket, Key, UploadId, PartNumber, Body: bytes.subarray(offset, offset + size) }));
  Parts.push({ PartNumber, ETag: result.ETag });
  console.log("Uploaded part", PartNumber);
}
await s3.send(new CompleteMultipartUploadCommand({ Bucket, Key, UploadId, MultipartUpload: { Parts } }));
const object = await s3.send(new GetObjectCommand({ Bucket, Key }));
writeFileSync("completed.bin", await object.Body.transformToByteArray());
console.log("Completed and downloaded", Key);
JS
node --env-file=.env.s3 complete.mjs

파트 업로드 메시지 두 줄 뒤에 완료 메시지가 출력되는지 확인합니다. 다운로드한 파일을 바이트 단위로 비교합니다.

cmp archive.bin completed.bin && printf "Multipart bytes match\n"

멀티파트 객체의 ETag 가 최종 파일의 MD5 라는 보장은 없습니다. 바이트 비교를 통해 콘텐츠가 직접 보존되었음을 확인할 수 있습니다. Dashboard 에서 이 실습의 버킷을 열고 exports/archive.bin을 확인합니다. 보존한 핸드북도 여전히 있어야 합니다.

View prefixes as folders 선택을 해제하면 예시처럼 두 객체의 전체 키가 표시됩니다. 생성된 버킷 이름은 예시와 다릅니다. 6.29 MB6 MiB(6,291,456 바이트) 의 십진수 표시입니다. 상단 Bucket Size: 0 B 요약은 업데이트가 늦을 수 있으므로 객체 행과 API 로 검증한 바이트를 기준으로 저장 내용을 확인하세요.

완료된 멀티파트 객체와 보존된 안내서

미완료 업로드 확인

이 단계에서는 새 업로드 하나를 의도적으로 미완료 상태로 남긴 다음, 해당 세션과 파트를 목록으로 확인합니다. 일반 객체 목록에는 완료된 파일만 표시되지만, 미완료 파트도 스토리지 용량을 차지합니다. 따라서 정리할 때는 객체 목록뿐 아니라 업로드 목록도 확인해야 합니다.

cat > abandon.mjs <<'JS'
import { CreateMultipartUploadCommand, UploadPartCommand, ListMultipartUploadsCommand, ListPartsCommand } from "@aws-sdk/client-s3";
import { readFileSync, writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const Key = "temporary/unfinished.bin";
const result = await s3.send(new CreateMultipartUploadCommand({ Bucket, Key }));
const UploadId = result.UploadId;
writeFileSync("abandoned.json", JSON.stringify({ Key, UploadId }));
await s3.send(new UploadPartCommand({ Bucket, Key, UploadId, PartNumber: 1, Body: readFileSync("archive.bin").subarray(0, 5 * 1024 * 1024) }));
const uploads = await s3.send(new ListMultipartUploadsCommand({ Bucket }));
console.log(uploads.Uploads.map(upload => ({ key: upload.Key, uploadId: upload.UploadId })));
const parts = await s3.send(new ListPartsCommand({ Bucket, Key, UploadId }));
console.log(parts.Parts.map(part => ({ part: part.PartNumber, bytes: part.Size })));
JS
node --env-file=.env.s3 abandon.mjs

멀티파트 목록에 temporary/unfinished.bin이 포함됩니다. ListParts 요청은 저장된 업로드 ID 를 사용하며 5,242,880 바이트인 파트 1 을 반환해야 합니다. 목록의 ID 문자열과 저장된 ID 를 비교하거나 다시 조회하기 위해 새 세션을 만들지 마세요. 저장된 ID 와 표준 목록 API 를 사용하고 업로드가 존재하는 동안 플랫폼 검사를 실행합니다.

중단된 세션만 중단

이 단계에서는 정확한 업로드 ID 를 사용해 중단하고 미완료 파트를 해제합니다. 중단은 완료된 객체를 삭제하는 것과 다릅니다. 완료된 아카이브와 핸드북은 모두 그대로 남아 있어야 합니다.

cat > abort.mjs <<'JS'
import { AbortMultipartUploadCommand, ListMultipartUploadsCommand } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const { Key, UploadId } = JSON.parse(readFileSync("abandoned.json", "utf8"));
await s3.send(new AbortMultipartUploadCommand({ Bucket, Key, UploadId }));
const uploads = await s3.send(new ListMultipartUploadsCommand({ Bucket }));
console.log("Incomplete uploads:", uploads.Uploads || []);
JS
node --env-file=.env.s3 abort.mjs

이 새 버킷의 멀티파트 업로드 목록이 이제 비어 있어야 합니다. 플랫폼 확인에서는 완료된 두 객체도 다운로드하여 변경되지 않고 남아 있는지 검증합니다. 목록 조회가 실패했다고 해서 빈 목록으로 해석하지 않습니다.

완료된 파일과 버킷 정리

이 단계에서는 중단 확인이 통과한 후 완료된 객체 정확히 두 개를 삭제합니다. 명시적으로 정리하므로 기본 미완료 업로드 수명 주기 규칙이 실행될 때까지 기다릴 필요가 없습니다.

npx wrangler r2 object delete "$BUCKET/exports/archive.bin" --remote --env-file=.env.management
npx wrangler r2 object delete "$BUCKET/retained/handbook.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
npx wrangler r2 bucket list --env-file=.env.management

생성된 이 버킷의 이름만 확인합니다. 성공한 목록 조회 결과에서 해당 이름이 사라졌는지 확인한 다음, 자격 증명을 폐기하기 전에 플랫폼 정리 확인을 실행합니다.

실습 자격 증명 폐기 및 로그아웃

이 단계에서는 이 실습으로 남은 액세스 권한을 종료합니다. R2 API Tokens 페이지에서 이 실습용으로 생성한 객체 토큰만 폐기합니다. 프로필의 API Tokens 페이지에서는 이 실습용으로 생성한 별도의 R2 관리 토큰을 폐기합니다. 버킷을 삭제해도 토큰은 폐기되지 않으며, Wrangler 에서 로그아웃해도 S3 자격 증명은 폐기되지 않습니다.

토큰을 폐기한 후 로컬 자격 증명 파일을 삭제하고 이 VM 에서 로그아웃합니다.

rm .env.s3 .env.management
npx wrangler logout

구조화된 인증 정보를 확인합니다. 로그아웃한 상태에서는 종료 상태가 0 이 아닌 것이 정상입니다.

npx wrangler whoami --json || true

loggedIn: false인지 확인합니다. 일반적인 Dashboard 로그인은 유지합니다. 플랫폼 확인에서는 로컬 자격 증명 파일 삭제와 Wrangler 로그아웃을 검사합니다. 두 토큰의 폐기는 이 실습에서 Dashboard 를 통해 수동으로 확인하는 항목이며, 파일 삭제만으로 판단하지 않습니다.

요약

정확한 바이트로 멀티파트 객체를 완료하고, 완료되지 않은 파트를 확인 및 중단하며, 다른 객체를 보존하고 스토리지 자격 증명을 정리합니다.