Skip to content

Fitur backup & restore file asset storage dari menu Pengaturan Database#1617

Open
habibie11 wants to merge 1 commit into
rilis-devfrom
dev-1616
Open

Fitur backup & restore file asset storage dari menu Pengaturan Database#1617
habibie11 wants to merge 1 commit into
rilis-devfrom
dev-1616

Conversation

@habibie11

Copy link
Copy Markdown
Contributor

issue #1616

🎯 Deskripsi

Pull request ini mengimplementasikan fitur backup dan restore file asset storage pada menu Pengaturan → Pengaturan Database, sesuai permintaan di issue #1616.

Sebelumnya, fitur backup yang menggunakan spatie/laravel-backup sudah menghasilkan file ZIP berisi database dump (.sql) dan seluruh file di storage/app/ (termasuk foto artikel, dokumen, gambar widget, dll). Namun, fitur restore hanya menerima file .sql — tidak ada mekanisme untuk mengembalikan file asset dari ZIP backup. Akibatnya, saat OpenDK perlu dipindahkan atau diinstal ulang, file asset tidak bisa dipulihkan dari backup yang sudah dibuat, sehingga data lama tidak dapat sepenuhnya berpindah ke instalasi baru.

PR ini memperluas fitur restore yang sudah ada agar dapat menerima file .zip (backup spatie) dan mengembalikan database + file asset sekaligus dalam satu operasi. Selain itu, PR ini juga memperbaiki beberapa bug kritis pada alur backup yang menyebabkan notifikasi sukses palsu.


🛠️ Perubahan yang Dilakukan

1. config/backup.php

Perbaikan konfigurasi — Path relatif & exclude direktori backup:

  • Mengubah 'relative_path' dari nullbase_path() agar entri di dalam ZIP menggunakan path relatif (mis. storage/app/public/artikel/foto.jpg) alih-alih absolute path. Ini membuat backup ZIP portable antar server/OS yang berbeda.
  • Menambahkan storage/app/backup-storage dan storage/app/backup-temp ke daftar 'exclude' untuk mencegah backup ZIP menumpuk recursively di dalam backup berikutnya.
  'exclude' => [
      base_path('vendor'),
      base_path('node_modules'),
      base_path('storage/debugbar'),
      base_path('storage/framework'),
      base_path('storage/logs'),
+     base_path('storage/app/backup-storage'),
+     base_path('storage/app/backup-temp'),
  ],

- 'relative_path' => null,
+ 'relative_path' => base_path(),

2. app/Http/Controllers/Setting/PengaturanDatabaseController.php

Fitur — Restore dari file .zip:

restoreBackup() — Memperluas method yang sudah ada untuk menerima kedua format file:

  • Mengubah $allowedExtensions dari ['sql']['sql', 'zip'].
  • Branching berdasarkan ekstensi: .sql → alur lama (DB only), .zip → alur baru (Files + DB).
  • Untuk .zip: menggunakan MIME types dari FileUploadService::getAllowedMimes('archive') dan max size 500MB.
  • Memperbaiki bug path resolution: Storage::path()Storage::disk('public')->path() (file di-upload ke disk public, bukan local).
- $allowedExtensions = ['sql'];
+ $allowedExtensions = ['sql', 'zip'];
  $file = $request->file('backupFile');
- $extension = $file->getClientOriginalExtension();
+ $extension = strtolower($file->getClientOriginalExtension());

+ if ($extension === 'zip') {
+     $allowedMimes = \App\Services\FileUploadService::getAllowedMimes('archive');
+     $maxSize = 512000; // 500MB
+ } else {
+     $allowedMimes = ['application/octet-stream', 'text/plain', 'application/sql'];
+     $maxSize = 102400; // 100MB
+ }
+
  $path = $fileUploadService->uploadSecure($file, 'backup-temp', $allowedMimes, $maxSize);

- $finalPath = Storage::path($path);
+ $finalPath = Storage::disk('public')->path($path);

+ if ($extension === 'zip') {
+     $result = $this->restoreFromZip($finalPath);
+     return response()->json([
+         'message' => "Restore berhasil. Database dan {$result['files_restored']} file asset telah dipulihkan.",
+     ], 200);
+ }
+
  $this->runRestoreDatabase($finalPath);

Fitur — Method restoreFromZip() (BARU):

Method private baru yang menangani ekstraksi ZIP backup spatie:

  1. Membuka ZIP dengan ZipArchive::open() — melempar exception jika file rusak.
  2. Iterasi semua entri ZIP, dikategorisasikan:
    • Entri db-dumps/*.sql → diekstrak ke temp file untuk restore via mysql command.
    • Entri storage (mengandung storage/app/) → diekstrak file individual ke storage/app/ saat ini.
    • Entri di backup-storage/, backup-temp/, framework/, logs/, debugbar/di-skip.
    • Entri lain → di-skip.
  3. Validasi path traversal: setiap target path harus berada di dalam storage/app/.
  4. Validasi: str_contains($relativePath, '..') → ditolak.
  5. Menjalankan runRestoreDatabase() untuk restore DB dari SQL dump yang ditemukan.
  6. Log semua aktivitas untuk audit trail.
  7. Cleanup temp file di finally block.

Fitur — Method mapZipEntryToStoragePath() (BARU):

Method private baru yang memetakan entri ZIP ke path relatif di dalam storage/app/:

  • Menangani baik path absolute (backup lama, relative_path=null) maupun path relative (backup baru, relative_path=base_path()).
  • Normalisasi terhadap perbedaan path separator Windows/Linux.
  • Menolak path traversal (..).
  • Me-skip direktori yang dikecualikan.

Bug Fix — createBackup() tidak cek exit code:

Artisan::call('backup:run') mengembalikan exit code (int), bukan throw exception saat backup gagal. Controller hanya pakai try/catch sehingga selalu return success: true meskipun backup gagal. Sekarang exit code dicek:

- Artisan::call('backup:run');
- Log::info('Backup command output: ' . Artisan::output());
- Log::info('Ending backup process.');
- return response()->json(['success' => true, 'message' => 'Backup completed successfully']);
+ $exitCode = Artisan::call('backup:run');
+ $output = Artisan::output();
+ Log::info('Backup command output: ' . $output);
+ Log::info('Backup exit code: ' . $exitCode);
+
+ if ($exitCode !== 0) {
+     Log::error('Backup process failed with exit code: ' . $exitCode);
+     return response()->json([
+         'success' => false,
+         'message' => 'Backup gagal. Periksa log aplikasi untuk detail.',
+     ], 500);
+ }
+
+ Log::info('Ending backup process.');
+ return response()->json(['success' => true, 'message' => 'Backup completed successfully']);

Bug Fix — runRestoreDatabase() tidak menemukan mysql.exe:

Command mysql dipanggil sebagai bare command tanpa path lengkap. Di Windows/Laragon, mysql.exe tidak ada di PATH sistem. Sekarang menggunakan config('database.connections.mysql.dump.dump_binary_path') (sama seperti spatie untuk mysqldump):

- $command = sprintf(
-     'mysql -h%s -P%s -u%s %s %s < "%s"',
-     escapeshellarg($dbHost),
-     ...
-     $dbPass !== '' ? '-p' . escapeshellarg($dbPass) : '',
-     ...
- );
+ $binaryPath = config('database.connections.mysql.dump.dump_binary_path', '');
+ $mysqlBinary = $binaryPath
+     ? rtrim($binaryPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'mysql'
+     : 'mysql';
+
+ $command = sprintf(
+     '%s -h%s -P%s -u%s %s %s < "%s" 2>&1',
+     escapeshellarg($mysqlBinary),
+     ...
+     $dbPass !== '' ? '--password=' . escapeshellarg($dbPass) : '',
+     ...
+ );

Perbaikan tambahan:

  • Mengganti -p dengan --password=-p + escapeshellarg() rusak di Linux (menghasilkan -p'123456' dengan kutip literal sebagai bagian password).
  • Menambahkan 2>&1 — redirect stderr ke stdout agar pesan error MySQL tercapture di log.

3. resources/views/setting/pengaturan_database/table-restore.blade.php

Fitur — UI restore menerima file .zip:

  • Mengubah accept=".sql"accept=".sql,.zip".
  • Menambahkan info box yang menjelaskan perbedaan .sql (DB only) dan .zip (DB + file asset).
  • Menambahkan peringatan bahwa restore .zip akan menimpa file di storage.
  • Success message sekarang menggunakan response.message dari server (sebelumnya hardcoded).
- <input type="file" id="backupFile" class="form-control" name="backupFile" accept=".sql" required>
- <p class="help-block">Unggah file database (.sql)</p>
+ <input type="file" id="backupFile" class="form-control" name="backupFile" accept=".sql,.zip" required>
+ <p class="help-block">Unggah file backup (.sql atau .zip)</p>
+ <div class="callout callout-warning">
+     <p><strong>Informasi:</strong></p>
+     <ul>
+         <li>File <strong>.sql</strong> — hanya memulihkan database</li>
+         <li>File <strong>.zip</strong> — memulihkan <strong>database + file asset</strong></li>
+     </ul>
+     <p><strong>Peringatan:</strong> Restore file .zip akan menimpa file yang ada di storage.</p>
+ </div>

4. resources/views/setting/pengaturan_database/table-backup.blade.php

Bug Fix — JS runBackup() menampilkan notifikasi sukses palsu:

Callback complete di jQuery AJAX selalu berjalan setelah success maupun error, sehingga pesan "Berhasil membuat salinan database" ditampilkan bahkan saat backup gagal, menimpa pesan error.

  success: function(response) {
      $('#data-backup-database').DataTable().ajax.reload();
+     restoreMessage.html('<p class="text-success">Berhasil membuat salinan database.</p>');
+ },
+ error: function(xhr, status, error) {
+     restoreMessage.html('<p class="text-danger">Error: ' + (xhr.responseJSON?.message || 'Gagal melakukan backup.') + '</p>');
+ },
+ complete: function() {
      btnBackup.addClass("btn-social");
      btnBackup.attr("disabled", false);
      btnBackup.html('<i class="fa fa-plus"></i>Buat Cadangan Baru Database');
  },
- error: function(xhr, status, error) {
-     restoreMessage.html('<p class="text-danger">Error: ' + xhr.responseJSON.message + '</p>');
-     ...
- },
- complete: function() {
-     restoreMessage.html('<p class="text-success">Berhasil membuat salinan database.</p>');
-     ...
- }

5. .env

Konfigurasi — Path binary MySQL:

Menambahkan DB_MYSQLDUMP_PATH yang diperlukan oleh spatie/laravel-backup (untuk mysqldump) dan oleh runRestoreDatabase() (untuk mysql):

  DB_PASSWORD=123456
+ DB_MYSQLDUMP_PATH=C:\laragon\bin\mysql\mysql-8.0.30-winx64\bin

6. Test Files (3 file BARU)

tests/Unit/PengaturanDatabaseTest.php — 10 test cases

Test mapZipEntryToStoragePath() via Reflection — menguji path mapping, path traversal rejection, skip directories, dan cross-platform path normalization:

  • ✅ Path relative baru (storage/app/public/artikel/foto.jpg) → public/artikel/foto.jpg
  • ✅ Path absolute Windows (C:/laragon/.../storage/app/public/artikel/foto.jpg) → public/artikel/foto.jpg
  • ✅ Path absolute Linux (/var/www/html/storage/app/...) → public/artikel/foto.jpg
  • ✅ Path traversal (storage/app/public/../../../etc/passwd) → null (rejected)
  • ✅ Skip db-dumps/, backup-storage/, backup-temp/, framework/, logs/, debugbar/
  • ✅ Entri tanpa marker storage/app/null
  • ✅ Path kosong setelah marker → null
  • ✅ Subdirektori dalam → benar dipetakan

tests/Feature/Settings/PengaturanDatabaseBackupTest.php — 2 test cases

Test createBackup() dengan mocking Artisan::call():

  • ✅ Exit code 0 → response success: true, HTTP 200
  • ✅ Exit code non-zero → response success: false, HTTP 500

tests/Feature/Settings/PengaturanDatabaseRestoreTest.php — 5 test cases

Test restoreBackup() endpoint:

  • ✅ Menolak file .txt → 422
  • ✅ Menolak file .exe → 422
  • ✅ Menolak request tanpa file → 422
  • ✅ File ZIP corrupt → 500 dengan pesan "Gagal membuka file ZIP"
  • ✅ File .sql diterima → tidak 422 (lolos validasi ekstensi)

✅ Test Cases yang Diimplementasikan

  • Backup dari halaman web menghasilkan file ZIP di storage/app/backup-storage/.
  • ZIP backup berisi DB dump (db-dumps/mysql-{dbname}.sql) dan file storage dengan path relatif.
  • backup-storage/ dan backup-temp/ tidak ikut di-ZIP (tidak menumpuk recursively).
  • Restore file .sql berfungsi seperti sebelumnya (backward compatible).
  • Restore file .zip mengembalikan database dan file asset ke lokasi yang benar.
  • File di backup-storage/, backup-temp/, framework/, logs/, debugbar/ tidak diekstrak saat restore.
  • Path traversal (..) dalam entri ZIP ditolak.
  • Backup yang gagal menampilkan notifikasi error (bukan sukses palsu).
  • Restore yang gagal menampilkan pesan error yang informatif di log.
  • Unit test: 10 passed (17 assertions).
  • Feature test backup: 2 passed (5 assertions).
  • Feature test restore: 5 passed (9 assertions).

📸 Cara Menjalankan Uji Coba Manual

  1. Pastikan DB_MYSQLDUMP_PATH di .env sudah menunjuk ke direktori binary MySQL (mis. C:\laragon\bin\mysql\mysql-8.0.30-winx64\bin).

  2. Test Backup:

    php artisan config:clear
    php artisan backup:run

    Verifikasi ZIP dibuat:

    dir storage\app\backup-storage\

    Verifikasi struktur path relatif di dalam ZIP:

    php -r "$z=new ZipArchive(); $f=glob('storage/app/backup-storage/*.zip'); $z->open($f[0]); for($i=0;$i<min(5,$z->numFiles);$i++){echo $z->getNameIndex($i).PHP_EOL;} $z->close();"
  3. Test Restore .sql (backward compat):

    • Buka Pengaturan → Backup Database → tab Restore Database.
    • Upload file .sql — harus berhasil restore database.
  4. Test Restore .zip (fitur baru):

    • Upload file .zip backup dari halaman Restore Database.
    • Verifikasi database terestorasi (cek data kembali).
    • Verifikasi file asset kembali ke storage/app/public/ (mis. cek folder artikel/).
  5. Test notifikasi error backup:

    • Sengaja set DB_MYSQLDUMP_PATH ke path yang salah di .env.
    • Jalankan php artisan config:clear, lalu klik "Buat Cadangan Baru Database".
    • Verifikasi notifikasi error muncul (bukan "Berhasil membuat salinan database").
  6. Jalankan automated tests:

    php artisan test --filter=PengaturanDatabase

📸 Screenshot atau Video

image

⚠️ Catatan Penting

Backup ZIP yang dibuat sebelum PR ini (dengan relative_path: null) menggunakan absolute path di dalam ZIP. Fitur restore .zip dirancang untuk menangani baik path absolute maupun relative, sehingga backup lama tetap bisa di-restore. Namun, disarankan untuk membuat backup baru setelah PR ini di-merge untuk konsistensi dan portabilitas.

Konfigurasi DB_MYSQLDUMP_PATH perlu disesuaikan di setiap environment deployment. Di Linux, biasanya tidak diperlukan jika mysqldump dan mysql sudah ada di PATH sistem.

@habibie11 habibie11 requested a review from vickyrolanda July 3, 2026 09:37
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI PR Review sedang antri di server...

Proses review akan segera dimulai di background — hasil akan muncul sebagai komentar setelah selesai.
Powered by CrewAI · PR #1617

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant