Terminer et interrompre des téléchargements multipart

CloudflareBeginner
Pratiquer maintenant

Introduction

Un outil d'envoi de sauvegardes doit terminer les téléchargements réussis et libérer les parties abandonnées après une interruption. Vous allez découper un fichier synthétique de taille définie, terminer sa session multipart, puis inspecter et interrompre une autre session inachevée sans modifier les objets déjà terminés.

Commencez par terminer les leçons précédentes sur les objets R2 et les identifiants à portée limitée. Cette nouvelle VM contient Node.js 22.22.0, Wrangler 4.131.1 et AWS SDK 3.888.0. Vous allez créer un bucket Standard privé et ses propres identifiants temporaires. R2 doit être activé ; consultez les limites multipart et la tarification. Les parties incomplètes comptent dans le stockage utilisé. Ce lab ne transfère qu'un petit fichier synthétique et ne nécessite aucun domaine. Ne réutilisez ni les téléchargements ni les buckets précédents.

Créer votre bucket privé de documents

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

Démarrez 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 vos 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 portées 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
npx wrangler whoami --json

Vérifiez que loggedIn: true apparaît. Lisez le nom du compte, même si un seul compte est affiché. 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 toute collision avec une exécution précédente. Le 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-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

Pour gérer le bucket, ouvrez la page API Tokens de votre profil Cloudflare et créez un jeton personnalisé portant le nom de ce lab. Accordez Account → Workers R2 Storage → Edit, puis limitez Account Resources au compte d'apprentissage dont vous avez enregistré l'ID. Définissez une courte durée d'expiration. 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. Plus loin dans cette même étape, vous créez un jeton distinct limité aux objets de ce bucket pour les opérations du SDK S3.

Copiez une seule fois le jeton dans cette invite de la 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 d'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

Listez 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 la liste des objets est vide. Dans ses paramètres, laissez l'URL de développement publique et les domaines personnalisés désactivés. Le nom d'un bucket dans le Dashboard confirme son identité ; les vérifications de téléchargement ultérieures prouveront que les octets sont bien stockés.

L'API compatible S3 permet aux SDK de stockage standard d'accéder à R2. Elle utilise une paire de clés d'accès distincte du jeton d'appareil de Wrangler. Dans R2 Overview, sélectionnez Account Details → API Tokens → Manage, puis créez un User API token portant le nom de la ressource générée par ce lab. Choisissez Object Read & Write, limitez le jeton à ce nouveau bucket précis et sélectionnez une courte durée d'expiration si le formulaire le propose. Ne choisissez ni tous les buckets ni l'accès Admin. Gardez cette page de jeton ouverte jusqu'à avoir enregistré le secret à usage unique.

Utilisez les invites Bash suivantes dans la VM. read -s masque la saisie ; umask 077 rend le fichier d'identifiants lisible uniquement par votre utilisateur. Ces noms sont les variables d'environnement standard d'AWS SDK. Collez l'Access Key ID et la Secret Access Key dans les invites correspondantes, puis appuyez sur Entrée. Ne collez pas la valeur du jeton API général.

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

Écrivez un client SDK standard réutilisable. Le SDK exige une chaîne de région ; R2 utilise auto. La lecture de la configuration existante garantit que les opérations de l'interface de ligne de commande et du SDK ciblent le même compte et le même bucket.

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

Téléversez le manuel synthétique conservé avec Wrangler. Il doit rester inchangé pendant les opérations de finalisation et d'interruption :

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

Démarrer un téléchargement multipart de taille définie

Dans cette étape, vous démarrez un téléchargement multipart : une session de téléchargement côté serveur qui accepte des parties numérotées avant d'assembler l'objet final. Les parties téléversées ne constituent pas encore un objet téléchargeable. L'enregistrement de l'ID du téléchargement vous permet de reprendre ou d'interrompre cette session précise.

Créez un fichier binaire synthétique de 6 Mio avec Python standard. La première partie fera 5 Mio et la dernière 1 Mio. R2 exige des tailles de parties prises en charge ; les parties autres que la dernière doivent faire au moins 5 Mio et utiliser des tailles égales. Ce petit fichier permet de présenter le protocole sans effectuer un transfert volumineux.

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

Conservez upload.json : ce fichier identifie cette opération, mais ne confirme pas sa réussite. Ne relancez pas inutilement la commande de démarrage ; chaque appel crée un autre téléchargement incomplet qu'il faudra supprimer. Continuez à utiliser l’identifiant enregistré lors de la création. Sur le point de terminaison R2 testé, la liste renvoyait une autre chaîne opaque ; comparez la clé exacte de l’objet et utilisez l’identifiant enregistré avec ListParts pour confirmer la session active.

Téléverser les parties dans l'ordre et terminer l'objet

Dans cette étape, vous envoyez les deux parties et indiquez à R2 quels identifiants de parties renvoyés composent l'objet final. La numérotation commence à 1. La requête de finalisation inclut chaque ETag exactement comme il a été renvoyé par le téléchargement de sa partie ; elle ne correspond pas au hachage du fichier source complet effectué de votre côté.

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

Vérifiez la présence de deux lignes indiquant le téléversement des parties, suivies de la ligne de finalisation. Comparez ensuite le fichier téléchargé octet par octet :

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

L'ETag d'un objet multipart n'est pas nécessairement le MD5 du fichier final. La comparaison octet par octet prouve directement que le contenu a été préservé. Dans le Dashboard, ouvrez le bucket de ce lab et recherchez exports/archive.bin ; le manuel conservé doit toujours être présent.

Décochez View prefixes as folders pour afficher les deux clés complètes comme dans cet exemple. Le nom généré de votre bucket sera différent. 6.29 MB est l’affichage décimal de 6 MiB (6 291 456 octets). Le résumé Bucket Size: 0 B peut être en retard ; confirmez le contenu avec les lignes des objets et les octets vérifiés via l’API.

Objet multipart terminé et manuel conservé

Inspecter un téléchargement incomplet

Dans cette étape, vous laissez volontairement un nouveau téléchargement incomplet, puis vous listez la session et ses parties. Les parties incomplètes consomment de l'espace de stockage, même si une liste normale d'objets n'affiche pas de fichier terminé. C'est pourquoi le nettoyage nécessite un inventaire des téléchargements en plus de l'inventaire des objets.

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

L’inventaire multipart contient temporary/unfinished.bin. La requête ListParts utilise l’identifiant enregistré et doit renvoyer la partie 1 de 5,242,880 octets. Ne comparez pas le texte des identifiants et ne créez pas une autre session pour répéter une lecture. Utilisez l’identifiant enregistré avec les API de liste et lancez la vérification tant que ce téléversement existe.

Interrompre uniquement la session abandonnée

Dans cette étape, vous libérez les parties inachevées en interrompant le téléchargement correspondant à leur ID exact. Interrompre une session est différent de supprimer un objet terminé. Cette opération doit laisser intactes l'archive terminée et le manuel.

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

Ce nouveau bucket doit maintenant afficher une liste vide de téléchargements multipart. Le contrôle de la plateforme télécharge également les deux objets terminés pour prouver qu'ils restent inchangés. N'interprétez jamais une liste ayant échoué comme une liste vide.

Supprimer les fichiers terminés et le bucket

Dans cette étape, vous supprimez exactement les deux objets terminés une fois le contrôle de l'interruption réussi. Le nettoyage explicite n'attend pas la règle par défaut du cycle de vie des téléchargements incomplets.

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

Vérifiez que seul le nom généré de ce bucket apparaît. Confirmez son absence dans un inventaire exécuté avec succès, puis lancez le contrôle de nettoyage de la plateforme avant de révoquer les identifiants.

Révoquer l'identifiant du lab et se déconnecter

Dans cette étape, vous fermez les accès laissés par cet exercice. Dans la page R2 API Tokens, révoquez uniquement le jeton d'objet portant le nom de ce lab. Dans la page API Tokens de votre profil, révoquez le jeton de gestion R2 distinct créé pour ce lab. La suppression d'un bucket ne révoque pas un jeton, et la déconnexion de Wrangler ne révoque pas les identifiants S3.

Après la révocation, supprimez le fichier d'identifiants local et déconnectez cette VM :

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

Inspectez l'identité structurée. Son statut non nul est attendu après la déconnexion :

npx wrangler whoami --json || true

Vérifiez que loggedIn: false apparaît ; conservez votre connexion habituelle au Dashboard. Les contrôles de la plateforme vérifient la suppression des identifiants locaux et la déconnexion de Wrangler. Les deux révocations de jetons sont des points de contrôle manuels dans ce lab ; elles ne sont pas déduites de la suppression des fichiers.

Résumé

Terminez un objet multipart avec les octets exacts, inspectez et interrompez les parties inachevées, préservez les autres objets et supprimez les identifiants de stockage.