cloudlog/application/controllers/Backup.php
Peter Goodhall 3fdeee9dc6 Add ZIP extension check to backup page
The backup controller now checks if the PHP ZIP extension is installed and passes this information to the view. The backup page displays a warning if the extension is missing, guiding users to install it for backup and restore functionality.
2026-01-15 15:26:15 +00:00

398 lines
17 KiB
PHP

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Backup extends CI_Controller {
function __construct()
{
parent::__construct();
}
/* ===== User-level JSON+ZIP backup (Stations, Logbooks, QSOs) ===== */
public function user_export() {
$this->load->model('user_model');
if ($this->user_model->validate_session() == 0) { redirect('user/login'); }
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
// Increase time limit for large databases
ini_set('max_execution_time', '600');
ini_set('memory_limit', '256M'); // Reduced since we're streaming
$this->load->model('Stations');
$this->load->model('Logbook_model');
$this->load->dbutil();
$tmp_json = tempnam(sys_get_temp_dir(), 'cloudlog_backup_') . '.json';
$tmp_zip = tempnam(sys_get_temp_dir(), 'cloudlog_backup_') . '.zip';
try {
$user_id = $this->session->userdata('user_id');
if (!$user_id) { show_error('User not authenticated.', 401); return; }
while (ob_get_level()) { ob_end_clean(); }
// Open file handle for streaming JSON
$fp = fopen($tmp_json, 'w');
if (!$fp) { throw new Exception('Could not create temporary file'); }
// Start JSON object
fwrite($fp, "{\n");
fwrite($fp, ' "schema_version": "1.0",'."\n");
fwrite($fp, ' "exported_at": "'.date('c').'",'."\n");
// Stations for user
$this->db->where('user_id', $user_id);
$stations = $this->db->get('station_profile')->result_array();
fwrite($fp, ' "stations": '.json_encode($stations, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).",\n");
// QSOs per station - stream in chunks
fwrite($fp, ' "station_qsos": {'."\n");
$first_station = true;
foreach ($stations as $station) {
if (!isset($station['station_id'])) continue;
$station_id = $station['station_id'];
if (!$first_station) { fwrite($fp, ",\n"); }
$first_station = false;
fwrite($fp, ' "'.addslashes($station_id).'": ['."\n");
$qso_chunk_size = 5000; // Smaller chunks for better memory efficiency
$qso_offset = 0;
$first_qso = true;
do {
$this->db->where('station_id', $station_id);
$this->db->limit($qso_chunk_size, $qso_offset);
$chunk = $this->db->get($this->config->item('table_name'))->result_array();
foreach ($chunk as $qso) {
if (!$first_qso) { fwrite($fp, ",\n"); }
$first_qso = false;
fwrite($fp, ' '.json_encode($qso, JSON_UNESCAPED_SLASHES));
}
$qso_offset += $qso_chunk_size;
// Free memory
unset($chunk);
if ($qso_offset % 20000 == 0) {
gc_collect_cycles();
}
} while (count($this->db->where('station_id', $station_id)->limit(1, $qso_offset)->get($this->config->item('table_name'))->result_array()) > 0);
fwrite($fp, "\n".' ]');
}
fwrite($fp, "\n".' },'."\n");
// Logbooks for user - stream QSOs
fwrite($fp, ' "logbooks": ['."\n");
$this->db->where('user_id', $user_id);
$logbooks_result = $this->db->get('station_logbooks')->result_array();
$first_logbook = true;
foreach ($logbooks_result as $logbook) {
if (!$first_logbook) { fwrite($fp, ",\n"); }
$first_logbook = false;
$station_id = isset($logbook['station_id']) ? $logbook['station_id'] : null;
// Write logbook metadata
fwrite($fp, ' {'."\n");
foreach ($logbook as $key => $value) {
fwrite($fp, ' "'.addslashes($key).'": '.json_encode($value, JSON_UNESCAPED_SLASHES).",\n");
}
fwrite($fp, ' "qsos": ['."\n");
// Stream QSOs for this logbook
if ($station_id) {
$qso_chunk_size = 5000;
$qso_offset = 0;
$first_qso = true;
do {
$this->db->where('station_id', $station_id);
$this->db->limit($qso_chunk_size, $qso_offset);
$chunk = $this->db->get($this->config->item('table_name'))->result_array();
foreach ($chunk as $qso) {
if (!$first_qso) { fwrite($fp, ",\n"); }
$first_qso = false;
fwrite($fp, ' '.json_encode($qso, JSON_UNESCAPED_SLASHES));
}
$qso_offset += $qso_chunk_size;
// Free memory
unset($chunk);
if ($qso_offset % 20000 == 0) {
gc_collect_cycles();
}
} while (count($this->db->where('station_id', $station_id)->limit(1, $qso_offset)->get($this->config->item('table_name'))->result_array()) > 0);
}
fwrite($fp, "\n".' ]'."\n");
fwrite($fp, ' }');
}
fwrite($fp, "\n".' ],'."\n");
// Relationships if present
$tables = $this->db->list_tables();
if (in_array('station_logbooks_entity', $tables)) {
$this->db->where('user_id', $user_id);
$entities = $this->db->get('station_logbooks_entity')->result_array();
fwrite($fp, ' "logbooks_entity": '.json_encode($entities, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n");
} else {
fwrite($fp, ' "logbooks_entity": []'."\n");
}
// Close JSON object
fwrite($fp, "}\n");
fclose($fp);
// Create ZIP
$zip = new ZipArchive();
if ($zip->open($tmp_zip, ZipArchive::CREATE) !== TRUE) { throw new Exception('Could not create ZIP file'); }
$zip->addFile($tmp_json, 'cloudlog_backup.json');
$zip->close();
// Send to browser
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="cloudlog_backup_'.date('Ymd_His').'.zip"');
header('Content-Length: ' . filesize($tmp_zip));
header('Cache-Control: no-store, no-cache');
readfile($tmp_zip);
// Cleanup
@unlink($tmp_json); @unlink($tmp_zip); exit;
} catch (Exception $e) {
log_message('error', 'User export error: '.$e->getMessage());
show_error('Export failed: '.$e->getMessage(), 500);
}
}
public function user_import() {
$this->load->model('user_model');
if ($this->user_model->validate_session() == 0) { redirect('user/login'); }
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
ini_set('memory_limit', '512M');
$this->load->model('Stations');
$this->load->model('Logbook_model');
$this->load->library('session');
if (!isset($_FILES['backup_file']) || $_FILES['backup_file']['error'] !== UPLOAD_ERR_OK) { $this->session->set_flashdata('notice', 'No file uploaded or upload error.'); redirect('backup'); return; }
$uploaded_file = $_FILES['backup_file']['tmp_name'];
$temp_json = tempnam(sys_get_temp_dir(), 'cloudlog_import_') . '.json';
$finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $uploaded_file); finfo_close($finfo);
if ($mime_type === 'application/zip') {
$zip = new ZipArchive();
if ($zip->open($uploaded_file) === TRUE) {
$json_content = $zip->getFromName('cloudlog_backup.json'); $zip->close();
if ($json_content === false) { $this->session->set_flashdata('notice', 'Invalid backup file: JSON not found in ZIP.'); redirect('backup'); return; }
file_put_contents($temp_json, $json_content); unset($json_content);
} else { $this->session->set_flashdata('notice', 'Could not open ZIP file.'); redirect('backup'); return; }
} else { copy($uploaded_file, $temp_json); }
$json = file_get_contents($temp_json); $data = json_decode($json, true); unset($json);
if (!$data || !isset($data['stations']) || !isset($data['logbooks'])) { @unlink($temp_json); $this->session->set_flashdata('notice', 'Invalid backup file.'); redirect('backup'); return; }
$station_qsos = isset($data['station_qsos']) ? $data['station_qsos'] : array();
$import_preview = array(
'stations' => array_map(function($station) use ($station_qsos) {
$station_id = isset($station['station_id']) ? $station['station_id'] : null;
$qso_count = ($station_id && isset($station_qsos[$station_id])) ? count($station_qsos[$station_id]) : 0;
return array(
'station_id' => $station_id,
'station_callsign' => $station['station_callsign'],
'station_profile_name' => $station['station_profile_name'],
'qsos_count' => $qso_count,
);
}, $data['stations']),
'logbooks' => array_map(function($logbook) {
$qso_count = isset($logbook['qsos']) ? count($logbook['qsos']) : 0;
return array(
'logbook_id' => $logbook['logbook_id'],
'logbook_name' => $logbook['logbook_name'],
'station_id' => isset($logbook['station_id']) ? $logbook['station_id'] : null,
'qsos_count' => $qso_count,
);
}, $data['logbooks']),
);
$this->session->set_userdata('import_backup_file', $temp_json);
// HTMX partial
$this->load->view('backup/import_preview', $import_preview);
}
public function user_do_import() {
$this->load->model('user_model');
if ($this->user_model->validate_session() == 0) { redirect('user/login'); }
if(!$this->user_model->authorize(2)) { echo '<div class="alert alert-danger">Not authorized</div>'; return; }
ini_set('memory_limit', '512M');
$this->load->model('Stations');
$this->load->model('Logbook_model');
$this->load->library('session');
$temp_file = $this->session->userdata('import_backup_file');
if (!$temp_file || !file_exists($temp_file)) { echo '<div class="alert alert-danger">No import data found.</div>'; return; }
$json = file_get_contents($temp_file); $data = json_decode($json, true); unset($json);
if (!$data) { echo '<div class="alert alert-danger">Failed to parse backup data.</div>'; @unlink($temp_file); $this->session->unset_userdata('import_backup_file'); return; }
$import_stations = $this->input->post('import_stations');
$import_logbooks = $this->input->post('import_logbooks');
if (!is_array($import_stations)) $import_stations = array();
if (!is_array($import_logbooks)) $import_logbooks = array();
if (empty($import_stations) && empty($import_logbooks)) { echo '<div class="alert alert-warning">Please select at least one station or logbook to import.</div>'; return; }
$current_user_id = $this->session->userdata('user_id');
if (!$current_user_id) { echo '<div class="alert alert-danger">User not authenticated.</div>'; return; }
$total_stations = count($import_stations); $total_logbooks = count($import_logbooks); $total_qsos = 0;
foreach ($data['logbooks'] as $logbook) { if (in_array($logbook['logbook_id'], $import_logbooks) && isset($logbook['qsos'])) { $total_qsos += count($logbook['qsos']); } }
if (isset($data['station_qsos'])) { foreach ($data['station_qsos'] as $sid => $qsosArr) { if (in_array($sid, $import_stations)) { $total_qsos += count($qsosArr); } } }
$imported = array('stations'=>0,'logbooks'=>0,'qsos'=>0,'conflicts'=>array(),'step'=>'','total_stations'=>$total_stations,'total_logbooks'=>$total_logbooks,'total_qsos'=>$total_qsos);
$station_id_map = array();
// Stations
$imported['step'] = 'Importing stations';
foreach ($data['stations'] as $station) {
if (!in_array($station['station_id'], $import_stations)) continue; $old_station_id = $station['station_id'];
$this->db->where('station_callsign', $station['station_callsign']);
$this->db->where('station_profile_name', $station['station_profile_name']);
$this->db->where('user_id', $current_user_id);
$conflict = $this->db->get('station_profile')->row_array();
if ($conflict) { $imported['conflicts'][] = 'Station exists (reusing): '.$station['station_callsign'].' - '.$station['station_profile_name'].' (ID '.$conflict['station_id'].')'; $station_id_map[$old_station_id] = $conflict['station_id']; continue; }
unset($station['station_id']); $station['user_id'] = $current_user_id; $station['station_active'] = 0; // never active on import
$this->db->insert('station_profile', $station); $new_station_id = $this->db->insert_id(); if (!$new_station_id) { $imported['conflicts'][] = 'Failed to create station: '.$station['station_callsign'].' - '.$station['station_profile_name']; continue; }
$station_id_map[$old_station_id] = $new_station_id; $imported['stations']++;
}
// Logbooks
$imported['step'] = 'Importing logbooks';
foreach ($data['logbooks'] as $logbook) {
if (!in_array($logbook['logbook_id'], $import_logbooks)) continue;
$this->db->where('logbook_name', $logbook['logbook_name']); $this->db->where('user_id', $current_user_id); $conflict = $this->db->get('station_logbooks')->row_array();
if ($conflict) { $imported['conflicts'][] = 'Logbook exists (skipping): '.$logbook['logbook_name']; continue; }
$logbook_copy = $logbook; unset($logbook_copy['qsos']); unset($logbook_copy['logbook_id']); if (isset($logbook_copy['active_logbook'])) unset($logbook_copy['active_logbook']); $logbook_copy['user_id'] = $current_user_id; if (isset($logbook_copy['station_id']) && isset($station_id_map[$logbook_copy['station_id']])) { $logbook_copy['station_id'] = $station_id_map[$logbook_copy['station_id']]; }
$this->db->insert('station_logbooks', $logbook_copy); $imported['logbooks']++;
}
// QSOs from station_qsos
$imported['step'] = 'Importing QSOs';
if (isset($data['station_qsos'])) {
foreach ($data['station_qsos'] as $old_station_id => $qsos) {
if (!in_array($old_station_id, $import_stations)) continue; if (!isset($station_id_map[$old_station_id])) continue; $new_station_id = $station_id_map[$old_station_id];
foreach (array_chunk($qsos, 1000) as $qso_chunk) {
foreach ($qso_chunk as $qso) {
unset($qso['COL_PRIMARY_KEY']); $qso['station_id'] = $new_station_id;
$this->db->where('COL_TIME_ON', $qso['COL_TIME_ON']); $this->db->where('COL_CALL', $qso['COL_CALL']); $this->db->where('station_id', $qso['station_id']);
$conflict = $this->db->get($this->config->item('table_name'))->row_array(); if ($conflict) { $imported['conflicts'][] = 'QSO conflict: '.$qso['COL_CALL'].' @ '.$qso['COL_TIME_ON']; continue; }
$this->db->insert($this->config->item('table_name'), $qso); $imported['qsos']++;
}
}
}
}
// QSOs from logbooks (legacy)
foreach ($data['logbooks'] as $logbook) {
if (!in_array($logbook['logbook_id'], $import_logbooks)) continue; if (!isset($logbook['qsos'])) continue;
foreach (array_chunk($logbook['qsos'], 1000) as $qso_chunk) {
foreach ($qso_chunk as $qso) {
unset($qso['COL_PRIMARY_KEY']); if (isset($qso['station_id']) && isset($station_id_map[$qso['station_id']])) { $qso['station_id'] = $station_id_map[$qso['station_id']]; }
$this->db->where('COL_TIME_ON', $qso['COL_TIME_ON']); $this->db->where('COL_CALL', $qso['COL_CALL']); $this->db->where('station_id', $qso['station_id']);
$conflict = $this->db->get($this->config->item('table_name'))->row_array(); if ($conflict) { $imported['conflicts'][] = 'QSO conflict: '.$qso['COL_CALL'].' @ '.$qso['COL_TIME_ON']; continue; }
$this->db->insert($this->config->item('table_name'), $qso); $imported['qsos']++;
}
}
}
$temp_file = $this->session->userdata('import_backup_file'); if ($temp_file && file_exists($temp_file)) { @unlink($temp_file); }
$this->session->unset_userdata('import_backup_file');
$this->load->view('backup/import_progress', $imported);
}
/* User Facing Links to Backup URLs */
public function index()
{
$this->load->model('user_model');
// Allow all authenticated operators (level 2+) instead of only admins
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
$data['page_title'] = "Backup";
$data['zip_installed'] = in_array('zip', get_loaded_extensions());
$this->load->view('interface_assets/header', $data);
$this->load->view('backup/main');
$this->load->view('interface_assets/footer');
}
/* Gets all QSOs and Dumps them to logbook.adi */
public function adif($key = null){
if ($key == null) {
$this->load->model('user_model');
if(!$this->user_model->authorize(99)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
}
$this->load->helper('file');
// Set memory limit to unlimited to allow heavy usage
ini_set('memory_limit', '-1');
$this->load->model('adif_data');
$data['qsos'] = $this->adif_data->export_all($key);
$data['filename'] = 'backup/logbook'. date('_Y_m_d_H_i_s') .'.adi';
if ( ! write_file($data['filename'], $this->load->view('backup/exportall', $data, true)))
{
$data['status'] = false;
}
else
{
$data['status'] = true;
}
$data['page_title'] = "ADIF - Backup";
$this->load->view('interface_assets/header', $data);
$this->load->view('backup/adif_view');
$this->load->view('interface_assets/footer');
}
/* Export the notes to XML */
public function notes($key = null) {
if ($key == null) {
$this->load->model('user_model');
if(!$this->user_model->authorize(99)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
}
$this->load->helper('file');
$this->load->model('note');
$data['list_note'] = $this->note->list_all($key);
$data['filename'] = 'backup/notes'. date('_Y_m_d_H_i_s') .'.xml';
if ( ! write_file($data['filename'], $this->load->view('backup/notes', $data, true)))
{
$data['status'] = false;
}
else
{
$data['status'] = true;
}
$data['page_title'] = "Notes - Backup";
$this->load->view('interface_assets/header', $data);
$this->load->view('backup/notes_view');
$this->load->view('interface_assets/footer');
}
}
/* End of file Backup.php */