Đây là bài viết Phần 5 nằm trong series: Laravel File Upload
Trong 4 phần đầu tiên của series hướng dẫn File Upload trong Laravel, mình đã chia sẻ từng bước cách xây dựng một hệ thống upload file đơn giản, với các file được lưu trữ trên ổ cứng cục bộ (local server):
- Phần 1: Tạo form upload file, xử lý lưu file và cấu hình để truy cập qua URL.
- Phần 2: Thêm validation để đảm bảo file đúng định dạng, đúng kích thước yêu cầu.
- Phần 3: Nâng cấp tính năng upload nhiều file cùng lúc
- Phần 4: Lưu thông tin file vào database, hiển thị danh sách và xoá file
Bài viết [Phần 5] hôm nay sẽ hướng dẫn cách upload file lên lưu trữ ở Amazon S3, với các thao tác sau
- Cấu hình Laravel để kết nối với Amazon S3
- Upload file trực tiếp từ form lên S3
- Lấy đường dẫn file để hiển thị cho người dùng
Mục Lục
I. Tạo S3 Bucket và lấy Access Key từ AWS
Trước tiên bạn cần tạo S3 Bucket và lấy Access Key từ AWS. Tham khảo bài viết bên dưới để biết cách lấy các thông tin cấu hình cần thiết: Bucket Name, Region, Access Key ID, Secret Access Key.
https://thuanbui.me/huong-dan-tao-s3-bucket-va-access-key-tren-aws
II. Cấu hình hệ thống
1. Cài đặt package cho S3 Driver
Chúng ta cần phải cài đặt thêm package flysystem-aws-s3 để Laravel có thể kết nối được với AWS S3
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
Code language: JavaScript (javascript)
2. Cấu hình .env
Chỉnh sửa lại thông số của AWS_ trong file cấu hình .env
, nhập vào các thông tin bạn đã lấy ở bước I.
AWS_ACCESS_KEY_ID=xxxxxxxx
AWS_SECRET_ACCESS_KEY=yyyyyyy
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=zzzzzzzzzzzz
III. Upload file lên S3
Chỉnh sửa lại hàm store
trong UploadController
- Thay đổi:
$disk = 'public'
→$disk = 's3'
- Cập nhật:
$storedFilePaths[] = Storage::disk($disk)->url($storedFilePath);
public function store(Request $request)
{
// Kiểm tra request có chứa file không? và có dáp ứng các yêu cầu không
$request->validate([
'files' => 'required|array', // Tên input là 'files[]' trong HTML
'files.*' => 'required|image|mimes:jpg,jpeg,png|max:2048', // max = 2MB mỗi file
]);
// Tạo biến mới để lưu đường dẫn và tên file gốc
$storedFilePaths = []; // Array lưu đường dẫn các file đã lưu thành công
$originalFilenames = []; // Array lưu tên gốc của các file
$uploadedFiles = $request->file('files'); // Lấy array các đối tượng file đã upload
$numberOfFiles = count($uploadedFiles); // Đếm số lượng file đã upload
// Lặp qua từng file trong array $uploadedFiles
foreach ($uploadedFiles as $file) {
// Lấy tên file gốc từ client
$originalFilename = $file->getClientOriginalName();
$originalFilenames[] = $originalFilename; // Thêm tên gốc vào array
// Chuẩn bị các phần của tên file
$filenameWithoutExtension = pathinfo($originalFilename, PATHINFO_FILENAME); // Lấy tên file không có phần mở rộng
$extension = $file->getClientOriginalExtension(); // Lấy phần mở rộng
$directory = 'uploads'; // Thư mục lưu file trên disk
$disk = 's3'; // Disk s3 sẽ sử dụng (được định nghĩa trong config/filesystems.php)
// Xác định tên file duy nhất
$finalFilename = $originalFilename; // Bắt đầu với tên gốc
$counter = 1;
// Kiểm tra xem file đã tồn tại chưa
while (Storage::disk($disk)->exists($directory . '/' . $finalFilename)) {
// Nếu tồn tại, tạo tên mới với hậu tố 1,2,3,...
$finalFilename = $filenameWithoutExtension . '-' . $counter . '.' . $extension;
$counter++;
}
// Lưu file bằng storeAs với tên file mới
$storedFilePath = $file->storeAs($directory, $finalFilename, $disk); // Trả về đường dẫn tương đối: 'uploads/ten_file_cuoi_cung.jpg'
$storedFilePaths[] = Storage::disk($disk)->url($storedFilePath); // Lưu URL của từng file vào array $storedFilePath; // Thêm đường dẫn file đã lưu vào array $storedFilePaths
// Tạo bản ghi mới trong table uploads của database
Upload::create([
'filename' => $storedFilePath,
'original_filename' => $originalFilename,
]);
}
// Chuyển hướng về trang trước đó
return back()->with('success', 'You have successfully uploaded ' . $numberOfFiles . ' files')
// Gửi kèm array các đường dẫn file đã lưu vào session flash data với key 'stored_paths'
->with('stored_paths', $storedFilePaths)
// Gửi kèm array các tên file gốc vào session flash data với key 'original_filenames'
->with('original_filenames', $originalFilenames);
}
Code language: PHP (php)
Hệ thống giờ đã upload file lên S3 thành công.

File đã được đẩy lên lưu trong thư mục uploads của laravel-file-upload-series
bucket

Tuy nhiên, hiện tại đang có 2 lỗi cần phải khắc phục:
- Database đang lưu đường dẫn tương đối của file
uploads/Coolify 4.png
vào cột filename. Cần phải sửa lại thành URL public của file

- Không thể truy cập vào Public URL của file ảnh, khiến upload form không thể hiển thị ảnh của file vừa mới upload. Khi truy cập trực tiếp vào URL sẽ bị báo lỗi: “This XML file does not appear to have any style information associated with it. The document tree is shown below.“
Mình sẽ khắc phục từng lỗi ở các bước kế tiếp.
IV. Lưu Public URL vào database
Sửa lại phần code của hàm store
trong UploadController.
// Lưu file bằng storeAs với tên file mới
$storedFilePath = $file->storeAs($directory, $finalFilename, $disk); // Trả về đường dẫn tương đối: 'uploads/ten_file_cuoi_cung.jpg'
$urlFilePath = Storage::disk($disk)->url($storedFilePath); // Trả về URL public của file
$storedFilePaths[] = $urlFilePath; // Lưu URL của từng file vào array $storedFilePath; // Thêm đường dẫn file đã lưu vào array $storedFilePaths
// Tạo bản ghi mới trong table uploads của database
Upload::create([
'filename' => $urlFilePath,
'original_filename' => $originalFilename,
]);
Code language: PHP (php)
Upload lại file để kiểm tra. Thông tin Public URL của file vừa upload đã được lưu vào database.

V. Cấu hình S3 Bucket
Lỗi “This XML file does not appear to have any style information associated with it” khi truy cập vào Public URL của file lưu trên S3 là lỗi từ chối truy cập, do file không có được cấp quyền public read.
Mặc định, tất cả các file khi upload lên S3 sẽ được tự động chỉnh về chế độ private (riêng tư). Có 2 cách để xử lý vấn đề này:
1. Upload file ở chế độ public
Trong hàm store
của UploadController thêm thông số 'visibility' => 'public'
$storedFilePath = $file->storeAs($directory, $finalFilename, [
'disk' => $disk,
'visibility' => 'public'
]);
Code language: PHP (php)
Truy cập vào trang quản lý S3 Bucket trên AWS, bấm vào mục Permission -> Object Ownership -> Edit

Bấm chọn mục ACLs enabled và chọn ô xác nhận I acknowledge… Bấm Save changes để lưu lại.

Nếu thiếu bước thiết lập Object Ownership này, Laravel sẽ không thể upload file được lên S3 ở chế độ public.
2. Cập nhật Bucket Policy
Thay vì phải cập nhật code để upload file ở chế độ public và chỉnh Object Ownership, sử dụng cách cập nhật Bucket Policy này theo mình là tiện hơn.
Nếu đã cập nhật code trong UploadController theo cách 1, bạn cần chỉnh về lại như cũ. Không cần khai báo 'visibility' => 'public'
nữa.
$storedFilePath = $file->storeAs($directory, $finalFilename, $disk); // Trả về đường dẫn tương đối: 'uploads/ten_file_cuoi_cung.jpg'
Code language: PHP (php)
Ngoài ra, nếu đã chỉnh sửa Object Policy thì cần chỉnh về lại chế độ ACLs disabled.
Truy cập vào phần Permission của bucket, tìm đến ô Bucket policy, bấm Edit

Nhập đoạn code này vào ô Policy và bấm Save changes để lưu lại
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObjectInUploads",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::laravel-file-upload-series/uploads/*"
}
]
}
Code language: JSON / JSON with Comments (json)
Bạn cần lưu ý cập nhật lại dòng "Resource": "arn:........
cho tương ứng với tên bucket và thư mục chứa file. Với thiết lập này, tất cả các file trong thư mục uploads của bucket laravel-file-upload-series
sẽ có quyền truy cập public.
Thử nghiệm lại, app upload của mình giờ đã hiển thị được ảnh lưu trên S3.

Tuy nhiên, giờ lại lòi ra thêm 1 lỗi khác. Nếu bấm nút Delete, bản ghi của file sẽ được xóa khỏi database, nhưng file S3 thì vẫn y nguyên, không bị xóa.
VI. Xóa file đã upload lên S3
Hàm destroy
trong UploadController hiện tại như sau
public function destroy(Upload $upload)
{
// Xoá file vật lý khỏi disk 'public' dựa vào đường dẫn lưu trong $upload->filename
Storage::disk('public')->delete($upload->filename);
// Xoá bản ghi tương ứng trong database
$upload->delete();
// Chuyển hướng người dùng về trang trước đó với thông báo thành công
return back()->with('success', 'You have successfully deleted ' . $upload->original_filename);
}
Code language: PHP (php)
Mình cần phải thay đổi disk('public')
thành disk('s3')
:
public function destroy(Upload $upload)
{
// Xoá file vật lý khỏi disk 's3' dựa vào đường dẫn lưu trong $upload->filename
$disk = 's3';
if (Storage::disk($disk)->exists($upload->filename)) {
Storage::disk($disk)->delete($upload->filename);
}
// Xoá bản ghi tương ứng trong database
$upload->delete();
// Chuyển hướng người dùng về trang trước đó với thông báo thành công
return back()->with('success', 'You have successfully deleted ' . $upload->original_filename);
}
Code language: PHP (php)
Tuy nhiên, khi thử lại thì file trên S3 vẫn chưa thể bị xóa. Lý do vì $upload->filename
đang lưu đường dẫn Public URL (https://...../file.jpg
) của file trên S3, trong đó khi đó 2 hàm exists
và delete
chỉ hoạt động với đường dẫn tương đối (uploads/file.jpg
)
Do đó, mình cần phải đảo ngược lại bước IV đã thực hiện ở trên, quay trở lại lưu đường dẫn tương đối của file vào database.
// Lưu file bằng storeAs với tên file mới
$storedFilePath = $file->storeAs($directory, $finalFilename, $disk); // Trả về đường dẫn tương đối: 'uploads/ten_file_cuoi_cung.jpg'
//$urlFilePath = Storage::disk($disk)->url($storedFilePath); // Trả về URL public của file
$storedFilePaths[] = $storedFilePath; // Lưu URL của từng file vào array $storedFilePath; // Thêm đường dẫn file đã lưu vào array $storedFilePaths
// Tạo bản ghi mới trong table uploads của database
Upload::create([
'filename' => $storedFilePath,
'original_filename' => $originalFilename,
]);
Code language: PHP (php)
Giờ thử nghiệm lại, khi bấm nút Xóa, bản ghi sẽ bị xóa khỏi database, đồng thời file cũng sẽ được xóa thành công trên S3.

VII. Hiển thị file đã upload
Tuy nhiên, giờ lại gặp lỗi ảnh đã lưu trên S3 không hiển thị được trên app. Mình cần phải cập nhập lại upload.blade.php
Tạo thêm hàm getUrlAttribute
trong Model Upload.php
public function getUrlAttribute(): string
{
// Đảm bảo 's3' là tên disk chính xác được sử dụng để lưu trữ các file này.
$disk = 's3';
if ($this->filename) {
return Storage::disk($disk)->url($this->filename);
}
return ''; // Hoặc xử lý một cách thích hợp nếu tên tệp là null
}
Code language: PHP (php)
Với hàm này, mình có thể truy xuất Public URL của các file đã upload bằng cách sử dụng $upload->url
Cập nhật lại phần hiển thị các file đã upload trong upload.blade.php
- Thay đổi
href="{{ $upload->filename }}"
thànhhref="{{ $upload->url }}"
- Thay đổi
src="{{ $upload->filename }}
thànhsrc="{{ $upload->url }}
@if (count($uploads) > 0)
<div class="container mx-auto mt-10 p-10 bg-white rounded-lg shadow-md max-w-md">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Previously Uploaded Files:</h2>
@foreach ($uploads as $upload)
<ul>
<li class="flex items-center justify-between mb-4">
<a class="flex items-center gap-4 py-2" href="{{ $upload->url }}" target="_blank">
<img src="{{ $upload->url }}" alt="{{ $upload->original_filename }}" width="50" height="50">
<span class="text-sm text-gray-700 hover:text-blue-600">{{ $upload->original_filename }}</span>
</a>
<form action="{{ route("upload.destroy", $upload->id) }}" method="POST"
style="display:inline;"
onsubmit="return confirm('Are you sure you want to delete this file?');">
@csrf
@method("DELETE")
<button type="submit"
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Delete</button>
</form>
</li>
</ul>
@endforeach
</div>
@endif
Code language: HTML, XML (xml)
Giờ mọi thứ đã hoạt động đúng như ý muốn:
- File được upload lên Amazon S3
- Thông tin file được lưu vào database
- Khi bấm vào nút Delete, bản ghi của file sẽ bị xóa khỏi database, file cũng sẽ bị xóa khỏi S3.

VIII. Lời kết
Trong [Phần 5] này, mình đã chia sẻ cách upload file lưu trữ trên Amazon S3:
- Tạo bucket và kết nối Laravel với Amazon S3 thông qua file
.env
- Upload file từ form lên cloud
- Cấu hình S3 Bucket để có thể truy cập file được lưu trên S3
- Lấy và hiển thị URL của file trên S3
Việc chuyển từ lưu trữ nội bộ (local) sang lưu trữ cloud như Amazon S3 mang lại nhiều lợi ích thiết thực:
- Dễ dàng mở rộng và tích hợp với các dịch vụ khác (CloudFront, Lambda, v.v.)
- Giảm tải cho server chính, đặc biệt khi ứng dụng có nhiều file media lớn
- Tăng tốc độ tải file, nhờ tận dụng hạ tầng CDN toàn cầu của Amazon
- Tính ổn định và an toàn cao hơn, khi file được lưu trên hệ thống chuyên biệt của AWS
🔗 Mã nguồn
Tham khảo mã nguồn sử dụng trong [Phần 5] ở đây: https://github.com/10h30/laravel-file-upload-series/tree/part-5-upload-to-s3
🔜 Phần 6: Quản lý & chia sẻ file S3 an toàn
Sau khi upload thành công lên S3, bước tiếp theo là quản lý và chia sẻ file một cách bảo mật. Trong Phần 6, mình sẽ chia sẻ thêm:
- Tạo Temporary URL để chia sẻ file có thời hạn
- Kiểm tra và xoá file trên S3
- Phân biệt giữa bucket public và private, và khi nào nên dùng temporary URL
Hẹn gặp lại ở [Phần 6] sẽ được ra lò vào tối Thứ Năm – 15/05/2025!
Bạn đang xem loạt bài viết: Laravel File Upload
- Phần 1: File Upload trong Laravel – [Phần 1] Tạo form, xử lý file, lưu trữ file
- Phần 2: File Upload trong Laravel – [Phần 2] Kiểm tra và bảo vệ file upload form
- Phần 3: File Upload trong Laravel – [Phần 3] Upload cùng lúc nhiều file
- Phần 4: File Upload trong Laravel – [Phần 4] Hiển thị và xoá các file đã upload
- Phần 5: File Upload trong Laravel – [Phần 5] Upload file lên Amazon S3
Nếu bạn cần hỗ trợ kỹ thuật miễn phí, vui lòng gửi câu hỏi trực tiếp ở phần Thảo luận bên dưới, mình sẽ trả lời trong thời gian sớm nhất.
Bạn cần hỗ trợ kỹ thuật chuyên sâu?
Khám phá các gói dịch vụ giúp bạn tối ưu công việc và vận hành hệ thống hiệu quả hơn. Từ chăm sóc website đến hỗ trợ kỹ thuật, mọi thứ đều linh hoạt và phù hợp với nhu cầu của bạn.