Export files to Amazon S3 using PHP: a comprehensive guide
To export a local file to Amazon S3 from PHP, install the AWS SDK and call putObject with a
SourceFile. This guide builds a CLI script that stores private objects with metadata and
server-side encryption, reports failures through its exit status, and supports multipart uploads.
You will also download the object independently to check that S3 received the exact bytes.
Prerequisites and setup
Use a Bash shell on Linux with PHP, Composer 2, and the AWS CLI v2 installed. The examples were tested with PHP 8.5.10, Composer 2.10.3, AWS CLI 2.37.10, and AWS SDK for PHP 3.399.2. PHP 8.5 is a maintained release branch; use its current patch release for new installations. These are tested versions, rather than the SDK’s minimum versions.
The SDK requires PHP 8.1 or later and SimpleXML.
This walkthrough also uses PHP’s cURL and OpenSSL extensions for HTTPS. Install missing tools using
the PHP installation guide,
Composer download instructions, and
AWS CLI v2 installation instructions.
GNU cmp, used to
compare files, is supplied by the GNU Diffutils package.
Before creating project files, check each prerequisite:
(
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);
}
}'
)
Installing AWS SDK for PHP
Work in a new, empty directory, outside your application’s public web directory. Save this as
composer.json; do not replace an existing application’s manifest:
{
"require": {
"aws/aws-sdk-php": "3.399.2"
}
}
Install the tested SDK version into this directory’s vendor/ folder:
COMPOSER=composer.json COMPOSER_VENDOR_DIR=vendor \
composer install --no-interaction --no-plugins --no-scripts
Keep the generated composer.lock for reproducible installations. If installation fails, fix the
reported prerequisite or network problem and repeat the same command in this directory. You do not
need to delete the manifest or lockfile. Composer’s
basic usage guide explains how the lockfile pins dependencies.
Configuring AWS credentials
Use an existing authorized AWS profile. The CLI and SDK both support the shared credentials provider. Choose the profile and your bucket’s region, then check the account before uploading:
export AWS_PROFILE=default
export AWS_DEFAULT_REGION=us-east-1
aws sts get-caller-identity --query Account --output text
Replace default and us-east-1 with your own values. If credentials are expired, refresh your
authorized session before continuing. Environment credentials take precedence over profiles, so
check for an unintended exported credential set when the account is unexpected. Never put access
keys in the PHP file. On AWS-hosted applications, prefer a workload IAM role over long-lived keys.
Creating a simple S3 bucket
The script uses an existing private, general purpose S3 bucket. If you need one, have your bucket
administrator follow AWS’s bucket creation guide.
Keep Block Public Access enabled and Object Ownership set to Bucket owner enforced. Use the bucket’s
actual region in AWS_DEFAULT_REGION; bucket administration is separate from the export script.
For the commands below, set a bucket name and a fresh object key under a prefix you own:
export S3_BUCKET=your-private-bucket
export S3_KEY=php-export-test/sample.bin
Replace both example values. Uploading to an existing key replaces its current object, or creates a new version if versioning is enabled. Use unique keys for these tests and run one export at a time.
Developing a PHP script to export files
Save this complete program as export.php, next to composer.json. Its arguments are the local
file, bucket, object key, and an optional mode, either put or 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);
}
Create a small fixture and upload it:
printf 'S3 export check\n' > sample.bin &&
php export.php ./sample.bin "$S3_BUCKET" "$S3_KEY"
The script prints Export complete followed by the private object’s S3 URI only after the SDK finishes the request. The URI identifies the object; it is not a public download link. A failed export writes diagnostics to stderr and exits nonzero. The program reads your local file without deleting it; keep that file unchanged until verification finishes.
Handling file metadata efficiently
The $options array applies the same metadata, content type, cache policy, and SSE-S3 encryption
to both upload modes. original-filename is percent-encoded so spaces and non-ASCII characters can
be represented in an HTTP metadata header. upload-date records a UTC timestamp. S3 keys are
independent of local filenames, so keep the full key when locating an exported file.
application/octet-stream suits arbitrary binary files. Set a more specific ContentType when your
application knows the format, and choose CacheControl for its consumers. The example uses
no-store. AES256 explicitly requests
SSE-S3 encryption;
it does not grant public access or replace access controls.
Testing and debugging your integration
Check the stored metadata and encryption, then download into a fresh local verification directory:
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
)
The metadata response should show AES256, your filename, and the expected byte count. cmp
returns zero when every byte matches, including for an empty file. Use a fresh verification
directory for each run. If a download fails, remove only the disposable s3-verify directory you
created, then repeat the block. It refuses an existing directory to protect earlier downloads.
Do not use the S3 ETag as a general-purpose file hash; multipart objects can have different ETag
semantics.
Try your own non-text binary file and an empty file with distinct keys before wiring the script into a job. In a scheduled job, capture stderr and check the process exit status. Keep credentials, signed URLs, and raw SDK exception messages out of logs exposed to users.
Advanced use cases: multipart uploads
AWS recommends multipart uploads for objects above 100 MB.
The script also lets you exercise that path with a smaller fixture: choose a file larger than
5 MiB so it spans at least two parts, save it as sample.bin, and select a fresh key:
php export.php ./sample.bin "$S3_BUCKET" "$S3_KEY" multipart
Run the same metadata and byte checks afterward. For this fixture, the uploader uses 5 MiB parts
with one part in flight, and waits for CompleteMultipartUpload before printing success. The SDK
can increase the part size for much larger files. Empty files use
putObject even when you select multipart mode, because they have no parts to assemble.
Retries are disabled, and each HTTP request has a 10-second connection timeout and a 60-second transfer timeout. These are per-request limits, not a deadline for the entire multipart upload. A slow connection can fail even for a small file. This example aborts failed multipart uploads instead of resuming them; fix the problem and rerun with the same local source. If abort itself fails, stderr includes the upload ID for your bucket administrator to remove. Uploaded parts incur storage charges until completed or aborted, as AWS’s multipart overview explains.
A lost completion response can leave an uncertain outcome: S3 may have committed the object even though the client reports failure. Inspect the exact key and bytes before retrying. An unfinished multipart upload is not a completed object.
Security best practices
Grant the export identity s3:PutObject for its owned prefix and s3:AbortMultipartUpload for
failure cleanup. The verification identity also needs s3:GetObject to inspect and download the
object. Keep these permissions scoped to the intended bucket and keys; the script does not need
permission to create buckets or change their policies. AWS lists the relevant permissions in its
multipart overview.
The example sends no ACL and assumes a private bucket. It explicitly uses SSE-S3; a bucket policy that requires SSE-KMS needs a deliberate encryption configuration change and matching KMS permissions. Keep TLS certificate verification enabled.
Troubleshooting common issues
- AccessDenied: Confirm the selected account, prefix permissions, and bucket encryption policy.
- Region or endpoint errors: Match
AWS_DEFAULT_REGIONto the bucket’s region. - Missing file: Pass a readable local path. Quote filenames with spaces; prefix a filename that
starts with a hyphen with
./in the shell. - Multipart failure: Check stderr for an abort failure. Do not treat a remaining multipart upload as a stored file, and verify an uncertain completion before retrying.
- Byte mismatch: Confirm the key and local file are the ones you uploaded, and check whether another writer replaced the object.
Conclusion and next steps
Once the private object passes the download comparison, use the same upload and exit-status handling in your PHP job. Keep independent byte checks in your integration tests and retain local sources until the export is confirmed. For resuming a failed multipart transfer, start with AWS’s MultipartUploader recovery documentation and decide how your application will retain upload state and clean up abandoned parts.
