

Hướng dẫn tùy chỉnh phpmyadmin fix lỗi export Database 30MB Cyberpanel
- 27-07-2025
- Toanngo92
- 0 Comments
Với tính năng của phpmyadmin trong cyberpanel, khi xuất file database thông qua nút export, sẽ gặp vấn đề khi kích thước tệp lớn hơn 30MB, tệp sẽ bị tự động cắt với lỗi trả ra là: The dynamic response body size is over the limit, the response will be truncated by the web server. The limit is set in the key ‘maxDynRespSize’ located in the tuning section of the server configuration, and labeled ‘max dynamic response body size. Mặc dù khi bạn tăng cấu hình này trong onelitespeed, lỗi vẫn không được khắc phục. Sau khi tìm hiểu trên cộng đồng cũng như hỏi AI, giải pháp cuối cùng mình lựa chọn là code thêm cho phpmyadmin để đáp ứng tính năng, chờ nhà phát hành vá lỗi.
Giải pháp tổng thể như sau:
- Tạo worker chạy liên tục để job dump sql ra thư mục backups
- Tùy chỉnh thêm 1 nút backup trong phpmyadmin và đẩy sự kiện cho worker thực thi
- Tạo tệp lắng nghe trạng thái worker
- Tạo tệp download cho phép tải CSDL đã backup từ worker.
Nội dung tệp backup_worker.sh:
#!/bin/bash
while true; do
for file in /var/backups/queue/*.txt; do
[ -f "$file" ] || continue
DB=$(cat "$file")
# Validate database name (security check)
if [[ ! "$DB" =~ ^[a-zA-Z0-9_]+$ ]]; then
echo "{\"error\":\"Invalid database name: $DB\",\"failed_at\":\"$(date '+%Y-%m-%d %H:%M:%S')\"}" > "$file.fail"
rm "$file"
continue
fi
OUT="/usr/local/CyberCP/public/phpmyadmin/backups/${DB}_$(date '+%Y-%m-%d_%H-%M-%S').sql"
OUT_ZIP="/usr/local/CyberCP/public/phpmyadmin/backups/${DB}_$(date '+%Y-%m-%d_%H-%M-%S').sql.zip"
# Thực hiện backup và kiểm tra kết quả
if mysqldump --defaults-extra-file=/root/.my.cnf "$DB" > "$OUT" && \
zip "${OUT}.zip" "$OUT" && rm "$OUT"; then
# Kiểm tra file backup đã được tạo và có kích thước > 0
if [ -f "$OUT_ZIP" ] && [ -s "$OUT_ZIP" ]; then
# Backup thành công: tạo file .done với thông tin đường dẫn file
cat > "$file.done" << EOF
{"file":"$OUT_ZIP","database":"$DB","completed_at":"$(date '+%Y-%m-%d %H:%M:%S')","size":$(stat -f%z "$OUT" 2>/dev/null || stat -c%s "$OUT" 2>/dev/null || echo "0")}
EOF
else
# File backup rỗng hoặc không tồn tại
echo "{\"error\":\"Backup file is empty or not created for database: $DB\",\"failed_at\":\"$(date '+%Y-%m-%d %H:%M:%S')\"}" > "$file.fail"
[ -f "$OUT_ZIP" ] && rm "$OUT_ZIP"
fi
else
# Backup thất bại: tạo file .fail với thông tin lỗi
echo "{\"error\":\"mysqldump failed for database: $DB\",\"failed_at\":\"$(date '+%Y-%m-%d %H:%M:%S')\"}" > "$file.fail"
# Xóa file backup không hoàn chỉnh (nếu có)
[ -f "$OUT" ] && rm "$OUT"
fi
# Xóa file job gốc
rm "$file"
done
sleep 10
done
Thêm tệp config.header.inc.php tại thư mục gốc phpmyadmin:
<?php
if (!defined('PHPMYADMIN')) exit;
?>
<script>
document.addEventListener('DOMContentLoaded', function() {
/** ================= helpers ================= **/
function getDbName() {
// Ưu tiên API nội bộ của phpMyAdmin nếu có
if (typeof PMA_commonParams !== 'undefined' && PMA_commonParams.get) {
return PMA_commonParams.get('db') || '';
}
const params = new URLSearchParams(location.search);
return params.get('db') || '';
}
const API = {
start: (db) => 'custom/quick-backup.php?db=' + encodeURIComponent(db),
status: (jobId) => 'custom/backup-status.php?job_id=' + encodeURIComponent(jobId)
};
function createBtn(dbName) {
const btn = document.createElement('button');
btn.type = 'button';
btn.id = 'quick-backup-btn';
btn.dataset.db = dbName;
btn.textContent = 'Quick Backup';
btn.className = 'btn btn-success';
btn.style.marginLeft = '10px';
return btn;
}
function downloadViaIframe(url) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
// có thể remove sau vài giây nếu muốn
setTimeout(() => iframe.remove(), 60000);
}
function injectQuickBackupButton() {
if (!location.href.includes('db=')) return;
const dbName = getDbName();
// Nếu đã có, chỉ cập nhật lại db
let btn = document.getElementById('quick-backup-btn');
if (btn) {
btn.dataset.db = dbName;
return;
}
btn = createBtn(dbName);
const exportBtn = document.querySelector('form[name="dump"] input[type=submit]');
if (exportBtn) exportBtn.insertAdjacentElement('afterend', btn);
btn.addEventListener('click', async function() {
const db = btn.dataset.db || getDbName();
if (!db) {
alert('Không lấy được tên database.');
return;
}
btn.disabled = true;
const oldText = btn.textContent;
btn.textContent = 'Starting backup…';
try {
// 1) Gửi yêu cầu tạo job
const startRes = await fetch(API.start(db), {
method: 'GET'
});
const startJson = await startRes.json();
if (!startJson.success) {
throw new Error(startJson.message || startJson.error || 'Cannot start job');
}
const jobId = startJson.job_id;
btn.textContent = 'Backing up… (Job: ' + jobId + ')';
// 2) Poll status cho tới khi done/failed
await pollUntilDone(jobId, function onDone(downloadUrl) {
// get current path
const currentPath = location.pathname;
const host = location.host;
console.log('Current path:', currentPath);
console.log('Host:', host);
let download = new URL(currentPath, 'https://' + host);
download.href = download.href.replace('/index.php', downloadUrl);
// window.open(download.href, '_blank');
downloadViaIframe(download.href);
});
btn.textContent = 'Done! Downloading…';
} catch (e) {
console.error(e);
alert('Backup failed: ' + e.message);
btn.textContent = oldText;
} finally {
btn.disabled = false;
setTimeout(() => (btn.textContent = oldText), 3000);
}
});
}
async function pollUntilDone(jobId, onDone) {
const maxAttempts = 90; // ~3 phút nếu interval = 2000ms
const intervalMs = 2000;
for (let i = 0; i < maxAttempts; i++) {
const stRes = await fetch(API.status(jobId));
const stJson = await stRes.json();
if (stJson.status === 'done' && stJson.download_url) {
onDone(stJson.download_url);
return;
}
if (stJson.status === 'failed') {
throw new Error(stJson.message || 'Worker failed');
}
// pending
await sleep(intervalMs);
}
throw new Error('Timeout: backup still pending.');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/** ================= boot ================= **/
injectQuickBackupButton();
// Lắng nghe thay đổi form / render lại bởi AJAX
document.addEventListener('change', function(e) {
if (
e.target.matches('select[name="db"]') ||
e.target.closest('form[name="dump"]')
) {
injectQuickBackupButton();
}
});
const observer = new MutationObserver(() => {
if (location.href.includes('db=')) injectQuickBackupButton();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
</script>
Tạo thư mục custom, bên trong chứa 2 tệp:
- backup-status.php
- download.php
Nội dung tệp backup-status.php:
<?php
// custom/backup-status.php
header('Content-Type: application/json; charset=utf-8');
$jobId = $_GET['job_id'] ?? '';
if (!$jobId) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing job_id']);
exit;
}
// Validate job_id format to prevent directory traversal
if (!preg_match('/^job_[a-zA-Z0-9_.]+$/', $jobId)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Invalid job_id format']);
exit;
}
$queueDir = '/var/backups/queue';
$queueFile = "$queueDir/$jobId.txt";
$done = "$queueDir/$jobId.txt.done";
$fail = "$queueDir/$jobId.txt.fail";
// Check if job completed successfully
if (file_exists($done)) {
$jsonContent = file_get_contents($done);
if ($jsonContent === false) {
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Cannot read done file']);
exit;
}
// Trim whitespace and check if it's just a database name (old format)
$trimmedContent = trim($jsonContent);
if (!str_starts_with($trimmedContent, '{')) {
// Old format: file contains just database name
// Try to find the backup file by database name pattern
$dbName = $trimmedContent;
$backupDir = '/var/backups/mysql';
$pattern = $backupDir . '/' . $dbName . '_*.sql.zip';
$files = glob($pattern);
if (!empty($files)) {
// Get the most recent backup file
$latestFile = end($files);
$downloadUrl = '/custom/download.php?f=' . urlencode(basename($latestFile));
echo json_encode([
'success' => true,
'status' => 'done',
'download_url' => $downloadUrl,
'database' => $dbName,
'completed_at' => date('Y-m-d H:i:s', filemtime($latestFile)),
'file_size' => filesize($latestFile),
'note' => 'Legacy format detected'
]);
exit;
} else {
echo json_encode([
'success' => false,
'status' => 'error',
'message' => 'Done file is in old format but no backup found for database: ' . $dbName
]);
exit;
}
}
$data = json_decode($jsonContent, true);
if ($data === null) {
echo json_encode([
'success' => false,
'status' => 'error',
'message' => 'Invalid JSON in done file',
'json_error' => json_last_error_msg(),
'raw_content' => substr($jsonContent, 0, 200) // First 200 chars for debugging
]);
exit;
}
$file = $data['file'] ?? '';
if (!$file) {
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'No file path in done file']);
exit;
}
if (!file_exists($file)) {
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Backup file not found: ' . $file]);
exit;
}
// $downloadUrl = '/custom/' . urlencode(basename($file));
$downloadUrl = '/custom/download.php?f=' . urlencode(basename($file));
echo json_encode([
'success' => true,
'status' => 'done',
'download_url' => $downloadUrl,
'database' => $data['database'] ?? '',
'completed_at' => $data['completed_at'] ?? '',
'file_size' => $data['size'] ?? (file_exists($file) ? filesize($file) : 0)
]);
exit;
}
// Check if job failed
if (file_exists($fail)) {
$jsonContent = file_get_contents($fail);
$data = json_decode($jsonContent, true);
if ($data === null) {
echo json_encode([
'success' => false,
'status' => 'failed',
'message' => 'Backup failed (invalid error format)',
'raw_error' => substr($jsonContent, 0, 200)
]);
exit;
}
echo json_encode([
'success' => false,
'status' => 'failed',
'message' => $data['error'] ?? 'Unknown error',
'failed_at' => $data['failed_at'] ?? ''
]);
exit;
}
// Check if job still in queue (pending)
if (file_exists($queueFile)) {
echo json_encode(['success' => true, 'status' => 'pending']);
exit;
}
// Job not found
echo json_encode(['success' => false, 'status' => 'not_found', 'message' => 'Job not found']);
Nội dung tệp download.php:
<?php
// custom/download.php
// $backupDir = '/var/backups/mysql';
// $f = $_GET['f'] ?? '';
// $path = realpath($backupDir . '/' . $f);
// if (!$f || !$path || strpos($path, realpath($backupDir)) !== 0 || !is_file($path)) {
// http_response_code(404);
// exit('File not found');
// }
// // header('Content-Type: application/gzip');
// header('Content-Type: application/octet-stream');
// header('Content-Disposition: attachment; filename="' . basename($path) . '"');
// header('Content-Length: ' . filesize($path));
// readfile($path);
// exit;
// $backupDir = '/var/backups/mysql';
$backupDir = '/usr/local/CyberCP/public/phpmyadmin/backups';
$f = $_GET['f'] ?? '';
$path = realpath($backupDir . '/' . $f);
if (!$f || !$path || strpos($path, realpath($backupDir)) !== 0 || !is_file($path)) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo "File not found: " . htmlspecialchars($f);
exit;
}
set_time_limit(0);
header('Cache-Control: no-cache, must-revalidate');
header('Expires: 0');
header('X-Content-Type-Options: nosniff');
header('Content-Type: application/zip'); // hoặc octet-stream nếu không chắc loại
header('Content-Disposition: attachment; filename="' . basename($path) . '"');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
Tạo thư mục backups trong thư mục gốc phpmyadmin, cùng cấp với thư mục custom để lưu trữ database đã dump. Chúc bạn thành công !
