db = $db; } /** * Create a full backup * * @param bool $includeFiles Include uploaded files in backup * @return string|false Path to backup file or false on error */ public function createBackup($includeFiles = true) { global $conf; $backupDir = $conf->kundenkarte->dir_output.'/backups'; if (!is_dir($backupDir)) { dol_mkdir($backupDir); } $timestamp = date('Y-m-d_H-i-s'); $backupName = 'kundenkarte_backup_'.$timestamp; $tempDir = $backupDir.'/'.$backupName; if (!dol_mkdir($tempDir)) { $this->error = 'Cannot create backup directory'; return false; } // Export database tables $dbData = $this->exportDatabaseTables(); if ($dbData === false) { return false; } // Save database export as JSON $dbFile = $tempDir.'/database.json'; if (file_put_contents($dbFile, json_encode($dbData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) === false) { $this->error = 'Cannot write database file'; return false; } // Create metadata file $metadata = array( 'version' => '3.6.0', 'created' => date('Y-m-d H:i:s'), 'tables' => array_keys($dbData), 'record_counts' => array(), 'includes_files' => $includeFiles, ); foreach ($dbData as $table => $records) { $metadata['record_counts'][$table] = count($records); } file_put_contents($tempDir.'/metadata.json', json_encode($metadata, JSON_PRETTY_PRINT)); // Copy uploaded files if requested if ($includeFiles) { $filesDir = $conf->kundenkarte->dir_output.'/anlagen'; if (is_dir($filesDir)) { $this->copyDirectory($filesDir, $tempDir.'/files'); } } // Create ZIP archive $zipFile = $backupDir.'/'.$backupName.'.zip'; if (!$this->createZipArchive($tempDir, $zipFile)) { $this->error = 'Cannot create ZIP archive'; return false; } // Clean up temp directory $this->deleteDirectory($tempDir); return $zipFile; } /** * Export all database tables * * @return array|false Array of table data or false on error */ private function exportDatabaseTables() { global $conf; $data = array(); foreach ($this->tables as $table) { $fullTable = MAIN_DB_PREFIX.$table; // Check if table exists $sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'"; $resql = $this->db->query($sql); if (!$resql || $this->db->num_rows($resql) == 0) { continue; // Skip non-existent tables } $records = array(); $sql = "SELECT * FROM ".$fullTable; $sql .= " WHERE entity = ".((int) $conf->entity); $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_array($resql)) { $records[] = $obj; } $this->db->free($resql); } $data[$table] = $records; } return $data; } /** * Restore from a backup file * * @param string $backupFile Path to backup ZIP file * @param bool $clearExisting Clear existing data before restore * @return bool True on success, false on error */ public function restoreBackup($backupFile, $clearExisting = false) { global $conf, $user; if (!file_exists($backupFile)) { $this->error = 'Backup file not found'; return false; } // Create temp directory for extraction $tempDir = $conf->kundenkarte->dir_output.'/backups/restore_'.uniqid(); if (!dol_mkdir($tempDir)) { $this->error = 'Cannot create temp directory'; return false; } // Extract ZIP $zip = new ZipArchive(); if ($zip->open($backupFile) !== true) { $this->error = 'Cannot open backup file'; return false; } $zip->extractTo($tempDir); $zip->close(); // Read metadata $metadataFile = $tempDir.'/metadata.json'; if (!file_exists($metadataFile)) { $this->error = 'Invalid backup: metadata.json not found'; $this->deleteDirectory($tempDir); return false; } $metadata = json_decode(file_get_contents($metadataFile), true); // Read database data $dbFile = $tempDir.'/database.json'; if (!file_exists($dbFile)) { $this->error = 'Invalid backup: database.json not found'; $this->deleteDirectory($tempDir); return false; } $dbData = json_decode(file_get_contents($dbFile), true); $this->db->begin(); try { // Clear existing data if requested if ($clearExisting) { $this->clearExistingData(); } // Import database tables (in correct order) foreach ($this->tables as $table) { if (isset($dbData[$table])) { $this->importTable($table, $dbData[$table]); } } // Restore files if included if (!empty($metadata['includes_files']) && is_dir($tempDir.'/files')) { $filesDir = $conf->kundenkarte->dir_output.'/anlagen'; if (!is_dir($filesDir)) { dol_mkdir($filesDir); } $this->copyDirectory($tempDir.'/files', $filesDir); } $this->db->commit(); } catch (Exception $e) { $this->db->rollback(); $this->error = $e->getMessage(); $this->deleteDirectory($tempDir); return false; } // Clean up $this->deleteDirectory($tempDir); return true; } /** * Clear existing data for this entity */ private function clearExistingData() { global $conf; // Delete in reverse order to respect foreign keys $reverseTables = array_reverse($this->tables); foreach ($reverseTables as $table) { $fullTable = MAIN_DB_PREFIX.$table; // Check if table exists $sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'"; $resql = $this->db->query($sql); if (!$resql || $this->db->num_rows($resql) == 0) { continue; } $sql = "DELETE FROM ".$fullTable." WHERE entity = ".((int) $conf->entity); $this->db->query($sql); } } /** * Import data into a table * * @param string $table Table name (without prefix) * @param array $records Array of records */ private function importTable($table, $records) { global $conf; if (empty($records)) { return; } $fullTable = MAIN_DB_PREFIX.$table; // Check if table exists $sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'"; $resql = $this->db->query($sql); if (!$resql || $this->db->num_rows($resql) == 0) { return; } // Get column info $columns = array(); $sql = "SHOW COLUMNS FROM ".$fullTable; $resql = $this->db->query($sql); while ($obj = $this->db->fetch_object($resql)) { $columns[] = $obj->Field; } foreach ($records as $record) { // Build insert statement $fields = array(); $values = array(); foreach ($record as $field => $value) { if (!in_array($field, $columns)) { continue; // Skip unknown columns } $fields[] = $field; if ($value === null) { $values[] = 'NULL'; } elseif (is_numeric($value)) { $values[] = $value; } else { $values[] = "'".$this->db->escape($value)."'"; } } if (empty($fields)) { continue; } $sql = "INSERT INTO ".$fullTable." (".implode(', ', $fields).") VALUES (".implode(', ', $values).")"; $sql .= " ON DUPLICATE KEY UPDATE "; $updates = array(); foreach ($fields as $i => $field) { if ($field != 'rowid') { $updates[] = $field." = ".$values[$i]; } } $sql .= implode(', ', $updates); if (!$this->db->query($sql)) { throw new Exception('Error importing '.$table.': '.$this->db->lasterror()); } } } /** * Get list of available backups * * @return array Array of backup info */ public function getBackupList() { global $conf; $backups = array(); $backupDir = $conf->kundenkarte->dir_output.'/backups'; if (!is_dir($backupDir)) { return $backups; } $files = glob($backupDir.'/kundenkarte_backup_*.zip'); if ($files) { foreach ($files as $file) { $filename = basename($file); // Extract date from filename if (preg_match('/kundenkarte_backup_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.zip/', $filename, $matches)) { $date = str_replace('_', ' ', $matches[1]); $date = str_replace('-', ':', substr($date, 11)); $date = substr($matches[1], 0, 10).' '.$date; $backups[] = array( 'file' => $file, 'filename' => $filename, 'date' => $date, 'size' => filesize($file), ); } } } // Sort by date descending usort($backups, function ($a, $b) { return strcmp($b['date'], $a['date']); }); return $backups; } /** * Delete a backup file * * @param string $filename Backup filename * @return bool */ public function deleteBackup($filename) { global $conf; $file = $conf->kundenkarte->dir_output.'/backups/'.basename($filename); if (file_exists($file) && strpos($filename, 'kundenkarte_backup_') === 0) { return unlink($file); } return false; } /** * Copy directory recursively * * @param string $src Source directory * @param string $dst Destination directory */ private function copyDirectory($src, $dst) { if (!is_dir($dst)) { dol_mkdir($dst); } $dir = opendir($src); while (($file = readdir($dir)) !== false) { if ($file == '.' || $file == '..') { continue; } $srcFile = $src.'/'.$file; $dstFile = $dst.'/'.$file; if (is_dir($srcFile)) { $this->copyDirectory($srcFile, $dstFile); } else { copy($srcFile, $dstFile); } } closedir($dir); } /** * Delete directory recursively * * @param string $dir Directory path */ private function deleteDirectory($dir) { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), array('.', '..')); foreach ($files as $file) { $path = $dir.'/'.$file; if (is_dir($path)) { $this->deleteDirectory($path); } else { unlink($path); } } rmdir($dir); } /** * Create ZIP archive from directory * * @param string $sourceDir Source directory * @param string $zipFile Target ZIP file * @return bool */ private function createZipArchive($sourceDir, $zipFile) { $zip = new ZipArchive(); if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { return false; } $sourceDir = realpath($sourceDir); $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($sourceDir), RecursiveIteratorIterator::LEAVES_ONLY ); foreach ($files as $file) { if (!$file->isDir()) { $filePath = $file->getRealPath(); $relativePath = substr($filePath, strlen($sourceDir) + 1); $zip->addFile($filePath, $relativePath); } } return $zip->close(); } /** * Get backup statistics * * @return array Statistics array */ public function getStatistics() { global $conf; $stats = array( 'total_anlagen' => 0, 'total_files' => 0, 'total_connections' => 0, 'total_customers' => 0, 'files_size' => 0, ); // Count anlagen $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE entity = ".((int) $conf->entity); $resql = $this->db->query($sql); if ($resql && $obj = $this->db->fetch_object($resql)) { $stats['total_anlagen'] = $obj->cnt; } // Count files $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files WHERE entity = ".((int) $conf->entity); $resql = $this->db->query($sql); if ($resql && $obj = $this->db->fetch_object($resql)) { $stats['total_files'] = $obj->cnt; } // Count connections $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_connection WHERE entity = ".((int) $conf->entity); $resql = $this->db->query($sql); if ($resql && $obj = $this->db->fetch_object($resql)) { $stats['total_connections'] = $obj->cnt; } // Count customers with anlagen $sql = "SELECT COUNT(DISTINCT fk_soc) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE entity = ".((int) $conf->entity); $resql = $this->db->query($sql); if ($resql && $obj = $this->db->fetch_object($resql)) { $stats['total_customers'] = $obj->cnt; } // Calculate files size $filesDir = $conf->kundenkarte->dir_output.'/anlagen'; if (is_dir($filesDir)) { $stats['files_size'] = $this->getDirectorySize($filesDir); } return $stats; } /** * Get directory size recursively * * @param string $dir Directory path * @return int Size in bytes */ private function getDirectorySize($dir) { $size = 0; foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)) as $file) { if ($file->isFile()) { $size += $file->getSize(); } } return $size; } }