Exportar archivos a Amazon S3 con PHP: guía completa
Para exportar un archivo local a Amazon S3 desde PHP, instala el SDK de AWS y llama a
putObject con un SourceFile. Esta guía desarrolla un
script de CLI que almacena objetos privados con metadatos y cifrado del lado del servidor,
informa de los fallos mediante su estado de salida y admite subidas multiparte.
También descargarás el objeto de forma independiente para comprobar que S3 recibió exactamente
los mismos bytes.
Requisitos previos y preparación
Usa un shell Bash en Linux con PHP, Composer 2 y AWS CLI v2 instalados. Los ejemplos se probaron con PHP 8.5.10, Composer 2.10.3, AWS CLI 2.37.10 y AWS SDK for PHP 3.399.2. PHP 8.5 es una rama de versiones con mantenimiento; usa su versión de parche actual para las instalaciones nuevas. Estas son las versiones probadas, no las versiones mínimas del SDK.
El SDK requiere PHP 8.1 o posterior y SimpleXML.
Este tutorial también usa las extensiones cURL y OpenSSL de PHP para HTTPS. Instala las herramientas
que falten siguiendo la guía de instalación de PHP,
las instrucciones de descarga de Composer y las
instrucciones de instalación de AWS CLI v2.
GNU cmp, que se usa para
comparar archivos, se incluye en el paquete GNU Diffutils.
Antes de crear los archivos del proyecto, comprueba cada requisito previo:
(
for tool in php composer aws cmp; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf 'Missing prerequisite: %s\n' "$tool" >&2
exit 1
fi
done
php -r 'foreach (["simplexml", "curl", "openssl"] as $extension) {
if (!extension_loaded($extension)) {
fwrite(STDERR, "Missing PHP extension: $extension\n"); exit(1);
}
}'
)
Instala el SDK de AWS para PHP
Trabaja en un directorio nuevo y vacío, fuera del directorio web público de tu aplicación. Guarda
esto como composer.json; no reemplaces el manifiesto de una aplicación existente:
{
"require": {
"aws/aws-sdk-php": "3.399.2"
}
}
Instala la versión probada del SDK en la carpeta vendor/ de este directorio:
COMPOSER=composer.json COMPOSER_VENDOR_DIR=vendor \
composer install --no-interaction --no-plugins --no-scripts
Conserva el archivo composer.lock generado para obtener instalaciones reproducibles.
Si la instalación falla, resuelve el problema de requisitos previos o de red indicado y repite
el mismo comando en este directorio. No necesitas eliminar el manifiesto ni el archivo de bloqueo.
La guía de uso básico de Composer explica cómo el archivo de bloqueo
fija las versiones de las dependencias.
Configura las credenciales de AWS
Usa un perfil de AWS existente y autorizado. Tanto la CLI como el SDK admiten el proveedor de credenciales compartidas. Elige el perfil y la región de tu bucket, y luego comprueba la cuenta antes de subir archivos:
export AWS_PROFILE=default
export AWS_DEFAULT_REGION=us-east-1
aws sts get-caller-identity --query Account --output text
Reemplaza default y us-east-1 con tus propios valores.
Si las credenciales han caducado, renueva tu sesión autorizada antes de continuar. Las credenciales
del entorno tienen prioridad sobre los perfiles, así que comprueba si hay credenciales exportadas
por error cuando la cuenta no sea la esperada. Nunca pongas claves de acceso en el archivo PHP.
En las aplicaciones alojadas en AWS, prefiere un rol de IAM para la carga de trabajo en lugar de
claves de larga duración.
Crea un bucket de S3 sencillo
El script usa un bucket de S3 de uso general, privado y existente. Si necesitas uno, pide al
administrador de tu bucket que siga la guía de creación de buckets
de AWS. Mantén activada la opción Bloquear acceso público y establece Propiedad del objeto en
Propietario del bucket obligatorio. Usa la región real del bucket en
AWS_DEFAULT_REGION; la administración del bucket es independiente del script de
exportación.
Para los comandos siguientes, establece un nombre de bucket y una clave de objeto nueva bajo un prefijo que te pertenezca:
export S3_BUCKET=your-private-bucket
export S3_KEY=php-export-test/sample.bin
Reemplaza ambos valores de ejemplo. Subir un archivo a una clave existente reemplaza su objeto actual o crea una versión nueva si el control de versiones está activado. Usa claves únicas para estas pruebas y ejecuta una exportación a la vez.
Desarrolla un script de PHP para exportar archivos
Guarda este programa completo como export.php, junto a
composer.json. Sus argumentos son el archivo local, el bucket, la clave de objeto y
un modo opcional, que puede ser put o multipart:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Aws\Exception\AwsException;
use Aws\Exception\MultipartUploadException;
use Aws\S3\MultipartUploader;
use Aws\S3\S3Client;
$mode = $argv[4] ?? 'put';
if ($argc < 4 || $argc > 5 || !in_array($mode, ['put', 'multipart'], true)) {
fwrite(STDERR, "Usage: php export.php LOCAL_FILE BUCKET KEY [put|multipart]\n");
exit(2);
}
[$script, $sourceFile, $bucket, $key] = $argv;
$region = getenv('AWS_DEFAULT_REGION');
if (!$region || $bucket === '' || $key === '') {
fwrite(STDERR, "Set AWS_DEFAULT_REGION and provide a nonempty bucket and key.\n");
exit(2);
}
if (!is_file($sourceFile) || !is_readable($sourceFile)) {
fwrite(STDERR, "Cannot read source file: $sourceFile\n");
exit(2);
}
try {
$s3Client = new S3Client([
'version' => '2006-03-01',
'region' => $region,
'retries' => 0,
'http' => ['connect_timeout' => 10, 'timeout' => 60],
]);
$options = [
'Bucket' => $bucket,
'Key' => $key,
'ContentType' => 'application/octet-stream',
'Metadata' => [
'original-filename' => rawurlencode(basename($sourceFile)),
'upload-date' => gmdate('c'),
],
'ServerSideEncryption' => 'AES256',
'CacheControl' => 'no-store',
];
if ($mode === 'multipart' && filesize($sourceFile) > 0) {
$uploader = new MultipartUploader($s3Client, $sourceFile, [
'bucket' => $bucket,
'key' => $key,
'part_size' => 5 * 1024 * 1024,
'concurrency' => 1,
'params' => $options,
]);
$uploader->upload();
} else {
$s3Client->putObject($options + ['SourceFile' => $sourceFile]);
}
echo "Export complete: s3://$bucket/$key\n";
} catch (Throwable $e) {
if (isset($uploader)) {
$id = $uploader->getState()->getId();
if (!empty($id['UploadId'])) {
try {
$s3Client->abortMultipartUpload($id);
fwrite(STDERR, "Aborted incomplete multipart upload.\n");
} catch (Throwable $abortError) {
fwrite(STDERR, "Abort failed; clean up upload ID: {$id['UploadId']}\n");
}
}
}
if ($e instanceof MultipartUploadException) {
fwrite(STDERR, "Multipart export failed for: $sourceFile\n");
} elseif ($e instanceof AwsException) {
fwrite(STDERR, 'S3 export failed: ' . ($e->getAwsErrorCode() ?? 'request error') . "\n");
fwrite(STDERR, 'Request ID: ' . ($e->getAwsRequestId() ?? 'unavailable') . "\n");
} else {
fwrite(STDERR, "Export failed; check the local file, SDK installation, and credentials.\n");
}
exit(1);
}
Crea un archivo de prueba pequeño y súbelo:
printf 'S3 export check\n' > sample.bin &&
php export.php ./sample.bin "$S3_BUCKET" "$S3_KEY"
El script imprime Export complete seguido de la URI de S3 del objeto privado solo después de que el SDK termine la solicitud. La URI identifica el objeto; no es un enlace de descarga público. Una exportación fallida escribe información de diagnóstico en stderr y termina con un estado de salida distinto de cero. El programa lee tu archivo local sin eliminarlo; mantenlo sin cambios hasta que termine la verificación.
Gestiona los metadatos de archivos de forma eficiente
El array $options aplica los mismos metadatos, tipo de contenido, política de
caché y cifrado SSE-S3 a ambos modos de subida. original-filename usa codificación
porcentual para que los espacios y los caracteres no ASCII puedan representarse en un encabezado
HTTP de metadatos. upload-date registra una marca de tiempo UTC. Las claves de
S3 son independientes de los nombres de archivo locales, así que conserva la clave completa para
localizar un archivo exportado.
application/octet-stream es adecuado para archivos binarios de cualquier tipo. Establece un
ContentType más específico cuando tu aplicación conozca el formato y elige
CacheControl según las necesidades de quienes consumen esos archivos. El ejemplo
usa no-store. AES256 solicita explícitamente
cifrado SSE-S3;
no concede acceso público ni reemplaza los controles de acceso.
Prueba y depura tu integración
Comprueba los metadatos almacenados y el cifrado, y luego descarga el objeto en un directorio local nuevo para la verificación:
aws s3api head-object --bucket "$S3_BUCKET" --key "$S3_KEY" \
--region "$AWS_DEFAULT_REGION" \
--query '{bytes:ContentLength,type:ContentType,encryption:ServerSideEncryption,metadata:Metadata}'
(
mkdir s3-verify &&
aws s3api get-object --bucket "$S3_BUCKET" --key "$S3_KEY" \
--region "$AWS_DEFAULT_REGION" s3-verify/download.bin &&
cmp -- ./sample.bin s3-verify/download.bin
)
La respuesta de metadatos debería mostrar AES256, tu nombre de archivo y el
número de bytes esperado. cmp devuelve cero cuando todos los bytes
coinciden, incluso en un archivo vacío. Usa un directorio de verificación nuevo para cada ejecución.
Si una descarga falla, elimina solo el directorio temporal s3-verify que
creaste y luego repite el bloque. Este rechaza los directorios existentes para proteger las
descargas anteriores.
No uses el ETag de S3 como hash de archivo de uso general; los objetos multiparte pueden tener una
semántica de ETag diferente.
Prueba con un archivo binario no textual propio y un archivo vacío, con claves distintas, antes de integrar el script en una tarea. En una tarea programada, captura stderr y comprueba el estado de salida del proceso. Mantén las credenciales, las URL firmadas y los mensajes de excepción del SDK sin procesar fuera de los registros accesibles para los usuarios.
Casos de uso avanzados: subidas multiparte
AWS recomienda las subidas multiparte para objetos de más de 100 MB.
El script también te permite probar esa vía con un archivo de prueba más pequeño: elige un archivo
de más de 5 MiB para que abarque al menos dos partes, guárdalo como sample.bin y
selecciona una clave nueva:
php export.php ./sample.bin "$S3_BUCKET" "$S3_KEY" multipart
Después, ejecuta las mismas comprobaciones de metadatos y bytes. Para este archivo de prueba, el
componente de subida usa partes de 5 MiB, con una sola parte en transferencia a la vez, y espera a
CompleteMultipartUpload antes de informar del éxito. El SDK puede aumentar el tamaño de las
partes para archivos mucho más grandes. Los archivos vacíos usan putObject
incluso cuando seleccionas el modo multiparte, porque no tienen partes que ensamblar.
Los reintentos están desactivados, y cada solicitud HTTP tiene un tiempo de espera de conexión de 10 segundos y un tiempo de espera de transferencia de 60 segundos. Estos límites se aplican a cada solicitud, no son un plazo para toda la subida multiparte. Una conexión lenta puede fallar incluso con un archivo pequeño. Este ejemplo anula las subidas multiparte fallidas en lugar de reanudarlas; resuelve el problema y vuelve a ejecutarlo con el mismo archivo local de origen. Si la propia anulación falla, stderr incluye el ID de la subida para que el administrador de tu bucket la elimine. Las partes subidas generan cargos de almacenamiento hasta que la subida se complete o se anule, como explica la descripción general de las subidas multiparte de AWS.
La pérdida de una respuesta de finalización puede dejar un resultado incierto: es posible que S3 haya guardado el objeto aunque el cliente informe de un fallo. Inspecciona la clave exacta y los bytes antes de reintentarlo. Una subida multiparte sin terminar no es un objeto completo.
Buenas prácticas de seguridad
Concede a la identidad de exportación s3:PutObject para el prefijo que le
pertenece y s3:AbortMultipartUpload para la limpieza tras un fallo. La identidad de
verificación también necesita s3:GetObject para inspeccionar y descargar el
objeto. Limita estos permisos al bucket y las claves previstos; el script no necesita permiso
para crear buckets ni cambiar sus políticas. AWS enumera los permisos pertinentes en su
descripción general de las subidas multiparte.
El ejemplo no envía ninguna ACL y presupone un bucket privado. Usa SSE-S3 explícitamente; una política de bucket que exija SSE-KMS requiere un cambio deliberado en la configuración de cifrado y los permisos de KMS correspondientes. Mantén activada la verificación de certificados TLS.
Solución de problemas comunes
- AccessDenied: Confirma la cuenta seleccionada, los permisos del prefijo y la política de cifrado del bucket.
- Errores de región o de endpoint: Haz que
AWS_DEFAULT_REGIONcoincida con la región del bucket. - Archivo no encontrado: Pasa una ruta local legible. Pon entre comillas los nombres de archivo
con espacios; antepone
./en el shell a los nombres de archivo que comiencen con un guion. - Fallo de subida multiparte: Revisa stderr para detectar un fallo de anulación. No trates una subida multiparte restante como un archivo almacenado y verifica cualquier finalización incierta antes de reintentarlo.
- Los bytes no coinciden: Confirma que la clave y el archivo local sean los que usaste para la subida y comprueba si otro proceso de escritura reemplazó el objeto.
Conclusión y próximos pasos
Una vez que el objeto privado supere la comparación tras la descarga, usa el mismo manejo de subidas y estados de salida en tu tarea de PHP. Mantén comprobaciones independientes de bytes en tus pruebas de integración y conserva los archivos locales de origen hasta que la exportación esté confirmada. Para reanudar una transferencia multiparte fallida, comienza con la documentación de recuperación de MultipartUploader de AWS y decide cómo tu aplicación conservará el estado de la subida y eliminará las partes abandonadas.
