본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

객체 업로드 (Upload objects)

원문 보기 위키 갱신

R2에 객체를 업로드하는 방법은 여러 가지가 있어요. 어떤 방식을 선택할지는 객체의 크기와 성능 요구 사항에 달려 있어요.

출처: 문서

본문

업로드 방법 선택 (Choose an upload method)

| | 단일 업로드 (Single upload, PUT) | 멀티파트 업로드 (Multipart upload) | | 가장 적합한 경우 | 작은~중간 파일(~100MB 미만) | 대용량 파일, 또는 병렬성과 이어올리기(resumability)가 필요할 때 | | 최대 객체 크기 | 5 GiB | 5 TiB (최대 10,000개 파트) | | 파트 크기 | N/A | 파트당 5 MiB – 5 GiB | | 이어올리기 | 불가 — 전체 업로드를 다시 시작해야 함 | 가능 — 실패한 파트만 다시 시도하면 됨 | | 병렬 업로드 | 불가 | 가능 — 파트를 동시에 업로드할 수 있음 | | 사용 시기 | 작은 객체의 빠르고 간단한 업로드 | 비디오, 백업, 데이터셋, 또는 안정성이 중요한 모든 파일 |

대시보드로 업로드 (Upload via dashboard)

Cloudflare 대시보드에서 버킷으로 객체를 업로드하려면:

  • Cloudflare 대시보드에서 R2 객체 스토리지(R2 object storage) 페이지로 이동해요. 개요(Overview)로 이동 ↗
  • 버킷을 선택해요.
  • 업로드(Upload) 를 선택해요.
  • 업로드 영역에 파일을 드래그 앤 드롭하거나 컴퓨터에서 선택(select from computer) 해요.

업로드가 성공하면 확인 메시지를 받아요.

폴더 생성 (Create a folder)

대시보드에서 폴더 생성(Create folder) 을 선택해 폴더를 만들 수도 있어요. 이렇게 하면 /로 끝나는 키를 가진 0바이트 객체가 생성되어 자리 표시자(placeholder) 역할을 해요. R2에서 폴더가 작동하는 방식에 대한 자세한 내용은 접두사와 폴더(Prefixes and folders)를 참고해요.

Workers API로 업로드 (Upload via Workers API)

Workers에서 R2 바인딩을 사용해 서버 측에서 객체를 업로드해요. R2 바인딩 설정 방법은 Workers에서 R2 사용하기를 참고해요.

단일 업로드 (Single upload)

put()을 사용해 단일 요청으로 객체를 업로드해요. 작은~중간 객체에 가장 간단한 방식이에요.

export default {
	async fetch(request, env) {
		try {
			const object = await env.MY_BUCKET.put("image.png", request.body, {
				httpMetadata: {
					contentType: "image/png",
				},
			});

			if (object === null) {
				return new Response("Precondition failed or upload returned null", {
					status: 412,
				});
			}

			return Response.json({
				key: object.key,
				size: object.size,
				etag: object.etag,
			});
		} catch (err) {
			return new Response(`Upload failed: ${err}`, { status: 500 });
		}
	},
};
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		try {
			const object = await env.MY_BUCKET.put("image.png", request.body, {
				httpMetadata: {
					contentType: "image/png",
				},
			});

			if (object === null) {
				return new Response("Precondition failed or upload returned null", {
					status: 412,
				});
			}

			return Response.json({
				key: object.key,
				size: object.size,
				etag: object.etag,
			});
		} catch (err) {
			return new Response(`Upload failed: ${err}`, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

멀티파트 업로드 (Multipart upload)

대용량 파일이나 파트를 병렬로 업로드해야 할 때는 createMultipartUpload()와 resumeMultipartUpload()를 사용해요. 각 파트는 최소 5 MiB여야 해요(마지막 파트 제외).

export default {
	async fetch(request, env) {
		const key = "large-file.bin";

		// 새 멀티파트 업로드 생성
		const multipartUpload = await env.MY_BUCKET.createMultipartUpload(key);

		try {
			// 실제 애플리케이션에서는 이 부분이 실제 데이터 청크입니다.
			// 마지막 파트를 제외한 각 파트는 최소 5 MiB여야 합니다.
			const firstChunk = new Uint8Array(5 * 1024 * 1024); // placeholder
			const secondChunk = new Uint8Array(1024); // placeholder

			const part1 = await multipartUpload.uploadPart(1, firstChunk);
			const part2 = await multipartUpload.uploadPart(2, secondChunk);

			// 모든 파트로 업로드 완료
			const object = await multipartUpload.complete([part1, part2]);

			return Response.json({
				key: object.key,
				etag: object.httpEtag,
			});
		} catch (err) {
			// 실패 시 중단하여 미완료 업로드가 스토리지에 집계되지 않게 함
			await multipartUpload.abort();
			return new Response(`Multipart upload failed: ${err}`, { status: 500 });
		}
	},
};
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const key = "large-file.bin";

		// 새 멀티파트 업로드 생성
		const multipartUpload = await env.MY_BUCKET.createMultipartUpload(key);

		try {
			// 실제 애플리케이션에서는 이 부분이 실제 데이터 청크입니다.
			// 마지막 파트를 제외한 각 파트는 최소 5 MiB여야 합니다.
			const firstChunk = new Uint8Array(5 * 1024 * 1024); // placeholder
			const secondChunk = new Uint8Array(1024); // placeholder

			const part1 = await multipartUpload.uploadPart(1, firstChunk);
			const part2 = await multipartUpload.uploadPart(2, secondChunk);

			// 모든 파트로 업로드 완료
			const object = await multipartUpload.complete([part1, part2]);

			return Response.json({
				key: object.key,
				etag: object.httpEtag,
			});
		} catch (err) {
			// 실패 시 중단하여 미완료 업로드가 스토리지에 집계되지 않게 함
			await multipartUpload.abort();
			return new Response(`Multipart upload failed: ${err}`, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

대부분의 경우 멀티파트 상태(업로드 ID uploadId와 업로드된 파트 ETag)는 Worker에 요청을 보내는 클라이언트가 추적해요. 다음 예시는 클라이언트 애플리케이션이 멀티파트 업로드를 생성하고, 파트를 업로드하고, 완료하기 위해 호출할 수 있는 HTTP API를 노출하는 예시예요.

export default {
	async fetch(request, env) {
		const url = new URL(request.url);
		const key = url.pathname.slice(1);
		const action = url.searchParams.get("action");

		if (!key || !action) {
			return new Response("Missing key or action", { status: 400 });
		}

		switch (action) {
			// Step 1: Client calls POST /<key>?action=mpu-create
			case "mpu-create": {
				const upload = await env.MY_BUCKET.createMultipartUpload(key);
				return Response.json({ key: upload.key, uploadId: upload.uploadId });
			}

			// Step 2: Client calls PUT /<key>?action=mpu-uploadpart&uploadId=...&partNumber=...
			case "mpu-uploadpart": {
				const uploadId = url.searchParams.get("uploadId");
				const partNumber = Number(url.searchParams.get("partNumber"));
				if (!uploadId || !partNumber || !request.body) {
					return new Response("Missing uploadId, partNumber, or body", {
						status: 400,
					});
				}
				const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
				try {
					const part = await upload.uploadPart(partNumber, request.body);
					return Response.json(part);
				} catch (err) {
					return new Response(String(err), { status: 400 });
				}
			}

			// Step 3: Client calls POST /<key>?action=mpu-complete&uploadId=...
			case "mpu-complete": {
				const uploadId = url.searchParams.get("uploadId");
				if (!uploadId) {
					return new Response("Missing uploadId", { status: 400 });
				}
				const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
				const body = await request.json();
				try {
					const object = await upload.complete(body.parts);
					return new Response(null, {
						headers: { etag: object.httpEtag },
					});
				} catch (err) {
					return new Response(String(err), { status: 400 });
				}
			}

			// Abort an in-progress upload
			case "mpu-abort": {
				const uploadId = url.searchParams.get("uploadId");
				if (!uploadId) {
					return new Response("Missing uploadId", { status: 400 });
				}
				const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
				try {
					await upload.abort();
				} catch (err) {
					return new Response(String(err), { status: 400 });
				}
				return new Response(null, { status: 204 });
			}

			default:
				return new Response(`Unknown action: ${action}`, { status: 400 });
		}
	},
};
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		const key = url.pathname.slice(1);
		const action = url.searchParams.get("action");

		if (!key || !action) {
			return new Response("Missing key or action", { status: 400 });
		}

		switch (action) {
			// Step 1: Client calls POST /<key>?action=mpu-create
			case "mpu-create": {
				const upload = await env.MY_BUCKET.createMultipartUpload(key);
				return Response.json({ key: upload.key, uploadId: upload.uploadId });
			}

			// Step 2: Client calls PUT /<key>?action=mpu-uploadpart&uploadId=...&partNumber=...
			case "mpu-uploadpart": {
				const uploadId = url.searchParams.get("uploadId");
				const partNumber = Number(url.searchParams.get("partNumber"));
				if (!uploadId || !partNumber || !request.body) {
					return new Response("Missing uploadId, partNumber, or body", {
						status: 400,
					});
				}
				const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
				try {
					const part = await upload.uploadPart(partNumber, request.body);
					return Response.json(part);
				} catch (err) {
					return new Response(String(err), { status: 400 });
				}
			}

			// Step 3: Client calls POST /<key>?action=mpu-complete&uploadId=...
			case "mpu-complete": {
				const uploadId = url.searchParams.get("uploadId");
				if (!uploadId) {
					return new Response("Missing uploadId", { status: 400 });
				}
				const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
				const body = await request.json<{ parts: R2UploadedPart[] }>();
				try {
					const object = await upload.complete(body.parts);
					return new Response(null, {
						headers: { etag: object.httpEtag },
					});
				} catch (err) {
					return new Response(String(err), { status: 400 });
				}
			}

			// Abort an in-progress upload
			case "mpu-abort": {
				const uploadId = url.searchParams.get("uploadId");
				if (!uploadId) {
					return new Response("Missing uploadId", { status: 400 });
				}
				const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
				try {
					await upload.abort();
				} catch (err) {
					return new Response(String(err), { status: 400 });
				}
				return new Response(null, { status: 204 });
			}

			default:
				return new Response(`Unknown action: ${action}`, { status: 400 });
		}
	},
} satisfies ExportedHandler<Env>;

완전한 Workers API 참조는 Workers API 참조를 참고해요.

사전 서명 URL (Workers) (Presigned URLs (Workers))

클라이언트(브라우저, 모바일 앱)가 Worker를 경유하지 않고 R2에 직접 업로드해야 할 때, 서버 측에서 사전 서명 URL을 생성해 클라이언트에 넘겨줘요.

import { AwsClient } from "aws4fetch";

export default {
	async fetch(request, env) {
		const r2 = new AwsClient({
			accessKeyId: env.R2_ACCESS_KEY_ID,
			secretAccessKey: env.R2_SECRET_ACCESS_KEY,
		});

		// 1시간 동안 유효한 사전 서명 PUT URL 생성
		const url = new URL(
			"https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/image.png",
		);
		url.searchParams.set("X-Amz-Expires", "3600");

		const signed = await r2.sign(new Request(url, { method: "PUT" }), {
			aws: { signQuery: true },
		});

		// 클라이언트에 서명된 URL 반환 — 클라이언트가 R2에 직접 PUT할 수 있음
		return Response.json({ url: signed.url });
	},
};
import { AwsClient } from "aws4fetch";

interface Env {
	R2_ACCESS_KEY_ID: string;
	R2_SECRET_ACCESS_KEY: string;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const r2 = new AwsClient({
			accessKeyId: env.R2_ACCESS_KEY_ID,
			secretAccessKey: env.R2_SECRET_ACCESS_KEY,
		});

		// 1시간 동안 유효한 사전 서명 PUT URL 생성
		const url = new URL(
			"https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/image.png",
		);
		url.searchParams.set("X-Amz-Expires", "3600");

		const signed = await r2.sign(new Request(url, { method: "PUT" }), {
			aws: { signQuery: true },
		});

		// 클라이언트에 서명된 URL 반환 — 클라이언트가 R2에 직접 PUT할 수 있음
		return Response.json({ url: signed.url });
	},
} satisfies ExportedHandler<Env>;

GET, PUT을 포함한 전체 사전 서명 URL 문서와 보안 모범 사례는 사전 서명 URL(Presigned URLs)을 참고해요.

S3 API로 업로드 (Upload via S3 API)

S3 호환 SDK를 사용해 객체를 업로드해요. 계정 ID와 R2 API 토큰이 필요해요.

단일 업로드 (Single upload)

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFile } from "node:fs/promises";

const S3 = new S3Client({
	region: "auto",
	endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
	credentials: {
		accessKeyId: "<ACCESS_KEY_ID>",
		secretAccessKey: "<SECRET_ACCESS_KEY>",
	},
});

const fileContent = await readFile("./image.png");

const response = await S3.send(
	new PutObjectCommand({
		Bucket: "my-bucket",
		Key: "image.png",
		Body: fileContent,
		ContentType: "image/png",
	}),
);
console.log(`Uploaded successfully. ETag: ${response.ETag}`);
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFile } from "node:fs/promises";

const S3 = new S3Client({
	region: "auto",
	endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
	credentials: {
		accessKeyId: "<ACCESS_KEY_ID>",
		secretAccessKey: "<SECRET_ACCESS_KEY>",
	},
});

const fileContent = await readFile("./image.png");

const response = await S3.send(
	new PutObjectCommand({
		Bucket: "my-bucket",
		Key: "image.png",
		Body: fileContent,
		ContentType: "image/png",
	}),
);
console.log(`Uploaded successfully. ETag: ${response.ETag}`);
import boto3

s3 = boto3.client(
    service_name="s3",
    endpoint_url="https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
    aws_access_key_id="<ACCESS_KEY_ID>",
    aws_secret_access_key="<SECRET_ACCESS_KEY>",
    region_name="auto",
)

with open("./image.png", "rb") as f:
    response = s3.put_object(
        Bucket="my-bucket",
        Key="image.png",
        Body=f,
        ContentType="image/png",
    )
    print(f"Uploaded successfully. ETag: {response['ETag']}")

멀티파트 업로드 (Multipart upload)

대부분의 S3 SDK는 파일이 구성 가능한 임계값을 초과하면 멀티파트 업로드를 자동으로 처리해요. 아래 예시는 자동(고수준) 방식과 수동(저수준) 방식을 모두 보여줘요.

자동 멀티파트 업로드 (Automatic multipart upload)

SDK가 파일을 분할하고 파트를 병렬로 업로드해요.

import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from "node:fs";

const S3 = new S3Client({
	region: "auto",
	endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
	credentials: {
		accessKeyId: "<ACCESS_KEY_ID>",
		secretAccessKey: "<SECRET_ACCESS_KEY>",
	},
});

const upload = new Upload({
	client: S3,
	params: {
		Bucket: "my-bucket",
		Key: "large-file.bin",
		Body: createReadStream("./large-file.bin"),
	},
	// 파트를 병렬 업로드함 (기본값: 4)
	leavePartsOnError: false,
});

upload.on("httpUploadProgress", (progress) => {
	console.log(`Uploaded ${progress.loaded ?? 0} bytes`);
});

const result = await upload.done();
console.log(`Upload complete. ETag: ${result.ETag}`);
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from "node:fs";

const S3 = new S3Client({
	region: "auto",
	endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
	credentials: {
		accessKeyId: "<ACCESS_KEY_ID>",
		secretAccessKey: "<SECRET_ACCESS_KEY>",
	},
});

const upload = new Upload({
	client: S3,
	params: {
		Bucket: "my-bucket",
		Key: "large-file.bin",
		Body: createReadStream("./large-file.bin"),
	},
	leavePartsOnError: false,
});

upload.on("httpUploadProgress", (progress) => {
	console.log(`Uploaded ${progress.loaded ?? 0} bytes`);
});

const result = await upload.done();
console.log(`Upload complete. ETag: ${result.ETag}`);
import boto3

s3 = boto3.client(
    service_name="s3",
    endpoint_url="https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
    aws_access_key_id="<ACCESS_KEY_ID>",
    aws_secret_access_key="<SECRET_ACCESS_KEY>",
    region_name="auto",
)

# upload_file은 대용량 파일에 자동으로 멀티파트를 사용합니다.
# 대용량 객체의 처리량을 높이려면 아래의 수동 멀티파트 예시를 사용하세요.
s3.upload_file(
    Filename="./large-file.bin",
    Bucket="my-bucket",
    Key="large-file.bin",
)
수동 멀티파트 업로드 (Manual multipart upload)

파트 크기나 업로드 순서를 완전히 제어해야 할 때는 저수준 API를 사용해요.

import {
	S3Client,
	CreateMultipartUploadCommand,
	UploadPartCommand,
	CompleteMultipartUploadCommand,
	AbortMultipartUploadCommand,
	type CompletedPart,
} from "@aws-sdk/client-s3";
import { createReadStream, statSync } from "node:fs";

const S3 = new S3Client({
	region: "auto",
	endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
	credentials: {
		accessKeyId: "<ACCESS_KEY_ID>",
		secretAccessKey: "<SECRET_ACCESS_KEY>",
	},
});

const bucket = "my-bucket";
const key = "large-file.bin";
const partSize = 10 * 1024 * 1024; // 10 MiB per part

// Step 1: Create the multipart upload
const { UploadId } = await S3.send(
	new CreateMultipartUploadCommand({ Bucket: bucket, Key: key }),
);

try {
	const fileSize = statSync("./large-file.bin").size;
	const partCount = Math.ceil(fileSize / partSize);
	const parts: CompletedPart[] = [];

	// Step 2: Upload each part
	for (let i = 0; i < partCount; i++) {
		const start = i * partSize;
		const end = Math.min(start + partSize, fileSize);
		const { ETag } = await S3.send(
			new UploadPartCommand({
				Bucket: bucket,
				Key: key,
				UploadId,
				PartNumber: i + 1,
				Body: createReadStream("./large-file.bin", { start, end: end - 1 }),
				ContentLength: end - start,
			}),
		);
		parts.push({ PartNumber: i + 1, ETag });
	}

	// Step 3: Complete the upload
	await S3.send(
		new CompleteMultipartUploadCommand({
			Bucket: bucket,
			Key: key,
			UploadId,
			MultipartUpload: { Parts: parts },
		}),
	);
	console.log("Multipart upload complete.");
} catch (err) {
	// Abort on failure to clean up incomplete parts
	try {
		await S3.send(
			new AbortMultipartUploadCommand({ Bucket: bucket, Key: key, UploadId }),
		);
	} catch (_abortErr) {
		// Best-effort cleanup — the original error is more important
	}
	throw err;
}
import {
	S3Client,
	CreateMultipartUploadCommand,
	UploadPartCommand,
	CompleteMultipartUploadCommand,
	AbortMultipartUploadCommand,
} from "@aws-sdk/client-s3";
import { createReadStream, statSync } from "node:fs";

const S3 = new S3Client({
	region: "auto",
	endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
	credentials: {
		accessKeyId: "<ACCESS_KEY_ID>",
		secretAccessKey: "<SECRET_ACCESS_KEY>",
	},
});

const bucket = "my-bucket";
const key = "large-file.bin";
const partSize = 10 * 1024 * 1024; // 10 MiB per part

// Step 1: Create the multipart upload
const { UploadId } = await S3.send(
	new CreateMultipartUploadCommand({ Bucket: bucket, Key: key }),
);

try {
	const fileSize = statSync("./large-file.bin").size;
	const partCount = Math.ceil(fileSize / partSize);
	const parts = [];

	// Step 2: Upload each part
	for (let i = 0; i < partCount; i++) {
		const start = i * partSize;
		const end = Math.min(start + partSize, fileSize);
		const { ETag } = await S3.send(
			new UploadPartCommand({
				Bucket: bucket,
				Key: key,
				UploadId,
				PartNumber: i + 1,
				Body: createReadStream("./large-file.bin", { start, end: end - 1 }),
				ContentLength: end - start,
			}),
		);
		parts.push({ PartNumber: i + 1, ETag });
	}

	// Step 3: Complete the upload
	await S3.send(
		new CompleteMultipartUploadCommand({
			Bucket: bucket,
			Key: key,
			UploadId,
			MultipartUpload: { Parts: parts },
		}),
	);
	console.log("Multipart upload complete.");
} catch (err) {
	// Abort on failure to clean up incomplete parts
	try {
		await S3.send(
			new AbortMultipartUploadCommand({ Bucket: bucket, Key: key, UploadId }),
		);
	} catch (_abortErr) {
		// Best-effort cleanup — the original error is more important
	}
	throw err;
}
import boto3
import math
import os
from concurrent.futures import ThreadPoolExecutor

s3 = boto3.client(
    service_name="s3",
    endpoint_url="https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
    aws_access_key_id="<ACCESS_KEY_ID>",
    aws_secret_access_key="<SECRET_ACCESS_KEY>",
    region_name="auto",
)

bucket = "my-bucket"
key = "large-file.bin"
file_path = "./large-file.bin"
part_size = 16 * 1024 * 1024  # 16 MiB per part
max_workers = 10  # Number of parallel upload threads

# Step 1: Create the multipart upload
upload_id = None
mpu = s3.create_multipart_upload(Bucket=bucket, Key=key)
upload_id = mpu["UploadId"]

def upload_part(part_number, data):
    response = s3.upload_part(
        Bucket=bucket,
        Key=key,
        UploadId=upload_id,
        PartNumber=part_number,
        Body=data,
    )
    return {"PartNumber": part_number, "ETag": response["ETag"]}

try:
    file_size = os.path.getsize(file_path)
    part_count = math.ceil(file_size / part_size)

    # Step 2: Upload parts in parallel
    with ThreadPoolExecutor(max_workers=max_workers) as pool:
        futures = []
        with open(file_path, "rb") as f:
            for i in range(part_count):
                data = f.read(part_size)
                futures.append(pool.submit(upload_part, i + 1, data))

        parts = [future.result() for future in futures]

    # Step 3: Complete the upload
    s3.complete_multipart_upload(
        Bucket=bucket,
        Key=key,
        UploadId=upload_id,
        MultipartUpload={"Parts": parts},
    )
    print("Multipart upload complete.")
except Exception:
    # Abort on failure to clean up incomplete parts
    if upload_id:
        try:
            s3.abort_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id)
        except Exception:
            pass  # Best-effort cleanup — the original error is more important
    raise

사전 서명 URL (S3 API) (Presigned URLs (S3 API))

사용자가 서버를 거치지 않고 R2에 직접 업로드하는 클라이언트 측 업로드의 경우 사전 서명 PUT URL을 생성해요. 서버가 URL을 만들고 클라이언트가 그 URL로 업로드해요 — API 자격 증명이 클라이언트에 노출되지 않아요.

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const S3 = new S3Client({
	region: "auto",
	endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
	credentials: {
		accessKeyId: "<ACCESS_KEY_ID>",
		secretAccessKey: "<SECRET_ACCESS_KEY>",
	},
});

const presignedUrl = await getSignedUrl(
	S3,
	new PutObjectCommand({
		Bucket: "my-bucket",
		Key: "user-upload.png",
		ContentType: "image/png",
	}),
	{ expiresIn: 3600 }, // Valid for 1 hour
);

console.log(presignedUrl);
// Return presignedUrl to the client
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const S3 = new S3Client({
	region: "auto",
	endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
	credentials: {
		accessKeyId: "<ACCESS_KEY_ID>",
		secretAccessKey: "<SECRET_ACCESS_KEY>",
	},
});

const presignedUrl = await getSignedUrl(
	S3,
	new PutObjectCommand({
		Bucket: "my-bucket",
		Key: "user-upload.png",
		ContentType: "image/png",
	}),
	{ expiresIn: 3600 }, // Valid for 1 hour
);

console.log(presignedUrl);
// Return presignedUrl to the client
import boto3

s3 = boto3.client(
    service_name="s3",
    endpoint_url="https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
    aws_access_key_id="<ACCESS_KEY_ID>",
    aws_secret_access_key="<SECRET_ACCESS_KEY>",
    region_name="auto",
)

presigned_url = s3.generate_presigned_url(
    "put_object",
    Params={
        "Bucket": "my-bucket",
        "Key": "user-upload.png",
        "ContentType": "image/png",
    },
    ExpiresIn=3600,  # Valid for 1 hour
)

print(presigned_url)
# Return presigned_url to the client

전체 사전 서명 URL 문서는 사전 서명 URL(Presigned URLs)을 참고해요.

지원되는 모든 S3 API 메서드는 R2의 S3 API 문서를 참고해요.

CLI로 업로드 (Upload via CLI)

Rclone

Rclone ↗︎은 클라우드 스토리지에서 파일을 관리하는 명령줄 도구예요. 로컬 머신에서 여러 파일을 업로드하거나 다른 클라우드 스토리지 제공자에서 데이터를 복사할 때 Rclone이 잘 작동해요.

rclone을 사용하려면 공식 문서 - Install rclone ↗︎에 따라 머신에 설치해요.

rclone copy 명령으로 파일을 업로드해요.

# 단일 파일 업로드
rclone copy /path/to/local/image.png r2:bucket_name

# 디렉터리의 모든 것을 업로드
rclone copy /path/to/local/folder r2:bucket_name

rclone ls로 업로드를 확인해요.

rclone ls r2:bucket_name

자세한 내용은 rclone 예시를 참고해요.

Wrangler

Wrangler를 사용해 객체를 업로드해요. r2 object put 명령을 실행해요.

wrangler r2 object put test-bucket/image.png --file=image.png

Content-Type(MIME 유형), Content-Disposition, Cache-Control 및 기타 HTTP 헤더 메타데이터를 선택적 플래그를 통해 설정할 수 있어요.

멀티파트 업로드 세부 사항 (Multipart upload details)

파트 크기 제한 (Part size limits)

  • 최소 파트 크기: 5 MiB (마지막 파트 제외)
  • 최대 파트 크기: 5 GiB
  • 최대 파트 수: 10,000
  • 마지막 파트를 제외한 모든 파트는 같은 크기여야 해요

미완료 업로드 라이프사이클 (Incomplete upload lifecycles)

미완료 멀티파트 업로드는 기본적으로 7일 후 자동으로 중단돼요. 커스텀 라이프사이클 정책을 구성해 변경할 수 있어요.

ETags

멀티파트로 업로드한 객체의 ETag는 단일 PUT으로 업로드한 것과 달라요. 각 파트의 ETag는 해당 파트 내용의 MD5 해시예요. 완료된 멀티파트 객체의 ETag는 모든 파트의 이진 MD5 합계를 이어붙인 값의 해시 뒤에 하이픈과 파트 수가 붙은 형태예요.

예를 들어 두 파트 업로드의 파트 ETag가 bce6bf66aeb76c7040fdd5f4eccb78e6와 8165449fc15bbf43d3b674595cbcc406라면, 완료된 객체의 ETag는 f77dc0eecdebcd774a2a22cb393ad2ff-2가 돼요.

Workers API 참조

put(), createMultipartUpload() 등을 포함한 R2 Workers API의 전체 참조.

S3 API 호환성

지원되는 S3 API 작업과 R2 고유 동작.

사전 서명 URL (Presigned URLs)

클라이언트 측 접근을 위한 임시 업로드 및 다운로드 URL 생성.

객체 라이프사이클 (Object lifecycles)

미완료 멀티파트 업로드의 자동 정리 구성.

더 알아보기 (Learn more)