Потоковая передача документов через Worker

CloudflareBeginner
Практиковаться сейчас

Введение

Приложению службы поддержки нужно принимать документ и отправлять его обратно, не делая бакет хранилища общедоступным. Вы подключите частный бакет R2 к Worker, реализуете загрузку с ограничением размера и потоковую передачу скачиваемых данных вызывающим клиентам. Поток передаёт фрагменты по мере их доступности, не собирая весь файл сначала в памяти.

Сначала пройдите уроки Organize a Document Bucket и по настройке и секретам Workers. На этой новой виртуальной машине установлены Node.js 22.22.0 и Wrangler 4.131.1, а также подготовлены синтетические документы и предоставлен модуль аутентификации. Этот модуль защищает демонстрационный endpoint одноразовым токеном, чтобы в уроке по хранилищу не создавался неограниченный сервис загрузки. Позже в этом курсе вы научитесь исправлять авторизацию приложения.

Перед началом вашей учебной учётной записи потребуется активная подписка R2 и разрешение на управление новым бакетом и Worker. Ознакомьтесь с тарифами R2: хранение и операции, а также использование Worker тарифицируются отдельно. Покупать домен не нужно. Используйте только синтетические файлы и в конце удалите Worker, объекты и бакет этого практического задания. Для каждой виртуальной машины требуется отдельная авторизация; ресурсы предыдущих виртуальных машин не используются повторно.

Подключите бакет приложения

На этом шаге вы авторизуете виртуальную машину и создадите отдельный частный бакет для приложения. Авторизация устройства подтверждает вашу учебную учётную запись. Для управления бакетом 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 workers_scripts:write workers_kv:write
npx wrangler whoami --json

Убедитесь, что указано loggedIn: true. Прочитайте имя учётной записи, даже если в списке присутствует только одна запись. Замените YOUR_ACCOUNT_ID ниже фактическим 32-символьным идентификатором этой учётной записи. Команда openssl rand -hex 6 создаёт двенадцать случайных шестнадцатеричных символов, поэтому имя ресурсов этого задания не совпадёт с именем предыдущего запуска. Here-document записывает стандартный файл конфигурации; оболочка подставляет в него значения ваших переменных.

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

Для управления бакетом откройте в профиле Cloudflare страницу API Tokens и создайте пользовательский токен с именем этого практического задания. Выдайте разрешение Account → Workers R2 Storage → Edit, а в разделе Account Resources ограничьте его учебной учётной записью, идентификатор которой вы сохранили. Установите короткий срок действия. Не добавляйте другие учётные записи и несвязанные разрешения. Этот токен управления предназначен для администрирования бакетов, включая их создание и удаление. В этом практическом задании Worker обращается к объектам R2 через привязку DOCUMENTS.

Один раз скопируйте токен в скрытое приглашение виртуальной машины. 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, выберите этот бакет и проверьте, что список объектов пуст. В его настройках оставьте отключёнными public development URL и пользовательские домены. Имя бакета в Dashboard подтверждает его идентичность; последующие проверки скачивания подтвердят сохранность байтов.

Разрешение на скрипты Worker нужно для развёртывания. Разрешение KV нужно Wrangler для служебного учёта удаления; это практическое задание не создаёт пространство имён KV. Токен управления R2 остаётся отдельным учётным данным, ограниченным этой учётной записью.

Реализуйте ограниченные по размеру загрузки и потоковые скачивания

На этом шаге вы превратите конфигурационную привязку DOCUMENTS в операции с объектами. Binding — это объект среды выполнения, который Cloudflare предоставляет Worker. env.DOCUMENTS ссылается на частный бакет, заданный в конфигурации по имени; Worker не нужен секрет S3 для работы с ним.

Предоставленный файл src/auth.js проверяет одноразовый bearer-токен. Наш маршрут принимает только простые имена документов с расширением .txt. Метод PUT заменяет байты по выбранному ключу. В этом примере разрешены файлы размером не более 1 MiB (1 048 576 байт), в том числе от клиентов, которые не указывают заголовок длины. Фрагменты загрузки собираются только в пределах этого ограничения, поэтому R2 получает тело с известной длиной. При скачивании object.body передаётся непосредственно в ответ, поэтому данные остаются потоковыми.

Запишите обработчик с помощью следующего here-document:

cat > src/index.js <<'JS'
import { authorized } from "./auth.js";
const MAX_BYTES = 1024 * 1024;
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path === "/health" && request.method === "GET") return new Response("ok");
    if (!await authorized(request, env)) return new Response("Unauthorized", { status: 401 });
    if (!/^\/documents\/[a-z0-9-]+\.txt$/.test(path)) return new Response("Not found", { status: 404 });
    const key = path.slice(1);
    if (request.method === "PUT") {
      if (Number(request.headers.get("Content-Length")) > MAX_BYTES)
        return new Response("Too large", { status: 413 });
      // Count actual bytes too: a request may omit Content-Length.
      const reader = request.body?.getReader();
      if (!reader) return new Response("Body required", { status: 400 });
      const chunks = [];
      let total = 0;
      for (;;) {
        const { value, done } = await reader.read();
        if (done) break;
        total += value.byteLength;
        if (total > MAX_BYTES) {
          await reader.cancel();
          return new Response("Too large", { status: 413 });
        }
        chunks.push(value);
      }
      const bytes = new Uint8Array(total);
      let offset = 0;
      for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
      await env.DOCUMENTS.put(key, bytes, { httpMetadata: { contentType: "text/plain" } });
      return new Response("Stored", { status: 201 });
    }
    if (request.method !== "GET") return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, PUT" } });
    const object = await env.DOCUMENTS.get(key);
    if (object === null) return new Response("Not found", { status: 404 });
    const headers = new Headers();
    object.writeHttpMetadata(headers);
    headers.set("ETag", object.httpEtag);
    headers.set("Cache-Control", "private, no-store");
    return new Response(object.body, { headers });
  }
};
JS

get() возвращает null, если ключ отсутствует; обработайте этот случай до чтения тела объекта. writeHttpMetadata восстанавливает сохранённый тип содержимого, а httpEtag уже заключён в кавычки в правильном формате. Значение private, no-store не позволяет общим кэшам сохранять эти защищённые документы.

Создайте случайный токен приложения в .dev.vars; Wrangler загружает этот файл при локальной разработке. Это синтетическое учётное данное практического задания, не связанное с учётными данными вашей учётной записи Cloudflare:

umask 077
printf "ACCESS_TOKEN=%s\n" "$(openssl rand -hex 24)" > .dev.vars

Проверьте, что Wrangler может собрать код без развёртывания. Проверка платформы запускает отдельную временную локальную среду выполнения со свежими синтетическими данными, чтобы проверить точное совпадение байтов, оба сценария ограничения размера и отсутствие слишком больших объектов:

npx wrangler deploy --dry-run

Проверьте границу локального хранилища

На этом шаге вы запускаете Worker с локальным хранилищем R2. По умолчанию wrangler dev использует локальную симуляцию, поэтому эти запросы не создают объект в облаке. Запустите сервер разработки в фоновом режиме; $! сохранит идентификатор процесса этой задачи для последующего завершения.

npx wrangler dev --ip 127.0.0.1 --port 8787 > dev.log 2>&1 &
DEV_PID=$!

Дождитесь, пока в dev.log появится сообщение о готовности сервера, затем загрузите одноразовый токен приложения в этот терминал. Не выводите его на экран.

cat dev.log
set -a
source .dev.vars
set +a

Загрузите и скачайте подготовленный файл. --data-binary сохраняет его байты без изменений, а -o записывает скачанные данные в файл.

curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @document.txt http://127.0.0.1:8787/documents/report.txt
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/report.txt -o local-download.txt
cmp document.txt local-download.txt

Убедитесь, что загрузка вернула 201 Stored, а сравнение завершилось успешно и ничего не вывело. Проверьте отсутствующий ключ и загрузку файла, размер которого на один байт превышает ограничение. Python создаст только ограниченный по размеру синтетический файл:

curl -i -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/missing.txt
python3 -c "open('oversized.txt','wb').write(b'x' * (1024 * 1024 + 1))"
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @oversized.txt http://127.0.0.1:8787/documents/large.txt

Ожидайте 404 Not found и 413 Too large. В этих вызовах curl намеренно не используется --fail, чтобы ожидаемые ошибки HTTP оставались видимыми. Страница ошибки HTML от прокси не является ответом приложения. Перед остановкой локального сервера запустите проверку платформы.

Разверните и проверьте интеграцию с частным бакетом

На этом шаге вы повторите работу с документом в настоящем R2. Успешная локальная проверка не подтверждает удалённую привязку или владение учётной записью.

Остановите сервер разработки и опубликуйте Worker:

kill "$DEV_PID"
wait "$DEV_PID" 2>/dev/null || true
npx wrangler deploy

Загрузите секрет приложения с помощью стандартной массовой команды. .dev.vars не загружается автоматически при развёртывании.

npx wrangler secret bulk .dev.vars

Скопируйте точный HTTPS-адрес workers.dev из вывода развёртывания в переменную BASE_URL, не добавляя завершающий слеш. Дождитесь, пока /health вернёт ok; если развёртывание ещё распространяется, повторяйте запрос до одной минуты.

BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/health"

Загрузите отчёт в удалённый бакет, скачайте его и сравните:

curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @document.txt "$BASE_URL/documents/report.txt"
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/report.txt" -o remote-download.txt
cmp document.txt remote-download.txt

Ожидайте 201 Stored и полного совпадения байтов. Повторите отрицательные проверки для общедоступной конечной точки:

curl -i "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/missing.txt"
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @oversized.txt "$BASE_URL/documents/large.txt"

Ожидайте 401 Unauthorized, 404 Not found и 413 Too large. В Dashboard откройте этот Worker и проверьте его привязку R2, затем откройте точный бакет и найдите documents/report.txt. Его public development URL и пользовательские домены по-прежнему должны быть отключены. Worker предоставляет путь доступа; приватность бакета не означает, что каждый маршрут Worker автоматически безопасен.

Привязка DOCUMENTS соединяет Worker с закрытым бакетом R2

Строка DOCUMENTS связывает этот Worker с его конкретным бакетом. Сгенерированные имена ресурсов в примере отличаются от ваших.

Отчёт, загруженный через Worker в закрытый бакет Standard

Строка объекта показывает report.txt, text/plain и 41 B; Public Access остаётся Disabled. Имена и даты приведены для примера. Bucket Size может временно показывать 0 B из-за задержки обновления; строка объекта и успешное скачивание подтверждают наличие отчёта.

Удалите удалённое приложение и бакет

На этом шаге вы удалите только Worker и объекты этого практического задания, пока авторизация ещё действует. Частный бакет не исчезает автоматически после удаления Worker.

npx wrangler delete

Подтвердите точное сгенерированное имя Worker. Явно удалите единственный загруженный объект, затем удалите бакет:

BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
npx wrangler r2 object delete "$BUCKET/documents/report.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management

Запрос слишком большого размера не должен был создать documents/large.txt. Если бакет неожиданно не пуст, проверьте только этот бакет и после выяснения причины нарушения ограничения размера удалите точный синтетический ключ. В таком случае предыдущая функциональная проверка не считается пройденной.

Обновите списки Worker и бакетов в Dashboard и запустите проверку очистки платформы. Ошибки аутентификации или сети не подтверждают удаление: результат в этом случае не определён.

Отзовите оставшиеся учётные данные

На этом шаге отзовите токен управления этого практического задания на странице API Tokens вашего профиля, удалите локальный секрет приложения и закройте авторизацию виртуальной машины. Выполняйте это только после успешного прохождения предыдущей проверки очистки.

rm .env.management .dev.vars
unset ACCESS_TOKEN
npx wrangler logout
npx wrangler whoami --json || true

Убедитесь, что указано loggedIn: false. Отзыв токена управления — отдельная ручная проверка в Dashboard; одного удаления локального файла недостаточно для его отзыва. Не изменяйте обычный вход в Dashboard и токены других практических заданий.

Резюме

Подключите частное хранилище R2 к Worker, принимайте ограниченные по размеру загрузки, передавайте точные байты документов потоково, обрабатывайте ошибки и удаляйте созданные облачные ресурсы.