Workshop สำหรับพัฒนา REST API อัปโหลด ดาวน์โหลด และลบไฟล์ โดยใช้ MinIO เป็น S3-compatible object storage และ Laravel เป็น Backend

#1. สถาปัตยกรรม

flowchart LR
  U[Client / Postman] --> L[Laravel API]
  L -->|S3 API| M[MinIO Object Storage]
  L --> D[(Metadata Database - optional)]

MinIO เป็นระบบ Object Storage ที่รองรับ S3 API เหมาะกับการพัฒนาระบบเก็บรูปภาพ เอกสาร ไฟล์สำรอง และไฟล์ของแอปพลิเคชัน โดยสามารถติดตั้งภายในองค์กรได้

#2. สิ่งที่ต้องเตรียม

  • Docker และ Docker Compose
  • PHP, Composer และ Laravel (ใช้โปรเจกต์ Laravel ที่ติดตั้งแล้ว)
  • โปรแกรมทดสอบ API เช่น Postman หรือ curl

#3. เปิด MinIO ด้วย Docker Compose

สร้างไฟล์ compose.yaml ในโฟลเดอร์แยกสำหรับ MinIO:

services:
  minio:
    image: minio/minio:RELEASE.2025-09-07T16-13-09Z
    command: server /data --console-address ":9001"
    ports:
      - "127.0.0.1:9000:9000"
      - "127.0.0.1:9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: change-this-password-123
    volumes:
      - minio_data:/data
    restart: unless-stopped
volumes:
  minio_data:

แท็ก image เป็นตัวอย่างที่ระบุเวอร์ชันเพื่อให้ทดลองซ้ำได้ ควรตรวจสอบรุ่นที่ยังได้รับการสนับสนุนและเงื่อนไขการเผยแพร่ของ MinIO ก่อนใช้งานจริง และห้ามใช้รหัสผ่านตัวอย่างใน production

docker compose up -d
docker compose ps

เข้า MinIO Console ที่ http://localhost:9001 ด้วยบัญชีที่ตั้งไว้ จากนั้นสร้าง Bucket ชื่อ laravel-files โดยคงนโยบายเป็น private

#4. สร้างโปรเจกต์ Laravel

composer create-project laravel/laravel laravel-minio
cd laravel-minio
composer require league/flysystem-aws-s3-v3 "^3.0"
php artisan make:controller FileController

#5. ตั้งค่าเชื่อมต่อ MinIO

แก้ไข .env (กรณี Laravel ทำงานบนเครื่อง Host):

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=change-this-password-123
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=laravel-files
AWS_ENDPOINT=http://127.0.0.1:9000
AWS_USE_PATH_STYLE_ENDPOINT=true

ตรวจสอบ config/filesystems.php ว่ามี disk s3 ดังนี้ (โปรเจกต์ Laravel รุ่นใหม่มักมีอยู่แล้ว):

's3' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    'bucket' => env('AWS_BUCKET'),
    'endpoint' => env('AWS_ENDPOINT'),
    'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', true),
    'throw' => true,
],

หาก Laravel อยู่ใน Docker Compose network เดียวกับ MinIO ให้ใช้ AWS_ENDPOINT=http://minio:9000 แทน 127.0.0.1 (และไม่ใช้ localhost ภายใน container)

php artisan config:clear

#6. สร้าง API Controller

ไฟล์ app/Http/Controllers/FileController.php:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class FileController extends Controller
{
    public function upload(Request $request)
    {
        $validated = $request->validate([
            'file' => ['required', 'file', 'max:10240', 'mimes:jpg,jpeg,png,pdf,txt'],
        ]);

        $file = $validated['file'];
        $path = $file->storeAs(
            'uploads',
            Str::uuid() . '.' . $file->extension(),
            's3'
        );

        return response()->json([
            'message' => 'อัปโหลดสำเร็จ',
            'path' => $path,
        ], 201);
    }

    public function download(string $name)
    {
        $path = 'uploads/' . $name;
        abort_unless(Storage::disk('s3')->exists($path), 404);

        return Storage::disk('s3')->download($path);
    }

    public function delete(string $name)
    {
        $path = 'uploads/' . $name;
        abort_unless(Storage::disk('s3')->exists($path), 404);

        Storage::disk('s3')->delete($path);
        return response()->json(['message' => 'ลบไฟล์สำเร็จ']);
    }
}

ตัวอย่างนี้กำหนดชื่อไฟล์ด้วย UUID เพื่อป้องกันชื่อชนกัน และ route รับเฉพาะชื่อไฟล์หนึ่งระดับ ไม่รับ path อิสระจากผู้ใช้

#7. กำหนด API Routes

สำหรับ Laravel 11+ หากยังไม่มี routes/api.php ให้เปิดใช้งาน API routing ก่อน:

php artisan install:api

แก้ไข routes/api.php:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\FileController;

Route::post('/files', [FileController::class, 'upload']);
Route::get('/files/{name}', [FileController::class, 'download']);
Route::delete('/files/{name}', [FileController::class, 'delete']);

Routes นี้เปิดสาธารณะเพื่อสาธิตเฉพาะในเครื่องทดสอบ ก่อนเผยแพร่จริงต้องเพิ่ม authentication, authorization, rate limiting และการตรวจสอบสิทธิ์เจ้าของไฟล์

#8. ทดลองใช้งาน API

เปิด Laravel:

php artisan serve

อัปโหลด:

curl -X POST http://127.0.0.1:8000/api/files \
  -F "file=@sample.pdf"

ดาวน์โหลด (นำชื่อไฟล์จาก path ที่ API ตอบกลับมา):

curl -OJ http://127.0.0.1:8000/api/files/FILE_UUID.pdf

ลบไฟล์:

curl -X DELETE http://127.0.0.1:8000/api/files/FILE_UUID.pdf

ตรวจสอบไฟล์ใน MinIO Console ที่ Bucket laravel-files ภายใต้โฟลเดอร์ uploads/

#9. แบบฝึกหัดต่อยอด

  1. เพิ่ม endpoint แสดงรายการไฟล์ด้วย Storage::disk('s3')->files('uploads')
  2. บันทึก metadata (owner, filename, MIME type, size) ลงฐานข้อมูล
  3. เพิ่ม Sanctum เพื่อยืนยันตัวตนและตรวจสอบสิทธิ์รายไฟล์
  4. เพิ่มการสร้างลิงก์ดาวน์โหลดชั่วคราว (presigned URL) โดยตรวจสอบ endpoint ที่ client เข้าถึงได้
  5. เพิ่ม automated API tests สำหรับไฟล์ที่ถูกต้อง ไฟล์เกินขนาด และการเข้าถึงโดยไม่มีสิทธิ์

#10. ข้อควรระวังเมื่อใช้งานจริง

  • เปลี่ยน root credentials และใช้ access key ที่จำกัดสิทธิ์เฉพาะ bucket
  • เปิด TLS/HTTPS และเก็บ secrets ใน secret manager
  • ไม่เปิด MinIO Console หรือ S3 endpoint สู่สาธารณะโดยไม่ป้องกัน
  • ตรวจสอบ MIME/file signatures และพิจารณาสแกนมัลแวร์สำหรับไฟล์จากผู้ใช้
  • กำหนดนโยบาย backup, versioning, retention และ monitoring
  • ไม่คืนค่า URL ของ bucket แบบ public โดยไม่ตั้งใจ เพราะ workshop นี้ใช้ private bucket
  • การรัน Laravel และ MinIO คนละ network อาจทำให้ presigned URL ที่สร้างจาก internal hostname ใช้จาก browser ไม่ได้

#สรุป

Laravel ใช้ Flysystem S3 driver เชื่อมต่อ MinIO ผ่าน S3-compatible API ได้ ทำให้พัฒนา REST API สำหรับอัปโหลด ดาวน์โหลด และลบไฟล์ได้โดยไม่ผูกกับผู้ให้บริการ Cloud รายใดรายหนึ่ง และสามารถขยายไปสู่ระบบเอกสารหรือ media service ได้

#เอกสารอ้างอิง