Введение
Загрузчик резервных копий должен завершать успешные загрузки и освобождать оставшиеся после прерывания части. Вы разделите один ограниченный синтетический файл, завершите его многокомпонентную загрузку, а затем проверите и прервёте отдельную незавершённую загрузку, не затрагивая завершённые объекты.
Сначала пройдите предыдущие уроки по объектам R2 и учётным данным с ограниченной областью действия. На этой свежей виртуальной машине установлены Node.js 22.22.0, Wrangler 4.131.1 и AWS SDK 3.888.0. Вы создадите одно новое закрытое Standard-хранилище и отдельные краткосрочные учётные данные для него. R2 должен быть активен; ознакомьтесь с ограничениями многокомпонентной загрузки и тарифами. Незавершённые части учитываются в объёме хранилища. В этой лабораторной работе передаётся только небольшой синтетический файл, домен не требуется. Не используйте повторно предыдущие загрузки или хранилища.
Создайте закрытое хранилище документов
На этом шаге вы авторизуете виртуальную машину и создадите одно временное хранилище. Авторизация устройства подтверждает вашу учебную учётную запись. Для управления хранилищами 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
Авторизуйте отображённый код устройства в собственном браузере. Перед подтверждением согласия убедитесь, что выбраны ваша учебная учётная запись, а также запрошенные области чтения учётной записи и пользователя:
npx wrangler login --device --browser=false --scopes account:read user:read
npx wrangler whoami --json
Убедитесь, что указано loggedIn: true. Прочитайте имя учётной записи, даже если в списке присутствует только одна запись. Замените YOUR_ACCOUNT_ID ниже фактическим идентификатором этой учётной записи из 32 символов. Команда openssl rand -hex 6 создаёт двенадцать случайных шестнадцатеричных символов, поэтому имя ресурса этой лабораторной работы не совпадёт с именем из предыдущего запуска. Здесь-документ записывает стандартный файл конфигурации; оболочка подставляет в него значения ваших переменных.
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
Для управления хранилищем откройте страницу API Tokens в профиле Cloudflare и создайте пользовательский токен с именем этой лабораторной работы. Предоставьте разрешение Account → Workers R2 Storage → Edit и ограничьте Account Resources учебной учётной записью, идентификатор которой вы сохранили. Установите короткий срок действия. Не добавляйте другие учётные записи или несвязанные разрешения. Этот токен управления предназначен для администрирования бакетов, включая их создание и удаление. Далее в этом же шаге вы создадите отдельный токен объектов с доступом только к этому бакету для работы с объектами через S3 SDK.
Однократно вставьте токен в скрытое приглашение виртуальной машины. 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
Используйте --env-file=.env.management только для команд управления R2; обычная команда whoami продолжит проверять авторизацию устройства виртуальной машины.
Размещайте --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, выберите это точное хранилище и убедитесь, что список объектов пуст. В его настройках оставьте отключёнными URL для публичной разработки и пользовательские домены. Имя хранилища в Dashboard подтверждает его принадлежность; последующие проверки скачанных файлов подтвердят сохранность байтов.
S3-совместимый API предоставляет стандартным SDK доступ к R2. Для этого используется отдельная пара ключей доступа, а не токен устройства Wrangler. В разделе R2 Overview выберите Account Details → API Tokens → Manage, затем создайте User API token с именем, содержащим сгенерированное имя ресурса этой лабораторной работы. Выберите Object Read & Write, ограничьте токен именно этим новым хранилищем и укажите короткий срок действия, если форма это предлагает. Не выбирайте все хранилища или доступ Admin. Не закрывайте страницу с токеном, пока не сохраните секрет, который показывается только один раз.
Используйте следующие приглашения 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
Начните ограниченную многокомпонентную загрузку
На этом шаге вы запускаете multipart upload — серверный сеанс загрузки, который принимает пронумерованные части перед сборкой итогового объекта. Загруженные части ещё не являются объектом, доступным для скачивания. Сохранение идентификатора загрузки позволяет возобновить или прервать именно этот сеанс.
Создайте синтетический двоичный файл размером 6 MiB с помощью Python. Первая часть будет иметь размер 5 MiB, а последняя — 1 MiB. R2 требует поддерживаемые размеры частей: все части, кроме последней, должны быть размером не менее 5 MiB и иметь одинаковый размер. Небольшой файл позволяет продемонстрировать протокол без большой передачи данных.
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, сохранённый при создании загрузки. Проверенный endpoint 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 MB — десятичное отображение 6 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 и должен вернуть часть 1 размером 5,242,880 байт. Не сравнивайте текст ID из списка с сохранённым ID и не создавайте новую сессию для повторного чтения. Используйте сохранённый ID со стандартными API списка и выполните проверку, пока загрузка существует.
Прервите только оставшийся сеанс
На этом шаге вы освободите незавершённые части, прервав загрузку по точному идентификатору. Прерывание отличается от удаления завершённого объекта. Оно должно сохранить и завершённый архив, и справочник.
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.
После отзыва удалите локальные файлы с учётными данными и выйдите из системы на этой виртуальной машине:
rm .env.s3 .env.management
npx wrangler logout
Проверьте структурированные данные об идентификации. Ненулевой код завершения после выхода из системы ожидаем:
npx wrangler whoami --json || true
Убедитесь, что указано loggedIn: false; обычный вход в Dashboard сохраняйте. Проверки платформы контролируют удаление локальных учётных данных и выход из Wrangler. Отзыв обоих токенов выполняется вручную в Dashboard и является отдельным контрольным пунктом этой лабораторной работы; по удалению файлов он не определяется.
Резюме
Завершите многокомпонентную загрузку объекта с точным сохранением байтов, проверьте и прервите незавершённые части, сохраните другие объекты и удалите учётные данные хранилища.



