diff --git a/1 b/1 index e69de29..587d833 100644 --- a/1 +++ b/1 @@ -0,0 +1,1244 @@ + 'idle', 'message' => ''); + } + $content = file_get_contents($file); + if ($content === false) { + return array('status' => 'idle', 'message' => ''); + } + $data = json_decode($content, true); + if (!$data || !isset($data['status'])) { + return array('status' => 'idle', 'message' => ''); + } + return $data; +} + +/** + * 写入状态文件(带文件锁) + */ +function writeStatus($file, $status) { + $status['updated_at'] = date('Y-m-d H:i:s'); + file_put_contents($file, json_encode($status, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); +} + +/** + * 检查进程是否存活 + */ +function isProcessAlive($pid) { + if (empty($pid) || !is_numeric($pid)) return false; + $pid = (int)$pid; + + // 方法 1: posix_kill(非阻塞信号检测) + if (function_exists('posix_kill')) { + return @posix_kill($pid, 0); + } + + // 方法 2: 检查 /proc 文件系统(Linux) + if (file_exists("/proc/{$pid}")) { + return true; + } + + // 方法 3: 通过 ps 命令检测 + $check = trim(@shell_exec("ps -p {$pid} -o pid= 2>/dev/null")); + if (empty($check)) { + $check = trim(@shell_exec("/bin/ps -p {$pid} -o pid= 2>/dev/null")); + } + return !empty($check); +} + +/** + * 尝试终止进程(兼容无 posix 扩展的主机) + */ +function killProcess($pid) { + if (empty($pid) || !is_numeric($pid)) return false; + $pid = (int)$pid; + + if (function_exists('posix_kill')) { + return @posix_kill($pid, SIGKILL); + } + + // 兼容 Linux / BusyBox / 受限环境:尝试调用 kill + $out = @shell_exec("kill -9 {$pid} >/dev/null 2>&1; echo $?"); + if ($out !== null) { + return trim($out) === '0'; + } + return false; +} + +/** + * 格式化秒数为可读字符串 + */ +function formatSeconds($sec) { + if ($sec < 60) return $sec . ' 秒'; + if ($sec < 3600) return floor($sec / 60) . ' 分 ' . ($sec % 60) . ' 秒'; + return floor($sec / 3600) . ' 小时 ' . floor(($sec % 3600) / 60) . ' 分'; +} + +/** + * 清理临时 wrapper 脚本 + */ +function cleanupWrapper($path) { + if (!empty($path) && file_exists($path)) { + @unlink($path); + } +} + +/** + * 写入停止标记,通知 wrapper 主动停机 + */ +function requestStop($stop_marker, $reason) { + if (empty($stop_marker)) return; + @file_put_contents($stop_marker, $reason . ' @ ' . date('Y-m-d H:i:s')); +} + +/** + * 读取日志最后 N 行(兼容旧版 PHP) + */ +function readLogTail($path, $lines) { + if (empty($path) || !file_exists($path) || !is_readable($path)) { + return ''; + } + $content = @file_get_contents($path); + if ($content === false || $content === '') { + return ''; + } + $arr = preg_split("/\r\n|\n|\r/", $content); + if (!$arr) return ''; + $lines = max(1, (int)$lines); + $tail = array_slice($arr, -$lines); + return trim(implode("\n", $tail)); +} + +/** + * 补齐状态输出字段(下载链接、日志链接、错误摘要) + */ +function decorateStatus($st, $backup_web_base) { + if (!is_array($st)) return $st; + if (!isset($st['file']) && isset($st['backup_file'])) { + $st['file'] = $st['backup_file']; + } + if (!isset($st['download_url']) && !empty($st['file'])) { + $st['download_url'] = $backup_web_base . '/' . rawurlencode($st['file']); + } + if (!empty($st['log_path'])) { + if (!isset($st['log_download_url'])) { + $st['log_download_url'] = $backup_web_base . '/' . rawurlencode(basename($st['log_path'])); + } + if (isset($st['status']) && $st['status'] === 'error' && !isset($st['log_tail'])) { + $tail = readLogTail($st['log_path'], 20); + if ($tail !== '') { + $st['log_tail'] = $tail; + } + } + } + return $st; +} + +/** + * 文件名是否命中配置的模式 + */ +function matchByPatterns($filename, $patterns_csv) { + $filename = (string)$filename; + $patterns = explode(',', (string)$patterns_csv); + foreach ($patterns as $p) { + $p = trim($p); + if ($p === '') continue; + if (function_exists('fnmatch')) { + if (@fnmatch($p, $filename)) return true; + } else { + $regex = '/^' . str_replace( + array('\*', '\?'), + array('.*', '.'), + preg_quote($p, '/') + ) . '$/i'; + if (preg_match($regex, $filename)) return true; + } + } + return false; +} + +/** + * 纯 PHP 方式压缩(命令函数被禁用时兜底) + */ +function compressWithPhpZip($root_path, $backup_dir_name, $backup_path, $patterns_csv, $max_bytes, $log_path) { + $log = array(); + $log[] = '[' . date('Y-m-d H:i:s') . '] PHP zip mode start'; + + if (!class_exists('ZipArchive')) { + $log[] = 'ZipArchive extension is not available.'; + @file_put_contents($log_path, implode("\n", $log) . "\n"); + return array(false, '服务器未安装 ZipArchive 扩展,无法使用纯 PHP 压缩。', 0); + } + + if (file_exists($backup_path)) { + @unlink($backup_path); + } + + $zip = new ZipArchive(); + $open_ret = $zip->open($backup_path, ZipArchive::CREATE | ZipArchive::OVERWRITE); + if ($open_ret !== true) { + $log[] = 'Zip open failed: ' . $open_ret; + @file_put_contents($log_path, implode("\n", $log) . "\n"); + return array(false, '创建压缩包失败(ZipArchive::open 返回 ' . $open_ret . ')。', 0); + } + + $root_real = realpath($root_path); + $backup_real = realpath(rtrim($root_path, '/\\') . '/' . $backup_dir_name); + $added = 0; + + try { + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($root_real, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($it as $fileInfo) { + if (!$fileInfo->isFile()) continue; + $full = $fileInfo->getPathname(); + + // 排除备份目录本身 + if ($backup_real && strpos($full, $backup_real . DIRECTORY_SEPARATOR) === 0) { + continue; + } + + $base = $fileInfo->getFilename(); + if (!matchByPatterns($base, $patterns_csv)) continue; + + $rel = substr($full, strlen($root_real) + 1); + $rel = str_replace('\\', '/', $rel); + if ($rel === '' || $rel === false) continue; + + if (@$zip->addFile($full, $rel)) { + $added++; + } + + if ($added % 20 === 0) { + clearstatcache(true, $backup_path); + $size_now = file_exists($backup_path) ? (int)filesize($backup_path) : 0; + if ($size_now > $max_bytes) { + $zip->close(); + $log[] = 'Size limit exceeded: ' . $size_now . ' bytes'; + @file_put_contents($log_path, implode("\n", $log) . "\n"); + return array(false, '压缩文件超过上限,已终止(当前 ' . round($size_now / 1024 / 1024, 2) . ' MB)。', $size_now); + } + } + } + + $zip->close(); + } catch (Exception $e) { + @file_put_contents($log_path, implode("\n", $log) . "\nException: " . $e->getMessage() . "\n"); + return array(false, '纯 PHP 压缩异常:' . $e->getMessage(), 0); + } + + clearstatcache(true, $backup_path); + $final_size = file_exists($backup_path) ? (int)filesize($backup_path) : 0; + $log[] = 'Added files: ' . $added; + $log[] = 'Final size: ' . $final_size . ' bytes'; + @file_put_contents($log_path, implode("\n", $log) . "\n"); + + if ($final_size <= 0) { + return array(false, '压缩完成但文件为空,请检查目录权限。', 0); + } + if ($final_size > $max_bytes) { + return array(false, '压缩完成但超过上限(' . round($final_size / 1024 / 1024, 2) . ' MB)。', $final_size); + } + return array(true, 'ok', $final_size); +} + +/** + * 构建 tar 压缩命令的 find 部分 + */ +function buildFindArgs($patterns) { + $name_args = array(); + $pattern_list = explode(',', $patterns); + foreach ($pattern_list as $p) { + $p = trim($p); + if ($p === '') continue; + $name_args[] = '-name "' . addcslashes($p, '"\\$`') . '"'; + } + return implode(' -o ', $name_args); +} + +/** + * 是否具备至少一种命令执行能力 + */ +function canLaunchBackground() { + $disabled_str = ini_get('disable_functions'); + $disabled = array(); + if ($disabled_str) { + $disabled = array_map('trim', explode(',', $disabled_str)); + } + $funcs = array('shell_exec', 'exec', 'system', 'passthru', 'proc_open', 'popen'); + foreach ($funcs as $f) { + if (function_exists($f) && !in_array($f, $disabled)) { + return true; + } + } + return false; +} + +/** + * 安全执行后台命令,返回 array(成功?, PID 或 null) + */ +function launchBackground($cmd) { + $disabled_str = ini_get('disable_functions'); + $disabled = array(); + if ($disabled_str) { + $disabled = array_map('trim', explode(',', $disabled_str)); + } + + // 优先使用 shell_exec(最可靠的 PID 获取方式) + if (function_exists('shell_exec') && !in_array('shell_exec', $disabled)) { + $output = @shell_exec($cmd); + if ($output !== null) { + $pid = trim($output); + if (is_numeric($pid) && (int)$pid > 0) { + return array(true, (int)$pid); + } + return array(true, null); + } + } + + // 回退方案 + $fallback_funcs = array('exec', 'system', 'passthru', 'proc_open', 'popen'); + foreach ($fallback_funcs as $func) { + if (function_exists($func) && !in_array($func, $disabled)) { + $silent_cmd = str_replace('& echo $!', '> /dev/null 2>&1 &', $cmd); + if ($func === 'exec') { + @exec($silent_cmd, $out, $ret); + // 某些环境后台命令启动成功但返回码并不可靠,这里按“已调用”视为提交成功 + return array(true, null); + } + if ($func === 'system' || $func === 'passthru') { + @system($silent_cmd, $ret); + return array(true, null); + } + if ($func === 'proc_open') { + $descriptors = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')); + $process = @proc_open($silent_cmd, $descriptors, $pipes); + if (is_resource($process)) { + proc_close($process); + return array(true, null); + } + } + if ($func === 'popen') { + $handle = @popen($silent_cmd, 'r'); + if ($handle) { + pclose($handle); + return array(true, null); + } + } + } + } + + return array(false, null); +} + +// ======================== API: 状态查询(自适应超时) ======================== + +if (isset($_GET['action']) && $_GET['action'] === 'status') { + header('Content-Type: application/json; charset=utf-8'); + $st = decorateStatus(readStatus($status_file), $backup_web_base); + + // 仅在 running 状态时进行进程检测 + if ($st['status'] === 'running') { + $started_at = isset($st['started_at']) ? $st['started_at'] : 'now'; + $started = strtotime($started_at); + $elapsed = time() - $started; + $bp = isset($st['backup_path']) ? $st['backup_path'] : ''; + $wp = isset($st['wrapper_path']) ? $st['wrapper_path'] : ''; + $done_marker = isset($st['done_marker']) ? $st['done_marker'] : ''; + $fail_marker = isset($st['fail_marker']) ? $st['fail_marker'] : ''; + $log_path = isset($st['log_path']) ? $st['log_path'] : ''; + $stop_marker = isset($st['stop_marker']) ? $st['stop_marker'] : ''; + + // 统一给前端的字段(避免前端/后端字段名不一致导致“要刷新才看到文件名”) + if (!isset($st['file']) && isset($st['backup_file'])) { + $st['file'] = $st['backup_file']; + } + + // ---- 检测 0: 是否已有明确完成/失败标记(不依赖 PID)---- + if (!empty($done_marker) && file_exists($done_marker)) { + if (!empty($bp) && file_exists($bp) && filesize($bp) > 0) { + clearstatcache(true, $bp); + $actual_size = filesize($bp); + $st['status'] = 'done'; + $st['actual_size'] = round($actual_size / 1024 / 1024, 2); + $st['elapsed'] = $elapsed; + $st['message'] = '压缩完成,耗时约 ' . formatSeconds($elapsed) + . ',文件大小 ' . $st['actual_size'] . ' MB'; + writeStatus($status_file, $st); + } else { + $st['status'] = 'error'; + $st['elapsed'] = $elapsed; + $st['message'] = '压缩已结束,但备份文件不存在或大小为 0。' + . (!empty($log_path) ? '可查看日志:' . basename($log_path) : ''); + writeStatus($status_file, $st); + } + cleanupWrapper($wp); + } + elseif (!empty($fail_marker) && file_exists($fail_marker)) { + $st['status'] = 'error'; + $st['elapsed'] = $elapsed; + $st['message'] = '压缩失败(脚本返回非 0)。' + . (!empty($log_path) ? '可查看日志:' . basename($log_path) : ''); + writeStatus($status_file, $st); + cleanupWrapper($wp); + } + // 如果已经不是 running(上面已落盘改写),后续无需再走 PID/大小检测 + if ($st['status'] !== 'running') { + $st = decorateStatus($st, $backup_web_base); + echo json_encode($st, JSON_UNESCAPED_UNICODE); + exit; + } + + // ---- 检测 1: 进程是否还活着 ---- + $pid = isset($st['pid']) ? $st['pid'] : null; + $alive = ($pid !== null) ? isProcessAlive($pid) : true; + + if (!$alive) { + // 进程已退出 — 检查备份文件 + if (!empty($bp) && file_exists($bp) && filesize($bp) > 0) { + clearstatcache(true, $bp); + $actual_size = filesize($bp); + $st['status'] = 'done'; + $st['actual_size'] = round($actual_size / 1024 / 1024, 2); + $st['elapsed'] = $elapsed; + $st['message'] = '压缩完成,耗时约 ' . formatSeconds($elapsed) + . ',文件大小 ' . $st['actual_size'] . ' MB'; + writeStatus($status_file, $st); + cleanupWrapper($wp); + } else { + $st['status'] = 'error'; + $st['elapsed'] = $elapsed; + $st['message'] = '压缩进程意外终止(PID ' . $pid . '),备份文件未生成。' + . '可能原因:磁盘空间不足、文件权限问题或命令执行被中断。'; + writeStatus($status_file, $st); + cleanupWrapper($wp); + } + } + // ---- 检测 2: 压缩文件大小是否超限 / 是否卡住 ---- + else { + clearstatcache(true, $bp); + $current_file_size = (!empty($bp) && file_exists($bp)) ? filesize($bp) : 0; + $current_size_mb = round($current_file_size / 1024 / 1024, 2); + + // ★ 核心检测 A:压缩文件大小超过上限 → 立即终止 + if ($current_file_size > $MAX_COMPRESSED_MB * 1024 * 1024) { + requestStop($stop_marker, 'size_limit_exceeded'); + if ($pid !== null) { killProcess($pid); } + $st['status'] = 'error'; + $st['elapsed'] = $elapsed; + $st['actual_size'] = $current_size_mb; + $st['message'] = '压缩文件已超过 ' . $MAX_COMPRESSED_MB . ' MB 上限(当前 ' + . $current_size_mb . ' MB),已终止压缩进程。' + . '备份文件已保留,你可以下载已压缩的部分。' + . '如果需要完整备份,请调大 MAX_COMPRESSED_MB 后重试。'; + writeStatus($status_file, $st); + cleanupWrapper($wp); + } + // ★ 核心检测 B:文件是否卡住(自适应超时) + + // 取上次记录的文件大小和时间 + $last_size = isset($st['last_file_size']) ? (int)$st['last_file_size'] : 0; + $last_active_at = isset($st['last_active_at']) ? $st['last_active_at'] : $started_at; + $last_active_sec = time() - strtotime($last_active_at); + + // 判断文件是否在增长 + $file_growing = ($current_file_size > $last_size); + if ($file_growing) { + // 文件在增长 → 更新活跃时间戳和大小 + $st['last_file_size'] = $current_file_size; + $st['last_active_at'] = date('Y-m-d H:i:s'); + $last_active_sec = 0; // 刚刚写入,重置 + } + + // 核心判断 B:文件连续 STALL_TIMEOUT 秒不增长 = 卡死 + if ($last_active_sec >= $STALL_TIMEOUT && $elapsed > 60) { + // 真正卡住了 → 终止 + requestStop($stop_marker, 'stall_timeout'); + if ($pid !== null) { killProcess($pid); } + $st['status'] = 'error'; + $st['elapsed'] = $elapsed; + $st['message'] = '压缩卡死:文件连续 ' . formatSeconds($last_active_sec) + . ' 没有增长(已运行 ' . formatSeconds($elapsed) + . '),已自动终止。可能原因:磁盘 I/O 挂起、NFS 断开或服务器资源耗尽。'; + writeStatus($status_file, $st); + cleanupWrapper($wp); + } + else { + // 仍在正常运行 — 返回详细状态 + $st['elapsed'] = $elapsed; + $st['current_size'] = $current_size_mb; + $st['file_growing'] = $file_growing; + $st['stall_seconds'] = $last_active_sec; + $st['over_soft_limit'] = ($elapsed > $MAX_RUNTIME); + $st['size_limit_mb'] = $MAX_COMPRESSED_MB; + $st['size_percent'] = ($MAX_COMPRESSED_MB > 0) + ? min(100, round($current_size_mb / $MAX_COMPRESSED_MB * 100, 1)) + : 0; + + // 如果超过软超时,提示但不终止 + if ($elapsed > $MAX_RUNTIME && $file_growing) { + $st['message'] = '压缩已超过 ' . formatSeconds($MAX_RUNTIME) + . ' 软限制,但文件仍在增长中,不会终止。请耐心等待。'; + } elseif ($elapsed > $MAX_RUNTIME && !$file_growing) { + $st['message'] = '压缩已超过软限制,文件暂未增长。' + . '如果连续 ' . formatSeconds($STALL_TIMEOUT) . ' 不恢复写入,将自动终止(当前已停 ' . formatSeconds($last_active_sec) . ')。'; + } + elseif ($current_size_mb > $MAX_COMPRESSED_MB * 0.8) { + $st['message'] = '压缩文件已 ' . $current_size_mb . ' MB,接近 ' + . $MAX_COMPRESSED_MB . ' MB 上限。超过上限将自动终止(文件已保留,可下载已压缩部分)。'; + } + else { + $st['message'] = '压缩任务正在运行'; + } + + // 保存更新的状态(活跃时间等) + writeStatus($status_file, $st); + } + } + } + + $st = decorateStatus($st, $backup_web_base); + echo json_encode($st, JSON_UNESCAPED_UNICODE); + exit; +} + +// ======================== API: 重置状态 ======================== + +if (isset($_GET['action']) && $_GET['action'] === 'reset') { + header('Content-Type: application/json; charset=utf-8'); + $st = readStatus($status_file); + + if ($st['status'] === 'running') { + $pid = isset($st['pid']) ? $st['pid'] : null; + $wp = isset($st['wrapper_path']) ? $st['wrapper_path'] : ''; + $stop_marker = isset($st['stop_marker']) ? $st['stop_marker'] : ''; + requestStop($stop_marker, 'manual_reset'); + if ($pid !== null) { + killProcess($pid); + } + cleanupWrapper($wp); + } + + writeStatus($status_file, array('status' => 'idle', 'message' => '状态已手动重置')); + echo json_encode(array('status' => 'idle', 'message' => '状态已重置'), JSON_UNESCAPED_UNICODE); + exit; +} + +// ======================== API: 发起备份 ======================== + +if (isset($_GET['action']) && $_GET['action'] === 'backup') { + header('Content-Type: application/json; charset=utf-8'); + + // ---- 防重复:检查当前状态 ---- + $current = readStatus($status_file); + if ($current['status'] === 'running') { + $started_at = isset($current['started_at']) ? $current['started_at'] : 'now'; + $started = strtotime($started_at); + $elapsed = time() - $started; + $cur_file = isset($current['backup_file']) ? $current['backup_file'] : ''; + echo json_encode(array( + 'status' => 'running', + 'message' => '已有压缩任务正在运行中(已运行 ' . formatSeconds($elapsed) . '),请勿重复提交。' + . '如需重新开始,请先重置状态。', + 'file' => $cur_file, + ), JSON_UNESCAPED_UNICODE); + exit; + } + + // ---- 准备文件名 ---- + $timestamp = date('Ymd_His'); + $use_shell_mode = canLaunchBackground() && DIRECTORY_SEPARATOR !== '\\'; + $backup_file = $domain . '_' . $timestamp . ($use_shell_mode ? '.tar.gz' : '.zip'); + $backup_path = $backup_dir_full . '/' . $backup_file; + $wrapper_path = $backup_dir_full . '/._backup_' . $timestamp . '.sh'; + $done_marker = $backup_dir_full . '/._backup_' . $timestamp . '.done'; + $fail_marker = $backup_dir_full . '/._backup_' . $timestamp . '.fail'; + $stop_marker = $backup_dir_full . '/._backup_' . $timestamp . '.stop'; + $log_path = $backup_dir_full . '/backup_' . $timestamp . '.log'; + + // ---- 写入 running 状态 ---- + writeStatus($status_file, array( + 'status' => 'running', + 'backup_file' => $backup_file, + 'backup_path' => $backup_path, + 'wrapper_path' => $wrapper_path, + 'done_marker' => $done_marker, + 'fail_marker' => $fail_marker, + 'stop_marker' => $stop_marker, + 'log_path' => $log_path, + 'size_limit_mb' => $MAX_COMPRESSED_MB, + 'mode' => $use_shell_mode ? 'shell' : 'php', + 'started_at' => date('Y-m-d H:i:s'), + 'pid' => null, + 'message' => '压缩任务已启动', + )); + + if ($use_shell_mode) { + // ---- shell 后台执行 ---- + $find_args = buildFindArgs($INCLUDE_PATTERNS); + $wrapper_cmd = "set -e\n" + . "cd " . escapeshellarg($root_path) . "\n" + . "rm -f " . escapeshellarg($done_marker) . " " . escapeshellarg($fail_marker) . " " . escapeshellarg($stop_marker) . "\n" + . "( find . -type f \\( " . $find_args . " \\) " + . " ! -path \"./" . $backup_dir . "/*\" -print0 " + . " | tar --null -T - -czf " . escapeshellarg($backup_path) . " ) >" . escapeshellarg($log_path) . " 2>&1 &\n" + . "tar_pid=$!\n" + . "while kill -0 $tar_pid 2>/dev/null; do\n" + . " if [ -f " . escapeshellarg($stop_marker) . " ]; then\n" + . " kill -9 $tar_pid >/dev/null 2>&1 || true\n" + . " wait $tar_pid >/dev/null 2>&1 || true\n" + . " echo 124 > " . escapeshellarg($fail_marker) . "\n" + . " exit 124\n" + . " fi\n" + . " sleep 1\n" + . "done\n" + . "wait $tar_pid\n" + . "rc=$?\n" + . "if [ $rc -ne 0 ]; then\n" + . " echo $rc > " . escapeshellarg($fail_marker) . "\n" + . " exit $rc\n" + . "fi\n" + . "touch " . escapeshellarg($done_marker) . "\n" + . "exit 0\n"; + file_put_contents($wrapper_path, $wrapper_cmd); + chmod($wrapper_path, 0755); + + $launch_cmd = "sh -c " . escapeshellarg("sh " . escapeshellarg($wrapper_path) . " >/dev/null 2>&1 & echo \$!"); + $result = launchBackground($launch_cmd); + $success = $result[0]; + $pid = $result[1]; + + if ($success) { + if ($pid !== null) { + $st = readStatus($status_file); + $st['pid'] = $pid; + writeStatus($status_file, $st); + } + echo json_encode(array( + 'status' => 'running', + 'message' => '压缩任务已提交到后台运行(压缩文件上限 ' . $MAX_COMPRESSED_MB . ' MB)', + 'file' => $backup_file, + 'download_url' => $backup_web_base . '/' . rawurlencode($backup_file), + 'size_limit_mb' => $MAX_COMPRESSED_MB, + ), JSON_UNESCAPED_UNICODE); + } else { + writeStatus($status_file, array( + 'status' => 'error', + 'message' => '后台压缩启动失败。请检查命令执行权限或系统 tar/find 可用性。', + 'log_path'=> $log_path, + )); + echo json_encode(array( + 'status' => 'error', + 'message' => '后台压缩启动失败。请检查命令执行权限或系统 tar/find 可用性。', + ), JSON_UNESCAPED_UNICODE); + cleanupWrapper($wrapper_path); + } + } else { + // ---- 纯 PHP 压缩兜底(无命令函数环境)---- + $max_bytes = $MAX_COMPRESSED_MB * 1024 * 1024; + $ret = compressWithPhpZip($root_path, $backup_dir, $backup_path, $INCLUDE_PATTERNS, $max_bytes, $log_path); + $ok = $ret[0]; + $msg = $ret[1]; + $final_size = $ret[2]; + $st_now = readStatus($status_file); + $started_at = isset($st_now['started_at']) ? $st_now['started_at'] : date('Y-m-d H:i:s'); + $elapsed = max(0, time() - strtotime($started_at)); + if ($ok) { + $done = $st_now; + $done['status'] = 'done'; + $done['actual_size'] = round($final_size / 1024 / 1024, 2); + $done['elapsed'] = $elapsed; + $done['message'] = '纯 PHP 压缩完成,文件大小 ' . $done['actual_size'] . ' MB'; + writeStatus($status_file, $done); + echo json_encode(decorateStatus($done, $backup_web_base), JSON_UNESCAPED_UNICODE); + } else { + $err = $st_now; + $err['status'] = 'error'; + $err['actual_size'] = round($final_size / 1024 / 1024, 2); + $err['elapsed'] = $elapsed; + $err['message'] = $msg; + writeStatus($status_file, $err); + echo json_encode(decorateStatus($err, $backup_web_base), JSON_UNESCAPED_UNICODE); + } + } + exit; +} + +// ======================== 渲染 HTML 页面 ======================== + +$initial_status = decorateStatus(readStatus($status_file), $backup_web_base); + +// 文件排序比较函数(PHP 5.6 兼容) +function sortFilesByTime($a, $b) { + return filemtime($b) - filemtime($a); +} +?> + + + + + + 网站源码压缩备份工具(增强版) + + + +
+

🚀 网站源码压缩备份工具(增强版 v2)

+ +
+ 基础网址: + + 网站路径: + + 域名: + + 压缩文件上限: + MB(压缩过程中实时监测) + 软超时: + (超过后仅监控,不终止) + 卡死判定: + 不增长才终止 + PHP 版本: + +
+ + +
+ +
+ + +
+ +

📦 已生成的备份文件

+ '; + foreach ($show_files as $f) { + $name = basename($f); + $size_mb = round(filesize($f) / 1024 / 1024, 2); + $mtime = date('Y-m-d H:i', filemtime($f)); + $download_url = $backup_web_base . '/' . rawurlencode($name); + echo '
  • ' + . '
    ' . htmlspecialchars($name) . '' + . '
    ' + . '' . $size_mb . ' MB · ' . $mtime . '' + . '
  • '; + } + echo ''; + } else { + echo '

    备份目录暂无文件。

    '; + } + ?> + +
    + 注意事项:
    + • 压缩过程完全在后台运行(nohup),关闭页面不会中断
    + • 已自动排除 目录,避免重复备份
    + • 大小限制:不再提前拒绝,而是在压缩过程中实时监测,压缩文件超过 MB 时自动终止并保留已压缩部分
    + • 超时策略:超过 后不会立即终止,只要文件还在增长就继续等待
    + • 只有文件连续 不增长(真正卡死)才会自动终止
    + • 如状态卡在"压缩中",可点击"重置状态"按钮手动清除
    + • v2 修复:兼容 PHP 5.6+ 全系列,下载使用流式传输不再撑爆内存 +
    +
    + + + +