Exporter des fichiers par lot vers Amazon S3 avec curl et AWS CLI
Utilisez un petit programme de signature basé sur le SDK AWS pour générer des URL PUT présignées,
puis laissez curl téléverser chaque fichier vers Amazon S3. Le script Bash
ci-dessous exporte les fichiers ordinaires situés directement dans un répertoire, y compris les
fichiers cachés et vides, et signale un échec si un fichier sélectionné échoue. AWS CLI permet de
vérifier les informations d’accès et d’effectuer une vérification indépendante par téléchargement ;
aws s3 presign signe lui-même les téléchargements, pas les téléversements.
Prérequis
Cet exemple vise Linux avec Bash, GNU stat, file, curl, AWS CLI v2 et Corepack installés.
Utilisez Node.js 24.15 ou une version ultérieure de la branche 24.x, ou Node.js 26.x. Il s’agit des
prérequis de cet exemple, plutôt que du minimum requis par le SDK AWS à l’exécution ; Node exécute
le programme de signature TypeScript sans étape de compilation. Si AWS CLI n’est pas installé,
suivez son guide d’installation.
Le programme de signature a été vérifié avec Node.js 24.15.0, 26.0.0, 26.5.0 et 26.8.1. Le processus complet de téléversement utilise le SDK AWS 3.1148.0, Yarn 4.12.0, AWS CLI 2.37.10 et curl 8.22.0. Les versions de dépendances fixées ci-dessous rendent cette configuration reproductible ; elles n’exigent pas de rétrograder une installation existante de la CLI.
Vous avez également besoin d’un bucket S3 à usage général existant, de sa région et d’un profil AWS
authentifié ayant accès à votre préfixe de destination. Utilisez le processus de connexion existant
de votre organisation. La CLI et le SDK prennent tous deux en compte AWS_PROFILE ;
les informations d’accès de session fournies par des variables d’environnement doivent inclure
AWS_SESSION_TOKEN. Ne placez pas d’informations d’accès dans ces scripts.
Créez un projet distinct avec les deux dépendances du programme de signature. Ce bloc laisse votre shell dans son répertoire d’origine et refuse d’utiliser un répertoire de projet existant :
(
set -euo pipefail
for tool in node corepack aws curl file stat; do
command -v "$tool" >/dev/null || { echo "Missing prerequisite: $tool" >&2; exit 1; }
done
mkdir s3-batch-export
printf '%s\n' '{"private":true,"type":"module","packageManager":"yarn@4.12.0"}' \
> s3-batch-export/package.json
printf 'nodeLinker: node-modules\n' > s3-batch-export/.s3-batch-export.yarnrc.yml
touch s3-batch-export/yarn.lock
cd -- s3-batch-export
YARN_RC_FILENAME=.s3-batch-export.yarnrc.yml corepack yarn add --exact \
@aws-sdk/client-s3@3.1148.0 @aws-sdk/s3-request-presigner@3.1148.0
)
Le fichier yarn.lock vide fait de ce projet un projet Yarn distinct. Le fichier
de configuration nommé empêche l’installation d’hériter des fichiers .yarnrc.yml
des répertoires parents. Si l’installation échoue après la création, inspectez le nouveau
répertoire et réessayez la même commande YARN_RC_FILENAME=… corepack yarn add à l’intérieur ;
ne relancez pas mkdir et ne supprimez pas un projet existant. Une fois
l’installation réussie, placez-vous dans ce répertoire pour les exemples suivants :
cd -- s3-batch-export
Comprendre les composants
Amazon S3 est un service de stockage d’objets évolutif. Une
pre-signed URL accorde une autorisation limitée dans le temps pour effectuer
un type précis de requête S3 sans exposer vos informations d’accès AWS à la personne qui
l’effectue. La signature couvre la méthode HTTP, le bucket et la clé, l’expiration, ainsi que les
en-têtes que vous choisissez de signer. Une URL signée pour une requête GET ne peut donc pas être
réutilisée pour une requête PUT.
Il ne s’agit pas d’un jeton à usage unique. Vous pouvez réutiliser l’URL jusqu’à son expiration, et une requête PUT réussie remplace l’objet associé à cette clé. Gardez-la hors des journaux, des traces du shell et des sorties de terminal partagées. AWS décrit ces autorisations et ce comportement de remplacement dans son guide de téléversement par URL présignée.
La commande AWS CLI presign
autorise les requêtes GET. Sa sortie ne peut pas autoriser curl -T. Pour
signer une requête PUT, vous avez besoin d’un programme de signature qui vous permet de nommer
l’opération, ce que fait @aws-sdk/s3-request-presigner.
AWS CLI vérifie votre identité et récupère les octets téléversés ; curl
effectue le transfert.
Fournir les autorisations IAM requises
La signature elle-même est calculée localement ; aucune autorisation n’est donc vérifiée pendant la création de l’URL. Les autorisations déterminantes sont celles du signataire, car S3 évalue la politique de l’identité signataire lorsque l’URL présignée est utilisée. Demandez à l’administrateur du bucket une autorisation limitée à votre destination, par exemple :
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::your-bucket-name/exports/*"
}
]
}
Le téléversement exige s3:PutObject. La vérification indépendante par
téléchargement ci-dessous nécessite également s3:GetObject sur ce préfixe.
Les politiques du bucket et des clés de chiffrement peuvent imposer des exigences supplémentaires.
Remplacez your-bucket-name par votre bucket et exports/ par le
préfixe choisi.
Générer des URL PUT présignées avec le SDK AWS
Enregistrez ce code dans presign-put.ts. Il affiche une URL puis se termine, ce qui
facilite son appel ultérieur depuis une boucle du shell :
// presign-put.ts - print a pre-signed URL that authorizes PUT, and only PUT.
import { parseArgs } from 'node:util'
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const { values } = parseArgs({
options: {
bucket: { type: 'string' },
key: { type: 'string' },
region: { type: 'string' },
'content-type': { type: 'string', default: 'application/octet-stream' },
'expires-in': { type: 'string', default: '3600' },
},
})
const { bucket, key, region } = values
if (!bucket || !key || !region) {
throw new Error('Usage: node presign-put.ts --bucket B --key K --region R [--content-type T]')
}
const expiresIn = Number(values['expires-in'])
// SigV4 caps a pre-signed URL at 7 days. Reject longer values here rather than
// relying on the SDK's generic expiry error; fractional values are rejected too.
if (!Number.isInteger(expiresIn) || expiresIn < 1 || expiresIn > 604800) {
throw new Error(`--expires-in must be between 1 and 604800 seconds, got: ${values['expires-in']}`)
}
const client = new S3Client({
region,
maxAttempts: 1,
// curl supplies the body later; omit optional checksum metadata here.
requestChecksumCalculation: 'WHEN_REQUIRED',
})
const url = await getSignedUrl(
client,
new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: values['content-type'] }),
{
expiresIn,
// Require the uploader to send the Content-Type used when signing.
signableHeaders: new Set(['content-type']),
},
)
console.log(url)
Exécutez-le :
node presign-put.ts \
--bucket your-bucket-name \
--key exports/localfile.txt \
--region your-region \
--content-type text/plain \
--expires-in 3600
Remplacez le bucket, la clé et la région par vos valeurs. L’URL contient la signature et son expiration dans la chaîne de requête ; elle ne contient aucune clé secrète, mais quiconque la détient peut écrire cet objet précis, à plusieurs reprises, jusqu’à son expiration. Gardez-la donc hors des journaux.
--expires-in est un plafond, pas une garantie. Si vous signez avec des
informations d’accès temporaires issues d’un rôle IAM, d’une session SSO ou de
sts:AssumeRole, l’URL cesse de fonctionner dès que ces informations d’accès
expirent, souvent bien avant l’expiration demandée.
Dans l’URL générée, X-Amz-SignedHeaders vaut content-type;host. Il s’agit
de la liste que curl doit reproduire exactement. Envoyer un
Content-Type différent, ou omettre cet en-tête, invalide la signature.
La durée de validité maximale de SigV4 est de 7 jours (604 800 secondes).
Téléverser des fichiers avec curl
Utilisez curl avec l’option -T pour préciser
le fichier local à téléverser. L’ajout de --retry rend le téléversement plus
résistant aux problèmes réseau transitoires. L’en-tête Content-Type n’est pas
facultatif ici : il fait partie des en-têtes signés et doit donc correspondre au
--content-type que vous avez fourni au programme de signature. Le sous-shell
limite la gestion stricte des erreurs à cet exemple, même lorsque vous le collez dans une session
Bash existante.
(
set -euo pipefail
CONTENT_TYPE=$(file -b --mime-type -- ./localfile.txt)
# Sign for that exact type, then send that exact type
URL=$(node presign-put.ts \
--bucket your-bucket-name \
--key exports/localfile.txt \
--region your-region \
--content-type "$CONTENT_TYPE")
if status=$(curl --disable -fsS --globoff --retry 3 --retry-delay 2 \
--connect-timeout 10 --max-time 300 --retry-max-time 300 \
-T ./localfile.txt \
-H "Content-Type: $CONTENT_TYPE" \
-o /dev/null -w '%{http_code}' \
"$URL") && [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
echo 'Upload completed.'
else
echo 'Upload failed: expected an HTTP 2xx response.' >&2
exit 1
fi
)
Remplacez ./localfile.txt par le chemin de votre fichier, puis le bucket et la région
par vos propres valeurs. --disable ignore le .curlrc
de l’appelant ; --globoff conserve le sens littéral des accolades et des
crochets dans les noms de fichiers. -f fait terminer
curl avec un code de sortie non nul en cas d’erreur HTTP, au lieu
d’afficher le corps XML de l’erreur S3 comme s’il s’agissait d’un succès, et
-sS masque l’indicateur de progression tout en affichant les erreurs
réelles. La vérification explicite du code 2xx rejette également les redirections, que
-f seul ne considère pas comme des erreurs. N’ajoutez pas
-L : suivre une redirection modifie la requête signée. Chaque tentative
est limitée à 300 secondes, transfert des données compris ; un fichier valide peut donc échouer si
le transfert est lent. La planification des nouvelles tentatives s’arrête également après
300 secondes ; une nouvelle tentative déjà commencée peut durer 300 secondes supplémentaires.
Inspectez la destination avant de réessayer après une réponse incertaine.
Le manuel de curl explique ces options de transfert, de nouvelle
tentative et de gestion des noms de fichiers.
Automatiser les exports de fichiers par lots
Enregistrez ce code dans batch-export.sh, à côté de presign-put.ts.
Il traite les fichiers un par un, continue après un échec individuel et se termine avec un code de
sortie non nul si un fichier sélectionné échoue. Il ignore les répertoires et les liens
symboliques, accepte les noms de fichiers UTF-8 et considère un répertoire vide comme un lot de
zéro fichier traité avec succès. Ne modifiez pas le répertoire d’entrée pendant l’exécution.
#!/usr/bin/env bash
set -euo pipefail # Exit on error, undefined variable, or pipe failure
export LC_ALL=C AWS_MAX_ATTEMPTS=1 AWS_PAGER=""
trap 'echo "Batch interrupted; inspect uploaded objects before retrying." >&2; exit 130' INT
trap 'echo "Batch interrupted; inspect uploaded objects before retrying." >&2; exit 143' TERM
BUCKET="your-bucket-name"
EXPIRE=3600 # URL validity in seconds (1 hour)
FILES_DIR="/path/to/your/files" # Directory containing files to upload
REGION="your-region" # Your S3 bucket region
PRESIGN="./presign-put.ts" # The signer from the previous section
PREFIX="${S3_PREFIX:-exports/}" # Use a separate prefix for this batch
TRANSFER_SECONDS="${S3_TRANSFER_SECONDS:-300}"
# --- pre-flight checks ---
if [[ ! -d "$FILES_DIR" || ! -r "$FILES_DIR" || ! -x "$FILES_DIR" ]]; then
echo "Error: Directory '$FILES_DIR' must exist and be readable/searchable." >&2
exit 1
fi
if [[ ! -f "$PRESIGN" ]]; then
echo "Error: Signer '$PRESIGN' not found. See the previous section." >&2
exit 1
fi
for tool in aws node curl file stat; do
command -v "$tool" >/dev/null || { echo "Missing prerequisite: $tool" >&2; exit 1; }
done
if [[ ! "$TRANSFER_SECONDS" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: S3_TRANSFER_SECONDS must be a positive integer." >&2
exit 1
fi
# Verify the credentials the signer will pick up are present and valid.
if ! caller_arn=$(aws sts get-caller-identity --query Arn --output text 2>/dev/null); then
echo "Error: AWS credentials are not configured properly or are invalid." >&2
echo "Check your selected AWS profile or session credentials." >&2
exit 1
fi
echo "AWS credentials verified for: $caller_arn"
echo "Starting batch export from '$FILES_DIR' to bucket '$BUCKET' in region '$REGION'..."
# --- processing loop ---
failed=0
uploaded=0
# dotglob: a hidden file is an ordinary object to S3, and silently leaving
# .env.example behind is data loss. nullglob: an empty directory must not run
# the loop once with a literal '*'.
shopt -s dotglob nullglob
for file in "$FILES_DIR"/*; do
filename=${file##*/}
# Test for a symlink before -f, which follows them: a link inside FILES_DIR
# would otherwise upload bytes from outside it, under a name from inside it.
if [[ -L "$file" ]]; then
echo " Skipping '$filename': symbolic link."
continue
fi
# Skip directories and anything else that is not a regular file.
[[ -f "$file" ]] || continue
echo "Processing '$filename'..."
# Node's argv decoding is lossy for malformed UTF-8; reject before signing a key.
if ! printf '%s' "$filename" | node --input-type=module -e \
'import { readFileSync } from "node:fs"; new TextDecoder("utf-8", {fatal:true}).decode(readFileSync(0))' \
2>/dev/null; then
echo " Error: Filename is not valid UTF-8." >&2
failed=$((failed + 1))
continue
fi
if [[ ! -r "$file" ]] || ! file_size=$(stat -c%s -- "$file"); then
echo " Error: Cannot read or inspect '$filename'." >&2
failed=$((failed + 1))
continue
fi
# This example limits each single PUT to 5 GiB; use multipart for larger files.
if ((file_size > 5368709120)); then
echo " Skipping '$filename': $((file_size / 1024 / 1024)) MiB exceeds this script’s 5 GiB limit." >&2
echo " Use 'aws s3 cp' instead, which switches to multipart automatically." >&2
failed=$((failed + 1))
continue
fi
if ! content_type=$(file -b --mime-type -- "$file"); then
echo " Error: Cannot determine Content-Type for '$filename'." >&2
failed=$((failed + 1))
continue
fi
echo " Signing a PUT for '$filename' (Content-Type: $content_type)..."
if ! url=$(node "$PRESIGN" \
--bucket "$BUCKET" \
--key "$PREFIX$filename" \
--region "$REGION" \
--content-type "$content_type" \
--expires-in "$EXPIRE"); then
echo " Error: Failed to sign a URL for '$filename'." >&2
failed=$((failed + 1))
continue
fi
echo " Uploading '$filename'..."
# The Content-Type must match the signature. curl -f does not reject redirects;
# require a completed 2xx upload without replaying a signed body at another URL.
if status=$(curl --disable -fsS --globoff --retry 3 --retry-delay 2 \
--connect-timeout 10 --max-time "$TRANSFER_SECONDS" --retry-max-time "$TRANSFER_SECONDS" \
-T "$file" \
-H "Content-Type: $content_type" \
-o /dev/null -w '%{http_code}' \
"$url") && [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
echo " Successfully uploaded '$filename'."
uploaded=$((uploaded + 1))
else
echo " Error: Failed to upload '$filename'." >&2
failed=$((failed + 1))
fi
done
echo "Batch export completed: $uploaded uploaded, $failed failed."
# Surface partial failure to the caller: 'set -e' cannot do this for us, because
# every failure above was deliberately caught so the loop could continue.
if ((failed > 0)); then
exit 1
fi
Remplacez le bucket, le répertoire d’entrée et la région dans le script, puis exécutez-le depuis ce projet :
bash batch-export.sh
Pour deux fichiers transférés avec succès, le résumé est Batch export completed: 2 uploaded, 0 failed. Un
transfert échoué est comptabilisé dans le nombre d’échecs même si les fichiers suivants sont
transférés avec succès. Un répertoire d’entrée absent ou illisible provoque un échec avant tout
transfert. S3_PREFIX prend le pas sur exports/ ;
incluez la barre oblique finale. Les noms conservent les espaces, les crochets, les signes de
pourcentage, les emoji et les sauts de ligne. Les noms UTF-8 mal formés sont comptabilisés comme des
échecs, car ils ne peuvent pas être transmis de manière fiable via les arguments de type chaîne
de Node.
Ce script limite la taille des fichiers à 5 GiB et utilise les mêmes limites de 300 secondes pour
le transfert et les nouvelles tentatives que le téléversement individuel. Augmentez
S3_TRANSFER_SECONDS pour les transferts plus lents. Chaque fichier fait l’objet d’au
plus quatre tentatives, requête initiale comprise. Une nouvelle tentative renvoie le fichier
entier ; elle ne reprend pas une requête PUT interrompue. En cas d’interruption, le script renvoie
un code de sortie non nul sans résumé de fin. Les objets dont le transfert est terminé restent en
place, et une réponse perdue peut laisser un objet effectivement enregistré. Inspectez donc le
préfixe avant de recommencer.
Les téléversements remplacent les objets existants ayant la même clé. Utilisez un nouveau préfixe pour séparer les lots ; les répertoires imbriqués sont ignorés plutôt que mis à plat. Répéter un lot remplace les objets associés aux clés transférées avec succès et ne supprime pas les clés sans rapport avec ce lot.
Vérifiez indépendamment un fichier connu en le téléchargeant et en comparant ses octets. Cela crée un nouveau répertoire local et laisse les fichiers existants intacts :
(
set -euo pipefail
command -v cmp >/dev/null || { echo 'Missing prerequisite: cmp' >&2; exit 1; }
mkdir s3-download-check || exit $?
aws s3api get-object --bucket your-bucket-name --key exports/localfile.txt \
--region your-region s3-download-check/localfile.txt >/dev/null || exit $?
cmp -- ./localfile.txt s3-download-check/localfile.txt || exit $?
echo 'Downloaded bytes match.'
)
Comparez chaque fichier attendu lorsque vous vérifiez un lot complet. Le nombre de succès ne suffit pas à détecter un fichier omis ou un objet incorrect. Si le téléchargement échoue, inspectez et supprimez uniquement le répertoire de téléchargement partiel de cette vérification avant de réexécuter le bloc.
Appliquer les bonnes pratiques de sécurité
Limitez l’accès de l’identité signataire au préfixe du lot et choisissez une durée de validité de
l’URL suffisamment longue pour le transfert. Les informations d’accès temporaires peuvent expirer
avant --expires-in, comme AWS l’explique dans son
guide des URL présignées.
Les scripts ne modifient ni les politiques du bucket ni les paramètres de chiffrement. Pour un
bucket utilisant une clé KMS gérée par le client, confirmez que l’administrateur a accordé l’accès
requis à la clé avant de téléverser les fichiers.
Signer Content-Type lie la signature à cet en-tête, pas au contenu ni à
l’innocuité du fichier. Quiconque détient l’URL peut envoyer des octets différents avec ce même
en-tête. La vérification indépendante par téléchargement vous évite de comparer accidentellement
le mauvais objet ; elle ne transforme pas l’URL en politique de validation du contenu.
Résoudre les problèmes courants
- Échec de la vérification des informations d’accès : Renouvelez votre connexion existante et
vérifiez
AWS_PROFILEou les variables d’environnement de session. Une exécution réussie deaws sts get-caller-identityidentifie l’appelant de la CLI ; elle ne prouve pas que S3 autorise une écriture. Le programme de signature détermine également ses propres informations d’accès. - Le téléversement renvoie HTTP 403 : Vérifiez la région du bucket, la clé, l’expiration de
l’URL, les autorisations du signataire et la valeur exacte du
Content-Typesigné. Le bloc de téléversement écarte délibérément le corps de la réponse S3 ; son message d’échec générique ne permet donc pas d’identifier laquelle de ces causes s’applique. Ne consignez pas l’URL signée pendant le diagnostic. Les conseils de dépannage des téléversements d’AWS couvrent ces vérifications. - Le transfert dépasse le délai imparti : La limite de temps s’applique également aux fichiers valides dont la taille est inférieure à la limite. Inspectez l’objet, ajustez le délai de transfert si nécessaire, puis relancez avec une nouvelle URL. Ajouter des tentatives ne corrige pas les erreurs d’authentification.
- Le fichier est ignoré car il est trop volumineux : Ce script impose un plafond de 5 GiB pour une requête PUT unique. Utilisez un client de téléversement multipartie pour un fichier plus volumineux.
Pour les fichiers plus volumineux, aws s3 cp
gère le téléversement multipartie. Il s’agit d’un transfert distinct utilisant des informations
d’accès, et non d’une requête curl présignée :
aws s3 cp ./large_file.zip s3://your-bucket-name/exports/large_file.zip --region your-region
Conclusion
Utilisez un nouveau préfixe pour votre premier export de fichiers, téléchargez des objets connus pour vérifier leurs octets et vérifiez le code de sortie du script avant de considérer le lot comme terminé. Ce processus open source sépare la signature du transfert tout en rendant visibles les échecs partiels.
Si vos fichiers passent déjà par le traitement de Transloadit, le 🤖 /s3/store (English) Robot peut exporter les résultats d’un Step vers S3 au sein de la même Assembly.
