This commit is contained in:
Dwi Swandhana
2026-08-01 12:15:25 +07:00
parent 410916f963
commit 119f73403e
8 changed files with 933 additions and 301 deletions

No files matched your search

@@ -0,0 +1,155 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Symfony\Component\Process\Process;
class BackupRestoreLatestCommand extends Command
{
protected $signature = 'backup:restore-latest
{--url= : Base URL server cloud backup}
{--token= : Token API cloud backup}
{--name= : Nama backup}
{--output= : Lokasi file download}
{--file= : Pakai file backup lokal, tanpa download}
{--download-only : Hanya download, tidak restore}
{--host= : Host PostgreSQL target}
{--port= : Port PostgreSQL target}
{--database= : Database PostgreSQL target}
{--username= : Username PostgreSQL target}
{--password= : Password PostgreSQL target, opsional jika .pgpass sudah ada}
{--clean=1 : Jalankan pg_restore --clean}
{--if-exists=1 : Jalankan pg_restore --if-exists}
{--no-owner=1 : Jalankan pg_restore --no-owner}
{--yes : Lewati konfirmasi restore}';
protected $description = 'Download backup terbaru dari duidev.com dan restore penuh ke PostgreSQL.';
public function handle(): int
{
$backupName = (string) ($this->option('name') ?: config('cloud_backup.backup_name', 'lismikro'));
$file = $this->option('file')
? (string) $this->option('file')
: $this->downloadLatest($backupName);
if (!$file || !File::exists($file)) {
$this->error('File backup tidak ditemukan.');
return Command::FAILURE;
}
$this->info('File backup siap: ' . $file);
if ($this->option('download-only')) {
return Command::SUCCESS;
}
$host = (string) ($this->option('host') ?: config('cloud_backup.db_host'));
$port = (string) ($this->option('port') ?: config('cloud_backup.db_port'));
$database = (string) ($this->option('database') ?: config('cloud_backup.db_database'));
$username = (string) ($this->option('username') ?: config('cloud_backup.db_username'));
if (!$this->option('yes') && !$this->confirm("Restore penuh ke database {$database} di {$host}:{$port}?", false)) {
$this->warn('Restore dibatalkan.');
return Command::FAILURE;
}
$command = [
(string) config('cloud_backup.pg_restore', 'pg_restore'),
'-h', $host,
'-p', $port,
'-U', $username,
'-d', $database,
'-v',
];
if ((int) $this->option('clean') === 1) {
$command[] = '--clean';
}
if ((int) $this->option('if-exists') === 1) {
$command[] = '--if-exists';
}
if ((int) $this->option('no-owner') === 1) {
$command[] = '--no-owner';
}
$command[] = $file;
$env = null;
$password = (string) ($this->option('password') ?: config('cloud_backup.db_password'));
if ($password !== '') {
$env = ['PGPASSWORD' => $password];
}
$process = new Process($command, base_path(), $env, null, null);
$process->setTimeout(null);
$this->info('Menjalankan pg_restore...');
$process->run(function (string $type, string $buffer): void {
$this->output->write($buffer);
});
if (!$process->isSuccessful()) {
$this->error('Restore gagal dengan kode: ' . $process->getExitCode());
return Command::FAILURE;
}
$this->info('Restore selesai.');
return Command::SUCCESS;
}
private function downloadLatest(string $backupName): ?string
{
$baseUrl = rtrim((string) ($this->option('url') ?: config('cloud_backup.api_url')), '/');
$token = (string) ($this->option('token') ?: config('cloud_backup.api_token'));
if ($baseUrl === '' || $token === '') {
$this->error('CLOUD_BACKUP_API_URL dan CLOUD_BACKUP_API_TOKEN wajib diisi.');
return null;
}
$output = $this->option('output')
? (string) $this->option('output')
: (string) config('cloud_backup.download_path');
File::ensureDirectoryExists(dirname($output), 0750, true);
$metadata = Http::withToken($token)
->timeout(60)
->get($baseUrl . '/api/backups/latest', ['backup_name' => $backupName]);
if (!$metadata->successful()) {
$this->error('Gagal membaca metadata backup terbaru. HTTP ' . $metadata->status());
$this->line($metadata->body());
return null;
}
$info = $metadata->json();
$this->info('Backup terbaru: ' . ($info['file'] ?? '-'));
$download = Http::withToken($token)
->timeout(0)
->sink($output)
->get($baseUrl . '/api/backups/latest/download', ['backup_name' => $backupName]);
if (!$download->successful()) {
File::delete($output);
$this->error('Gagal download backup. HTTP ' . $download->status());
$this->line($download->body());
return null;
}
if (!empty($info['sha256']) && hash_file('sha256', $output) !== strtolower((string) $info['sha256'])) {
File::delete($output);
$this->error('Checksum file download tidak sesuai.');
return null;
}
return $output;
}
}
@@ -0,0 +1,95 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class BackupUploadFileCommand extends Command
{
protected $signature = 'backup:upload-file
{file : Path file .backup yang akan dikirim}
{--url= : Base URL server cloud backup}
{--token= : Token API cloud backup}
{--name= : Nama backup}
{--chunk-size= : Ukuran chunk dalam byte}';
protected $description = 'Upload file backup ke duidev.com secara bertahap/chunked.';
public function handle(): int
{
$file = (string) $this->argument('file');
if (!is_file($file) || !is_readable($file)) {
$this->error('File backup tidak ditemukan atau tidak bisa dibaca: ' . $file);
return Command::FAILURE;
}
$baseUrl = rtrim((string) ($this->option('url') ?: config('cloud_backup.api_url')), '/');
$token = (string) ($this->option('token') ?: config('cloud_backup.api_token'));
$backupName = (string) ($this->option('name') ?: config('cloud_backup.backup_name', 'lismikro'));
$chunkSize = (int) ($this->option('chunk-size') ?: config('cloud_backup.chunk_size', 5 * 1024 * 1024));
if ($baseUrl === '' || $token === '') {
$this->error('CLOUD_BACKUP_API_URL dan CLOUD_BACKUP_API_TOKEN wajib diisi.');
return Command::FAILURE;
}
if ($chunkSize < 1024 * 1024) {
$this->error('Chunk size minimal 1 MB.');
return Command::FAILURE;
}
$size = filesize($file);
$checksum = hash_file('sha256', $file);
$totalChunks = (int) ceil($size / $chunkSize);
$uploadId = now()->format('YmdHis') . '_' . Str::random(16);
$endpoint = $baseUrl . '/api/backups/chunk';
$this->info('Upload backup: ' . basename($file));
$this->info('Total chunk: ' . $totalChunks);
$handle = fopen($file, 'rb');
if (!$handle) {
$this->error('Gagal membuka file backup.');
return Command::FAILURE;
}
try {
for ($index = 0; $index < $totalChunks; $index++) {
$chunk = fread($handle, $chunkSize);
if ($chunk === false) {
$this->error('Gagal membaca chunk ke-' . $index);
return Command::FAILURE;
}
$response = Http::withToken($token)
->timeout(300)
->attach('chunk', $chunk, 'chunk_' . $index . '.part')
->post($endpoint, [
'backup_name' => $backupName,
'upload_id' => $uploadId,
'filename' => basename($file),
'chunk_index' => $index,
'total_chunks' => $totalChunks,
'total_size' => $size,
'checksum' => $checksum,
]);
if (!$response->successful()) {
$this->error('Upload chunk ke-' . $index . ' gagal. HTTP ' . $response->status());
$this->line($response->body());
return Command::FAILURE;
}
$this->line('Chunk ' . ($index + 1) . '/' . $totalChunks . ' terkirim.');
}
} finally {
fclose($handle);
}
$this->info('Upload backup selesai.');
return Command::SUCCESS;
}
}
@@ -0,0 +1,138 @@
<?php
namespace App\Console\Commands;
use App\Periksa;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class ExportRegisterApiByDateCommand extends Command
{
protected $signature = 'register-api:export
{date : Tanggal data periksa, format YYYY-MM-DD}
{--column=daftar : Kolom tanggal untuk filter, contoh daftar/tanggalregis/tanggalsampel}
{--output= : Simpan hasil JSON ke file}
{--pretty : Format JSON agar mudah dibaca}';
protected $description = 'Export data periksa per tanggal ke format JSON payload registerApi.';
public function handle(): int
{
$date = $this->argument('date');
$column = (string) $this->option('column');
if (!$this->isValidDate($date)) {
$this->error('Format tanggal harus YYYY-MM-DD.');
return Command::FAILURE;
}
if (!in_array($column, ['daftar', 'tanggalregis', 'tanggalsampel'], true)) {
$this->error('Kolom tanggal hanya boleh: daftar, tanggalregis, tanggalsampel.');
return Command::FAILURE;
}
$records = Periksa::with('getPasien')
->whereDate($column, $date)
->orderBy($column)
->orderBy('id')
->get()
->map(fn (Periksa $periksa) => $this->mapPeriksaToRegisterPayload($periksa))
->values();
$payload = [
'tanggal' => $date,
'filter_column' => $column,
'total' => $records->count(),
'data' => $records,
];
$jsonOptions = JSON_UNESCAPED_SLASHES;
if ($this->option('pretty')) {
$jsonOptions |= JSON_PRETTY_PRINT;
}
$json = json_encode($payload, $jsonOptions);
if ($json === false) {
$this->error('Gagal membuat JSON.');
return Command::FAILURE;
}
if ($this->option('output')) {
$output = (string) $this->option('output');
File::ensureDirectoryExists(dirname($output));
File::put($output, $json . PHP_EOL);
$this->info('Export selesai: ' . $output);
$this->info('Total data: ' . $records->count());
return Command::SUCCESS;
}
$this->line($json);
return Command::SUCCESS;
}
private function mapPeriksaToRegisterPayload(Periksa $periksa): array
{
$pasien = $periksa->getPasien;
$tanggalLahir = $this->formatDate($periksa->tgllahirpasien ?? $pasien?->tgl_lahir);
$tanggalRegis = $this->formatDate($periksa->tanggalregis) ?: $this->formatDate($periksa->daftar);
$tanggalSampel = $this->formatDate($periksa->tanggalsampel) ?: $tanggalRegis;
$diagnosa = $periksa->klinis ?: null;
$klinisi = $periksa->klinisi ?: null;
return [
'norm' => $periksa->noregister,
'nama' => $periksa->nmpasien,
'alamat' => $periksa->alamatpasien ?: config('global.addressapps'),
'kota' => $pasien?->kota ?: config('global.subdomainapps'),
'tgllahir' => $tanggalLahir ?: date('Y-m-d'),
'nohap' => $periksa->tlppasien ?: '0000000000',
'kelamin' => $periksa->jkpasien ?: 'L',
'urgensi' => $periksa->urgensi ?: 'Elective',
'beratbadan' => (string) ($periksa->berat ?? '0'),
'nik' => $periksa->ktp ?: '0',
'nobpjs' => $periksa->bpjs ?: '0',
'drpeminta' => $klinisi,
'asalpasien' => $periksa->asalpasien,
'diagnosa' => $diagnosa,
'statusbayar' => null,
'nomor_lab' => $periksa->orderid,
'jenispembayaran' => $periksa->asuransi ?: 'UMM',
'klinisi' => $klinisi,
'jenispemeriksaan' => $periksa->reques,
'spesimen' => $periksa->nm_spesimen,
'kodespesimen' => $periksa->kd_spesimen,
'pengambilan' => $periksa->pengambilan,
'keteranganrujukan' => $periksa->nmrs ?: 'RSSA Malang',
'namarspengirim' => $periksa->nmrs ?: 'RSSA Malang',
'tanggalregis' => $tanggalRegis ?: date('Y-m-d'),
'tanggalsampel' => $tanggalSampel ?: date('Y-m-d'),
'asalpengirim' => $periksa->asalpengirim,
];
}
private function isValidDate(string $date): bool
{
try {
$parsed = Carbon::createFromFormat('Y-m-d', $date);
} catch (\Throwable) {
return false;
}
return $parsed && $parsed->format('Y-m-d') === $date;
}
private function formatDate(mixed $value): ?string
{
if (!$value) {
return null;
}
try {
return Carbon::parse($value)->format('Y-m-d');
} catch (\Throwable) {
return null;
}
}
}