~/blog/laravel-s3-file-upload-architecture-guide-2026-2.md
Laravel 與後端開發 · 2026 / 02 / 25

伺服器硬碟又爆了?Laravel + S3 檔案上傳實戰:從 Pre-signed URL 到串流下載的 2026 架構指南

Eric — 浪花科技創辦人 / AI 架構師
Eric
浪花科技創辦人 · AI 架構師
伺服器硬碟又爆了?Laravel + S3 檔案上傳實戰:從 Pre-signed URL 到串流下載的 2026 架構指南
目錄 table-of-contents.md

凌晨三點手機震動,監控系統跳出「Disk Usage: 98%」——storage 目錄又被使用者上傳的檔案塞爆了。只要檔案還放在應用伺服器的本機硬碟,這齣救火戲碼就會一再重演。這篇把上傳架構整個搬上 S3:從 Pre-signed URL 直傳到串流下載,讓硬碟爆滿從此與 Laravel 主機無關。

相信每一位 Laravel 開發者都有過這樣的午夜夢迴:手機震動,DevOps 監控系統發出警報,訊息顯示 「Disk Usage: 98%」。你睡眼惺忪地 SSH 進伺服器,發現 storage/app/public 資料夾被使用者的上傳圖片塞爆了,或者是某個 php-fpm process 因為記憶體溢位(OOM)而掛掉,因為有人試圖上傳一個 500MB 的影片檔。

這在 2018 年或許是日常,但在 2026 年,如果你的 Laravel 專案還在把檔案直接存在 Web Server 的硬碟裡,或者還在使用傳統的 $request->file('avatar')->store('public') 這種「路過式」上傳,那你真的該檢討一下架構了。

今天這篇文章,我們要來聊聊 2026 年企業級 Laravel 專案該如何優雅地處理檔案上傳。我們不談那些「能動就好」的 Code,我們要談的是 高併發、低成本、且絕對不會讓硬碟爆炸 的 S3 整合架構。我們會涵蓋從基本的串流上傳(Streaming)到進階的預簽章 URL(Pre-signed URL)實戰。

為什麼 2026 年「本地儲存」已經是死路一條?

在進入程式碼之前,Eric 必須先囉嗦一下觀念。很多新手工程師會問:「S3 要錢耶,硬碟不是比較便宜嗎?」

這是一個典型的「窮人思維」陷阱。在現代化的容器化部署(如 Docker、K8s)或是無伺服器架構(Serverless)中,應用程式必須是無狀態(Stateless)的。這意味著:

  • 擴展性(Scalability): 當你的流量爆衝,開啟了 10 台 EC2 或 Pod,如果檔案存在「第 1 台」的硬碟裡,使用者連到「第 2 台」時就會看到圖片破圖(404)。
  • 安全性與備份: S3 提供了 99.999999999% 的耐久性(Durability)。你覺得你自己維護的 RAID 硬碟陣列能比 AWS 更安全嗎?
  • 頻寬成本: 讓 Web Server 去處理靜態檔案的讀取,是在浪費昂貴的運算資源。這種粗活,應該交給 CDN(如 CloudFront)去處理。

第一道防線:串流上傳 (Streaming Uploads) —— 拒絕記憶體溢位

這是最基礎的優化。傳統的 Laravel 上傳方式,會先把檔案完整讀入 PHP 的記憶體,然後再轉傳給 S3。如果 PHP 的 memory_limit 設為 128MB,而使用者上傳了一個 200MB 的影片,你的 Process 直接原地爆炸。

在 2026 年的 Laravel 12/13 中,我們可以利用 Flysystem 的串流功能,像接水管一樣,把檔案從 Client 端「流」向 S3,而不需要把整個水桶(檔案)扛在肩上。

錯誤的寫法(記憶體殺手)

// 這是新手最愛寫的,檔案小的時候沒事,檔案一大就 OOM
public function upload(Request $request)
{
    $file = $request->file('video');
    // 這裡會把整個檔案內容讀進記憶體!
    Storage::disk('s3')->put('videos/' . $file->hashName(), file_get_contents($file));
    
    return response()->json(['status' => 'success']);
}

正確的寫法(串流機制)

利用 putFile 或者 put 搭配 fopen 資源,Laravel 會自動處理串流。

public function upload(Request $request)
{
    // Laravel 內建的 putFile 已經實作了串流處理
    // 它會自動使用 fopen('rb') 來讀取暫存檔
    $path = Storage::disk('s3')->putFile(
        'videos',
        $request->file('video'),
        'public' // 可見性設定
    );

    return response()->json(['url' => Storage::disk('s3')->url($path)]);
}

這樣寫的好處是,無論檔案是 10MB 還是 1GB,PHP 佔用的記憶體都非常低,因為它只佔用一個 File Pointer 的資源。

終極架構:Pre-signed URL —— 讓 Web Server 「完全」脫身

雖然串流上傳解決了記憶體問題,但它還有一個致命傷:佔用連線數(Concurrency)

想像一下,你是一個影音平台,使用者上傳影片平均需要 5 分鐘。如果你的 Web Server 使用 PHP-FPM,這 5 分鐘內,這條 Process 就被卡住了(Blocked)。如果你的伺服器能處理 100 個併發,只要有 100 個人同時在上傳,第 101 個人就進不來了。

這時候,我們需要引入 Pre-signed URL(預簽章網址)。這就像是發給使用者一張「VIP 通行證」,讓前端(Vue/React/App)拿著這張通行證,直接把檔案上傳到 AWS S3,完全繞過 Laravel 後端。

運作流程:

  1. 前端告訴 Laravel:「我要上傳一個 avatar.jpg」。
  2. Laravel 檢查權限,確認你是會員,然後向 AWS S3 申請一張「只能上傳到特定路徑、且只有 5 分鐘時效」的簽名 URL。
  3. Laravel 把 URL 回傳給前端。
  4. 前端使用 PUT 方法,直接將檔案丟給 S3。
  5. 上傳完成後,S3 可以透過 Webhook (Lambda) 通知 Laravel,或者前端再次呼叫 API 更新資料庫。

Laravel 後端實作程式碼

在 Laravel 中生成 Pre-signed URL 非常簡單,但要注意 2026 年的 AWS SDK 版本差異,以下是標準實作:

use Illuminate\Support\Facades\Storage;

public function getUploadUrl(Request $request)
{
    // 1. 驗證使用者權限 (一定要做!)
    $this->authorize('upload', User::class);

    // 2. 定義檔案路徑,建議使用 UUID 防止檔名衝突
    $filename = Str::uuid() . '.jpg';
    $path = 'uploads/avatars/' . $filename;

    // 3. 建立 AWS S3 Client (假設你已經設定好 .env 的 AWS_ACCESS_KEY_ID 等)
    $client = Storage::disk('s3')->getClient();
    
    // 4. 建立 Command
    $command = $client->getCommand('PutObject', [
        'Bucket' => config('filesystems.disks.s3.bucket'),
        'Key'    => $path,
        'ACL'    => 'public-read', // 或是 private,看需求
        'ContentType' => 'image/jpeg', // 強制限制類型,資安關鍵!
    ]);

    // 5. 產生簽名 URL,設定 10 分鐘過期
    $request = $client->createPresignedRequest($command, '+10 minutes');
    $presignedUrl = (string) $request->getUri();

    return response()->json([
        'upload_url' => $presignedUrl,
        'file_path' => $path // 存入資料庫用
    ]);
}

前端如何使用? (JavaScript 範例)

// 拿到 upload_url 後...
async function uploadFileToS3(uploadUrl, file) {
    await fetch(uploadUrl, {
        method: 'PUT',
        body: file,
        headers: {
            'Content-Type': file.type // 必須與後端簽名時一致
        }
    });
    console.log('上傳成功,S3 沒經過後端伺服器!');
}

2026 架構思維:資安與效能的權衡

身為資深工程師,Eric 必須提醒大家,使用了 Pre-signed URL 雖然效能飛天,但「資安責任」並沒有消失,反而更需要細心設計。

1. 限制 Content-Type 與大小

在產生簽名時,務必限制 ContentType。如果你允許使用者上傳 text/html,駭客可能會上傳一個包含惡意 JavaScript 的 HTML 檔,當管理者打開時就會觸發 XSS 攻擊。AWS S3 Policy 也可以設定 content-length-range 來限制檔案大小,不要讓使用者的瀏覽器無限上傳。

2. 檔案掃毒 (Virus Scan)

既然檔案不經過你的伺服器,你就沒辦法用伺服器上的防毒軟體即時掃描。2026 年的標準做法是:檔案上傳到 S3 後,觸發 AWS Lambda,在 Lambda 裡面執行 ClamAV 掃毒。如果發現病毒,Lambda 直接刪除 S3 檔案,並呼叫你的 Laravel API 標記該檔案為「危險」。

3. 私有檔案的存取 (Private Content)

如果是付費課程影片或合約 PDF,千萬不要設為 public-read。上傳時設為 private,而當使用者要「下載/觀看」時,同樣使用 Laravel 產生一個 「下載用的 Pre-signed URL」 (Storage::disk('s3')->temporaryUrl()),這樣才能確保只有付費會員能看到內容。

結論

從「硬碟儲存」進化到「S3 串流」,再進化到「Pre-signed URL 直傳」,這不僅僅是程式碼的改變,更是架構思維的升級。在 2026 年,硬體雖然便宜,但「維護成本」才是最貴的。讓 AWS S3 幫你扛流量、扛硬碟空間,讓你的 Laravel 專案保持輕量、專注於商業邏輯,這才是資深工程師該有的佈局。

如果你發現你的 Laravel 專案越來越慢,或者硬碟整天在報警,別再手動清 Log 了,是時候重構你的上傳架構了。

你的企業系統也面臨檔案暴增、效能卡關的瓶頸嗎?浪花科技專精於 Laravel 高併發架構與雲端整合。

立即聯繫我們,為您的系統進行效能健檢

推薦閱讀

// FAQ

常見問題

為什麼 Laravel 專案不該把上傳檔案存在本地伺服器硬碟?
在容器化或無伺服器架構中,應用程式必須是無狀態的。若檔案存在某一台機器的硬碟,使用者連到其他台時就會看到破圖。此外本地儲存還有備份與耐久性不足、Web Server 處理靜態檔案浪費運算資源等問題,這些粗活應交給物件儲存與 CDN。
Laravel 上傳大檔案時如何避免記憶體溢位(OOM)?
不要用 file_get_contents() 把整個檔案讀進記憶體再上傳,這在檔案一大時會讓 PHP process 爆炸。改用 Storage::disk('s3')->putFile() 這類內建串流機制,Laravel 會自動以串流方式處理,無論檔案 10MB 還是 1GB,只佔用一個 File Pointer 的記憶體。
什麼是 Pre-signed URL?它解決了什麼問題?
Pre-signed URL(預簽章網址)是後端向 S3 申請的一張有時效的上傳通行證,讓前端拿著它直接把檔案上傳到 S3,完全繞過後端伺服器。它解決了串流上傳仍會長時間佔用後端連線數的問題,避免大量併發上傳把 PHP-FPM 的 process 卡滿。
使用 Pre-signed URL 上傳時有哪些資安要注意?
效能提升不代表資安責任消失。產生簽名時務必限制 ContentType,否則攻擊者可能上傳含惡意 JavaScript 的 HTML 檔造成 XSS。同時可透過 S3 的 content-length-range 限制檔案大小,避免無限上傳,並建議搭配後續的檔案掃毒機制。
~/roamer-tech/newsletter // FREE
// newsletter

訂閱免費電子報

把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。

$
// final.exec()

準備好讓你的網站開始為你工作了嗎?