Đâ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):

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

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-dependenciesCode 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:

  1. 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
    1. 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 existsdelete 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ành href="{{ $upload->url }}"
    • Thay đổi src="{{ $upload->filename }} thành src="{{ $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 publicprivate, 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!

    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.

    Để lại một bình luận

    Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *


    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.