สำรองฐานข้อมูลแล้ว แต่ไฟล์ที่ผู้ใช้อัปโหลดยังอยู่ที่เดียว
แอปส่วนใหญ่มีของสองอย่างที่หายแล้วสร้างใหม่ไม่ได้ คือฐานข้อมูล กับไฟล์ที่ผู้ใช้อัปโหลด เช่น รูปสินค้า สลิปโอนเงิน หรือเอกสาร PDF ฐานข้อมูลเรามักมี backup อยู่แล้ว แต่ไฟล์พวกนี้หลายครั้งยังอยู่บนดิสก์ของเซิร์ฟเวอร์เครื่องเดียว
ที่ผ่านมาเราก็มักใช้ระบบ backup ของ cPanel หรือ DirectAdmin ที่ zip ทั้งโฮมไดเรกทอรี หรือใช้ spatie/laravel-backup ที่ zip ไฟล์รวมกับฐานข้อมูลแล้วส่งขึ้นที่เก็บนอกเครื่อง วิธีนี้เหมาะกับฐานข้อมูลที่ dump ใหม่ทุกคืนอยู่แล้ว แต่กับโฟลเดอร์รูปที่โตขึ้นทุกวัน ทุกคืนต้อง zip และอัปโหลดทั้งก้อนใหม่ ทั้งที่ไฟล์ส่วนใหญ่ไม่ได้เปลี่ยน แถมถ้ามีใครลบไฟล์พลาด zip ของคืนถัดไปก็ไม่มีไฟล์นั้นแล้ว
files-backup-laravel ส่งเฉพาะไฟล์ที่เปลี่ยน
แพ็กเกจนี้ใช้ rclone เป็นตัวส่งไฟล์ (หรือ disk ของ Flysystem ถ้าไม่อยากติดตั้ง rclone) แต่ละคืน files:backup เทียบไฟล์ในเครื่องกับที่เก็บปลายทาง แล้วส่งขึ้นเฉพาะไฟล์ใหม่หรือไฟล์ที่เปลี่ยน คืนไหนไม่มีอะไรเปลี่ยนก็แทบไม่มีข้อมูลวิ่งออกไปเลย
files:backup
→ ไล่ไฟล์ในแต่ละ set (ข้ามไฟล์ที่ exclude ไว้)
→ rclone sync --backup-dir …/versions/<เวลาที่รัน>
→ rclone check ว่าปลายทางครบ
→ ลบ versions/ ที่เก่ากว่า versions_days
→ เขียน status.json ให้ files:backup-check อ่าน
ที่ปลายทาง แต่ละ set มีโฟลเดอร์ของตัวเอง current/ คือสำเนาล่าสุดแบบโฟลเดอร์ธรรมดา เปิดดูหรือก๊อปกลับได้เลย ส่วนไฟล์ที่คืนนั้นเขียนทับหรือลบไป rclone ย้ายไปไว้ใน versions/ แยกตามเวลาที่รัน
myapp/media/current/storage/app/public/… สำเนาล่าสุด
myapp/media/versions/2026-10-10_035000/storage/app/public/… ไฟล์ที่คืนนั้นเขียนทับหรือลบไป
myapp/media/status.json รอบล่าสุดที่สำเร็จ
แพ็กเกจนี้เป็นคู่กับ db-snapshot-sync-laravel ที่สำรองฐานข้อมูล ใช้ชื่อคำสั่งและรูปแบบ config ชุดเดียวกัน และเขียนไว้ข้างกันบนที่เก็บเดียวกันได้ เช่น myapp/db กับ myapp/media
ติดตั้ง
แพ็กเกจรองรับ PHP 8.4 ขึ้นไป กับ Laravel 12 และ 13 ตอนนี้เป็นเวอร์ชัน 0.1
composer require phattarachai/files-backup-laravel
php artisan vendor:publish --tag=files-backup-config
driver ค่าเริ่มต้นต้องมี rclone บนเซิร์ฟเวอร์ ติดตั้งด้วย apt install rclone หรือใช้ไฟล์ binary จาก หน้าดาวน์โหลด ก็ได้ ไม่ต้องสร้าง rclone.conf เพราะแพ็กเกจสร้าง remote จาก config ของแอปแล้วส่งให้ rclone เป็น environment variable รหัสผ่านจึงไม่ไปโผล่ใน command line
เลือกโฟลเดอร์ที่จะสำรองเป็น set
แต่ละโปรเจกต์เก็บไฟล์ไว้คนละที่ บางที่อยู่ใน storage/app/public บางที่อยู่ใน public/uploads เราจึงจับกลุ่มโฟลเดอร์เป็น set แล้วบอกว่าไฟล์แบบไหนไม่ต้องสำรอง
'sets' => [
'media' => [
'paths' => [
storage_path('app/public'), // ที่ปลายทางจะเป็น storage/app/public
// 'uploads' => public_path('uploads'), // ใส่ key เพื่อตั้งชื่อโฟลเดอร์เอง
],
'exclude' => ['.gitignore', '.DS_Store', 'livewire-tmp/**', '*.tmp'],
],
],
'target' => [
'driver' => env('FILES_BACKUP_DRIVER', 'rclone'), // rclone หรือ flysystem
'path' => env('FILES_BACKUP_PATH', ''), // เช่น myapp → myapp/media/…
'mode' => 'sync', // sync หรือ copy
],
'versions_days' => 30,
'stale_after_hours' => 26,
exclude ใช้รูปแบบ glob ของ rclone * จับในโฟลเดอร์เดียว ** จับข้ามโฟลเดอร์ ส่วน mode ถ้าเป็น sync ไฟล์ที่ลบในเครื่องจะหายจาก current/ ด้วย แต่ไปอยู่ใน versions/ แทน ถ้าอยากให้ current/ ไม่ลบอะไรเลย ใช้ copy
ถ้าโปรเจกต์ไหนยังไม่ต้องสำรองไฟล์ ตั้ง FILES_BACKUP_ENABLED=false ทุกคำสั่งจะจบเฉย ๆ โดยไม่ error เขียน schedule ไว้เหมือนกันทุกโปรเจกต์ได้
ส่งไป Google Drive, S3 หรือ NAS
ตั้งค่าใน target.rclone.remote ด้วยชื่อ option ของ rclone เอง แต่ละค่ากลายเป็น RCLONE_CONFIG_<NAME>_<OPTION> ตอนรัน
'remote' => [
'type' => 'drive',
'scope' => 'drive',
'service_account_file' => env('FILES_BACKUP_RCLONE_SERVICE_ACCOUNT_FILE'), // /etc/myapp/gdrive-sa.json
'team_drive' => env('FILES_BACKUP_RCLONE_TEAM_DRIVE'), // id ของ Shared Drive
],
'remote' => [
'type' => 's3',
'provider' => 'DigitalOcean', // หรือ AWS, Cloudflare, Minio
'access_key_id' => env('DO_SPACES_KEY'),
'secret_access_key' => env('DO_SPACES_SECRET'),
'endpoint' => 'sgp1.digitaloceanspaces.com',
'acl' => 'private',
],
// 'path' => 'my-bucket/myapp' // ส่วนแรกของ path คือชื่อ bucket
'remote' => [
'type' => 'sftp',
'host' => env('NAS_SFTP_HOST'),
'user' => env('NAS_SFTP_USERNAME'),
'key_file' => env('NAS_SFTP_PRIVATE_KEY_PATH'),
'known_hosts_file' => '/etc/myapp/nas_known_hosts',
],
// 'path' => '/volume1/backups/myapp'
option ที่ชื่อมีคำว่า pass, secret, token, key, credentials, bearer หรือ session แพ็กเกจถือว่าเป็นความลับ และแสดงเป็น ******** ในทุกข้อความ error และ log รวมถึง error ที่ rclone พิมพ์ออกมาเองด้วย
เข้ารหัสก่อนส่งออกด้วย rclone crypt
ถ้าที่เก็บปลายทางไม่ใช่ของเราเอง เปิด crypt ของ rclone ได้ ชื่อไฟล์และเนื้อไฟล์จะเข้ารหัสตั้งแต่ก่อนออกจากเซิร์ฟเวอร์ ตอนตรวจความครบถ้วน แพ็กเกจเปลี่ยนไปใช้ rclone cryptcheck ให้เอง
'crypt' => [
'enabled' => true,
'password' => env('FILES_BACKUP_RCLONE_CRYPT_PASSWORD'), // ค่าที่ได้จาก rclone obscure
'password2' => env('FILES_BACKUP_RCLONE_CRYPT_PASSWORD2'),
'filename_encryption' => 'standard',
'directory_name_encryption' => true,
],
rclone obscure 'the-real-passphrase'
ไม่มี rclone ก็ใช้ disk ของ Flysystem ได้
ถ้าเซิร์ฟเวอร์ติดตั้งโปรแกรมเพิ่มไม่ได้ เช่น shared hosting ให้ใช้ disk ใน config/filesystems.php แทน จะเป็น disk เดียวกับที่ db-snapshot-sync ใช้ก็ได้
'target' => [
'driver' => 'flysystem',
'flysystem' => ['disk' => 'gdrive'],
],
driver นี้เก็บ manifest.json ไว้ที่ปลายทาง บอกขนาด เวลาแก้ไข และ sha256 ของทุกไฟล์ แต่ละคืนเทียบไฟล์ในเครื่องกับ manifest แทนการไล่ list ปลายทาง ก่อนส่งไฟล์ที่เปลี่ยนขึ้นไป แพ็กเกจย้ายไฟล์เดิมไปไว้ใน versions/ บนปลายทางก่อน ไฟล์ที่อัปโหลดตั้งเป็น private เสมอไม่ว่า disk จะตั้งค่าเริ่มต้นไว้แบบไหน และบันทึก manifest ทุก 100 ไฟล์ ถ้ารอบแรกสะดุดกลางทาง รอบถัดไปทำต่อจากจุดที่ค้างไว้ได้
โฟลเดอร์ว่างกะทันหัน แพ็กเกจไม่ลบตามที่ปลายทาง
ถ้า set ที่เคยมีไฟล์ จู่ ๆ ว่างเปล่าทั้งหมด แพ็กเกจไม่ยอม sync ต่อ เพราะเหตุที่เจอบ่อยกว่าการลบไฟล์ทั้งหมดจริง คือ volume ยังไม่ได้ mount หรือ path ผิด ถ้าปล่อยให้ sync ไป สำเนาที่ปลายทางจะว่างตามไปด้วย ถ้าไฟล์หายไปจริง ๆ ให้ลบ status.json ของ set นั้นที่ปลายทาง แล้วรอบถัดไปจะทำงานตามปกติ
ตั้ง schedule และแจ้งเตือนเมื่อ backup ขาดช่วง
use Illuminate\Support\Facades\Schedule;
Schedule::command('files:backup')->dailyAt('03:50')->withoutOverlapping();
Schedule::command('files:backup-check')->hourly();
Schedule::command('files:drill')->monthlyOn(1, '05:30');
files:backup-check อ่าน status.json ของแต่ละ set แล้วส่ง event FilesBackupHealthy หรือ FilesBackupStale แพ็กเกจดูจากเวลาที่ backup รอบล่าสุดทำเสร็จ ไม่ได้ดูจากไฟล์ล่าสุด โฟลเดอร์ที่ทั้งสัปดาห์ไม่มีไฟล์ใหม่ ก็ต้องมี backup เมื่อคืนเหมือนกัน
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Phattarachai\FilesBackupLaravel\Events\FilesBackupStale;
Event::listen(FilesBackupStale::class, function (FilesBackupStale $event): void {
Log::critical('Off-site files backup is stale: '.$event->describe());
// หรือส่งเข้า LINE, Telegram, Slack
});
ทุกคำสั่งจบด้วย exit code ที่ไม่ใช่ศูนย์เมื่อล้มเหลว และ files:backup กับ files:drill โยน exception หลังทำครบทุก set แล้ว error จึงไปถึงตัวติดตาม error อย่าง Watchtower หรือ Sentry ด้วย ถ้าอยากให้งานไปรันบน queue แทน ใช้ Schedule::job(new BackupFiles) ได้ job นี้ไม่ retry (คืนถัดไปจะตามเก็บเอง) และรันได้นานถึงชั่วโมงหนึ่ง จึงควรวางไว้บน queue ที่ worker ตั้ง --timeout ไว้ยาวพอ
files:drill ลองกู้จริงทุกเดือน
backup ที่ไม่เคยลองกู้ เราก็ไม่รู้ว่าใช้ได้จริงไหม files:drill เช็กสำเนาที่ปลายทางให้ทีละ set
- อ่าน
status.jsonถ้าไม่มีรอบที่สำเร็จเลยถือว่าไม่ผ่าน - list
current/ที่ปลายทาง เทียบกับไฟล์จริง ไฟล์ที่เก่ากว่ารอบล่าสุดต้องมีครบและขนาดตรงกัน - สุ่มดาวน์โหลดมาจำนวนหนึ่ง (ค่าเริ่มต้น 20 ไฟล์ หรือ
--allทั้งหมด) แล้วเทียบ sha256 กับไฟล์จริง - ลบไฟล์ที่ดาวน์โหลดมาทิ้ง แล้วส่ง event
FilesDrillCompleted
ไฟล์ที่แก้หลังรอบล่าสุดเริ่ม แพ็กเกจนับไว้เฉย ๆ ไม่ถือว่าไม่ผ่าน และ drill ไม่เขียนอะไรลงปลายทางหรือโฟลเดอร์จริงเลย
กู้คืนด้วย rclone copy
ปลายทางเป็นโฟลเดอร์ธรรมดา กู้คืนจึงเป็นการก๊อปกลับครั้งเดียว ก่อนกู้ให้เปิด maintenance mode หยุดการเขียนไฟล์ก่อน
# กู้ทั้งโฟลเดอร์
rclone copy filesbackup:myapp/media/current/storage/app/public /var/www/myapp/storage/app/public --progress
# กู้ไฟล์เดียวจากก่อนที่คืนนั้นจะเขียนทับหรือลบไป
rclone lsf filesbackup:myapp/media/versions/
rclone copyto filesbackup:myapp/media/versions/2026-10-10_035000/storage/app/public/12/photo.jpg ./photo.jpg
ก่อนรัน rclone ต้องบอก remote ให้ shell รู้จักก่อน README มีตัวอย่าง environment variable ครบทั้งแบบปกติและแบบ crypt ส่วนฐานข้อมูล ควรกู้จากคืนเดียวกับไฟล์ แถวใน media library กับไฟล์จะได้ตรงกัน ถ้ายังไม่มี backup ฐานข้อมูล ลองดู db-snapshot-sync-laravel ได้ครับ
ไฟล์ที่ลบไปนานกว่า versions_days กู้ไม่ได้แล้ว
versions/ เก็บไฟล์ที่ลบหรือเขียนทับไว้ตาม versions_days (ค่าเริ่มต้น 30 วัน) แล้วลบทิ้ง ถ้ามีคนลบรูปไปแล้วเพิ่งมีคนสังเกตเห็นตอนผ่านไปสองเดือน ไฟล์นั้นไม่อยู่ที่ปลายทางแล้ว ถ้าข้อมูลประเภทเอกสารหรือสลิปต้องเก็บนานกว่านั้น ให้เพิ่ม versions_days หรือใช้ mode เป็น copy ให้ current/ ไม่ลบอะไรเลย แล้วยอมจ่ายค่าพื้นที่เพิ่ม
อีกเรื่องคือแพ็กเกจยังเป็นเวอร์ชัน 0.x ตาม semver การขึ้นจาก 0.1 เป็น 0.2 อาจมีการเปลี่ยนที่ต้องแก้ตาม ตั้ง composer ไว้ที่ ^0.1 แล้วอ่าน release notes ก่อนขยับเวอร์ชัน






