Ajouter des téléchargements conditionnels de documents

CloudflareBeginner
Pratiquer maintenant

Introduction

Un lecteur de documents n’a souvent besoin que des quelques octets suivants, ou simplement de vérifier que sa copie mise en cache est toujours à jour. Télécharger le fichier entier à chaque requête gaspille des ressources. Vous allez ajouter des validateurs HTTP et des téléchargements d’une seule plage d’octets à un Worker protégé, adossé à un stockage R2 privé.

Terminez d’abord l’expérience « Stream Documents Through a Worker ». Cette expérience démarre dans une nouvelle VM avec Node.js 22.22.0, Wrangler 4.131.1 et un module fourni de vérification des jetons ; vous créez un nouveau bucket et déployez un nouveau Worker. Votre abonnement R2 et les autorisations de votre compte d’apprentissage doivent déjà être configurés. Consultez la tarification de R2 pour connaître les opérations et le stockage. Aucun domaine personnalisé n’est nécessaire. Seul du texte synthétique est stocké ; effectuez le nettoyage avant de quitter.

Connecter le bucket de l’application

Dans cette étape, vous autorisez cette VM et créez un bucket privé indépendant pour l’application. L’autorisation de l’appareil confirme votre compte d’apprentissage. La gestion des buckets R2 utilise un jeton API distinct, limité à ce compte.

Lancez Bash pour utiliser la syntaxe des commandes ci-dessous, puis accédez au projet préparé et vérifiez ses outils. Gardez ce même terminal ouvert afin que les variables contenant les noms de ressources restent disponibles :

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

Autorisez le code d’appareil affiché dans votre propre navigateur. Vérifiez le compte d’apprentissage ainsi que les autorisations de lecture du compte et de l’utilisateur demandées avant d’accorder votre consentement :

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

Vérifiez que loggedIn: true est présent. Lisez le nom du compte, même si un seul compte est répertorié. Remplacez YOUR_ACCOUNT_ID ci-dessous par l’ID réel de 32 caractères de ce compte. openssl rand -hex 6 génère douze caractères hexadécimaux aléatoires afin d’éviter tout conflit avec une exécution précédente. Le document here-document écrit un fichier de configuration standard ; le shell y remplace vos variables.

ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r03-$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

Pour gérer le bucket, ouvrez la page API Tokens de votre profil Cloudflare et créez un jeton personnalisé portant un nom associé à cette expérience. Accordez l’autorisation Account → Workers R2 Storage → Edit, puis limitez Account Resources au compte d’apprentissage dont vous avez enregistré l’ID. Définissez une durée d’expiration courte. N’incluez aucun autre compte ni aucune autorisation sans rapport. Ce jeton de gestion sert à administrer les buckets, notamment à les créer et à les supprimer. Dans ce lab, le Worker accède aux objets R2 via son binding DOCUMENTS.

Copiez le jeton une seule fois dans l’invite de cette VM masquée. umask 077 limite l’accès au fichier à votre utilisateur ; read -s masque la saisie. Le fichier utilise la variable de jeton standard de Wrangler et est exclu de 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

Utilisez --env-file=.env.management uniquement pour les commandes de gestion R2 ; la commande whoami ordinaire continue de vérifier l’autorisation de l’appareil de la VM.

Placez --env-file à la fin de chaque commande Wrangler pour que sa liste de fichiers ne soit pas étendue au nom de la commande. Après la création de chaque compartiment, si Wrangler propose d’ajouter une liaison à la configuration, saisissez n et appuyez sur Entrée. La configuration contient déjà la liaison prévue.

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

Répertoriez vos buckets et recherchez le nom généré exact. Les autres buckets appartiennent à d’autres travaux ; ne les modifiez pas.

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

Dans le Dashboard, ouvrez Storage & databases → R2 → Overview, sélectionnez ce bucket précis et vérifiez que sa liste d’objets est vide. Dans ses paramètres, laissez l’URL publique de développement et les domaines personnalisés désactivés. Le nom du bucket dans le Dashboard confirme son identité ; les vérifications de téléchargement ultérieures prouveront quels octets y sont stockés.

L’autorisation du script Worker permet le déploiement. L’autorisation KV permet à Wrangler d’enregistrer les suppressions ; cette expérience ne crée aucun espace de noms KV. Le jeton de gestion R2 reste une information d’authentification distincte, limitée au compte.

Implémenter les lectures conditionnelles et partielles

Dans cette étape, vous utilisez les métadonnées R2 pour déterminer si un corps de réponse est nécessaire. Un ETag joue le rôle d’étiquette de version du fichier. Lorsqu’un client possède déjà une copie, il envoie cette étiquette dans If-None-Match pour demander si le fichier a changé. En cas de correspondance, le serveur renvoie 304 Not Modified sans corps, ce qui évite de télécharger à nouveau les mêmes octets. Une requête Range permet à un lecteur de récupérer une partie d’un fichier volumineux ou de reprendre un téléchargement interrompu. Elle demande des positions d’octets inclusives et produit 206 Partial Content, avec un en-tête Content-Range décrivant la portion.

Utilisez ce gestionnaire. head() lit les métadonnées sans lire les octets. L’appel get() ultérieur inclut onlyIf.etagMatches, afin qu’un objet modifié entre ces deux appels ne puisse pas être renvoyé avec des métadonnées obsolètes. Ce point d’accès prend en charge une seule plage et If-Range fondé sur un ETag ; la syntaxe non prise en charge des plages multiples renvoie 400. Lorsque l’ETag de If-Range est différent, une réponse complète 200 permet au client de remplacer son ancienne copie.

cat > src/index.js <<'JS'
import { authorized } from "./auth.js";
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path === "/health") return new Response("ok");
    if (!await authorized(request, env)) return new Response("Unauthorized", { status: 401 });
    if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
    if (path !== "/documents/report.txt") return new Response("Not found", { status: 404 });
    const key = path.slice(1);
    const metadata = await env.DOCUMENTS.head(key);
    if (!metadata) return new Response("Not found", { status: 404 });
    const headers = new Headers({ "ETag": metadata.httpEtag,
      "Last-Modified": metadata.uploaded.toUTCString(), "Accept-Ranges": "bytes",
      "Cache-Control": "private, no-store" });
    metadata.writeHttpMetadata(headers);
    // GET validators use weak comparison: W/"value" and "value" can match.
    const noneMatch = request.headers.get("If-None-Match");
    if (noneMatch && noneMatch.split(",").some(tag => tag.trim() === "*" || tag.trim().replace(/^W\//, "") === metadata.httpEtag))
      return new Response(null, { status: 304, headers });
    const since = Date.parse(request.headers.get("If-Modified-Since") || "");
    const uploadedSeconds = Math.floor(metadata.uploaded.getTime() / 1000) * 1000;
    if (!noneMatch && Number.isFinite(since) && uploadedSeconds <= since)
      return new Response(null, { status: 304, headers });
    let range = request.headers.get("Range");
    const ifRange = request.headers.get("If-Range");
    if (ifRange && ifRange !== metadata.httpEtag) range = null;
    let start = 0, end = metadata.size - 1;
    if (range) {
      const match = /^bytes=(\d*)-(\d*)$/.exec(range);
      // This endpoint supports exactly one range, not multipart ranges.
      if (!match || (!match[1] && !match[2]))
        return new Response("Invalid range", { status: 400 });
      if (!match[1]) { start = Math.max(0, metadata.size - Number(match[2])); }
      else { start = Number(match[1]); if (match[2]) end = Math.min(Number(match[2]), end); }
      if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start > end || start >= metadata.size) {
        headers.set("Content-Range", `bytes */${metadata.size}`);
        return new Response("Range not satisfiable", { status: 416, headers });
      }
      headers.set("Content-Range", `bytes ${start}-${end}/${metadata.size}`);
    }
    // Do not mix a HEAD result with bytes from an object replaced in between.
    const object = await env.DOCUMENTS.get(key, { onlyIf: { etagMatches: metadata.etag },
      ...(range ? { range: { offset: start, length: end - start + 1 } } : {}) });
    if (!object) return new Response("Not found", { status: 404 });
    if (!("body" in object)) return new Response("Object changed; retry", { status: 412 });
    headers.set("Content-Length", String(range ? end - start + 1 : metadata.size));
    return new Response(object.body, { status: range ? 206 : 200, headers });
  }
};
JS

La position de début demandée est indexée à partir de zéro. Un suffixe tel que bytes=-3 désigne les trois derniers octets. Une position de début située après la fin de l’objet produit 416 avec Content-Range: bytes */SIZE. La validation conditionnelle est prioritaire sur la sélection de la plage. Lorsque les deux sont présents, If-None-Match est prioritaire sur la validation par date.

Créez le secret local de l’application et vérifiez le bundle :

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

Comparer les corps locaux complets et partiels

Dans cette étape, vous alimentez uniquement le stockage local et examinez les véritables en-têtes HTTP. Un objet local est distinct de l’objet distant créé plus tard, même si les deux utilisent la même clé.

npx wrangler r2 object put "$BUCKET/documents/report.txt" --local --file document.txt --content-type text/plain
npx wrangler dev --ip 127.0.0.1 --port 8787 > dev.log 2>&1 &
DEV_PID=$!

Attendez le message indiquant que le serveur est prêt dans dev.log, puis chargez le secret synthétique de l’application :

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

Enregistrez séparément les en-têtes et le corps de la réponse complète. -D écrit les en-têtes dans un fichier :

curl -fsS -D full.headers -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/report.txt -o full.txt
cmp document.txt full.txt
cat full.headers

Vérifiez la présence de 200, du type de contenu stocké, d’un ETag entre guillemets et de Accept-Ranges: bytes. Copiez l’ETag exact, y compris ses guillemets doubles, dans ETAG, entre les guillemets simples indiqués ci-dessous :

ETAG='"COPY_ETAG_HERE"'
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "If-None-Match: $ETAG" http://127.0.0.1:8787/documents/report.txt

Vérifiez la présence de 304 et l’absence de corps. Un validateur à jour évite un transfert complet ; il ne rend pas le bucket public.

curl -sS -D range.headers -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=0-4" http://127.0.0.1:8787/documents/report.txt -o range.txt
head -c 5 document.txt > expected-range.txt
cmp expected-range.txt range.txt
cat range.headers

Vérifiez la présence de 206, de Content-Range: bytes 0-4/SIZE et de cinq octets exactement, correspondant au fichier. Demandez maintenant une position de début impossible à satisfaire :

curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=99999-" http://127.0.0.1:8787/documents/report.txt

Vérifiez la présence de 416, de l’en-tête bytes */SIZE et du message Range not satisfiable. La vérification de la plateforme répète ces lectures de manière indépendante.

Vérifier la diffusion conditionnelle distante

Dans cette étape, vous préparez indépendamment le fichier distant de référence et publiez le gestionnaire. Arrêtez le serveur local, puis téléversez le même fichier synthétique en utilisant explicitement l’option --remote :

kill "$DEV_PID"
wait "$DEV_PID" 2>/dev/null || true
npx wrangler r2 object put "$BUCKET/documents/report.txt" --remote --file document.txt --content-type text/plain --env-file=.env.management
npx wrangler deploy
npx wrangler secret bulk .dev.vars

Copiez l’URL déployée dans BASE_URL. Attendez que la vérification de l’état de santé renvoie ok ; si le nouveau déploiement est encore en cours de propagation, réessayez les lectures pendant une minute au maximum.

BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/health"
curl -fsS -D remote.headers -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/report.txt" -o remote.txt
cmp document.txt remote.txt
cat remote.headers

Utilisez l’ETag distant indiqué dans remote.headers, et non une valeur locale mémorisée. Répétez les requêtes conditionnelles et partielles :

ETAG='"COPY_REMOTE_ETAG_HERE"'
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "If-None-Match: $ETAG" "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=0-4" "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=99999-" "$BASE_URL/documents/report.txt"

Vérifiez la présence de 304 sans corps, de 206 avec les cinq premiers octets du fichier de référence et de 416 avec la limite de taille correcte. Dans le Dashboard, vérifiez la liaison exacte du Worker et l’objet du bucket. Laissez l’URL publique du bucket et les domaines personnalisés désactivés ; les comparaisons des en-têtes HTTP et du corps constituent les preuves de référence pour les plages.

Liaison DOCUMENTS du Worker vers le compartiment R2 privé

Cet exemple montre DOCUMENTS relié au compartiment privé exact. Le suffixe du nom généré sera différent.

Rapport synthétique dans un compartiment privé Standard

La ligne affiche report.txt avec le type text/plain, la classe Standard et une taille de 41 B, tandis que Public Access reste Disabled. Les noms générés et les dates sont des exemples. La valeur agrégée Bucket Size peut rester à 0 B par retard de mise à jour ; la ligne et la comparaison des octets prouvent que le fichier existe. Les en-têtes HTTP et les comparaisons du corps vérifient les réponses conditionnelles et par plage.

Supprimer l’application et le bucket distants

Dans cette étape, vous supprimez uniquement le Worker et les objets de cette expérience, tout en restant autorisé. Le bucket privé ne disparaît pas lorsque son Worker est supprimé.

npx wrangler delete

Confirmez le nom exact du Worker généré. Supprimez explicitement l’unique objet téléversé, puis supprimez le bucket :

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

Seul documents/report.txt a été créé à distance. Si d’autres objets existent, inspectez ce bucket précis et établissez leur propriétaire avant de les supprimer.

Actualisez les listes des Workers et des buckets dans le Dashboard, puis exécutez la vérification de nettoyage de la plateforme. Les échecs d’authentification ou de réseau ne permettent pas de conclure à une suppression réussie.

Révoquer les identifiants restants

Dans cette étape, vous révoquez le jeton de gestion de cette expérience depuis la page API Tokens de votre profil, supprimez le secret local de l’application et fermez l’autorisation de la VM. Effectuez cette étape uniquement une fois la vérification de nettoyage précédente réussie.

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

Vérifiez que loggedIn: false est présent. La révocation du jeton de gestion constitue un contrôle manuel distinct dans le Dashboard ; la suppression du fichier local ne le révoque pas. Ne modifiez pas la connexion ordinaire au Dashboard ni les jetons des autres expériences.

Résumé

Utiliser les métadonnées R2 pour les réponses conditionnelles, diffuser des plages d’octets uniques, gérer les requêtes impossibles à satisfaire et nettoyer le service de téléchargement privé.