<?php
require_once '/home/www/GNU/_PAGE/head.php';
/**
* Web Directory Zipper (그누보드 라이브러리 활용 버전)
* -------------------------------------------------------------------------
* 500 에러 방지 및 실시간 백업 목록 반영 강화 버전 (Dark UI Edition + Animation)
*/
// 1. 서버 환경 설정 (대용량 압축 시 에러 방지)
@set_time_limit(0); // 실행 시간 제한 해제
@ini_set('memory_limit', '-1'); // 메모리 제한 해제
@ini_set('zlib.output_compression', 'Off');
// 2. 그누보드 핵심 엔진 로드
if (file_exists('./common.php')) {
include_once('./common.php');
} else {
if (!function_exists('sql_query')) {
function sql_query($sql) { return null; }
}
}
/**
* 디렉토리 압축 함수 (시스템 리소스 최적화 및 검증 강화)
*/
function zipDirectory($source, $destination) {
if (!class_exists('ZipArchive')) {
return "서버에 ZipArchive PHP 모듈이 설치되어 있지 않습니다.";
}
$sourcePath = realpath($source);
if (!$sourcePath || !file_exists($sourcePath)) return "대상 디렉토리가 존재하지 않습니다.";
// 대상 디렉토리 자체에 대한 읽기 권한 체크
if (!is_readable($sourcePath)) {
return "대상 디렉토리($source)를 읽을 수 없습니다. 권한(777)을 확인하세요.";
}
$zip = new ZipArchive();
$res = $zip->open($destination, ZipArchive::CREATE | ZipArchive::OVERWRITE);
if ($res !== true) {
return "압축 파일을 열 수 없습니다. (에러 코드: $res)";
}
$sourcePath = str_replace('\\', '/', $sourcePath);
$fileCount = 0;
$skippedCount = 0;
try {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourcePath, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($files as $file) {
$file = str_replace('\\', '/', $file);
$filePath = realpath($file);
// 압축 파일 자신을 압축하려 하는 경우 방지
if ($filePath === realpath($destination)) continue;
$relativePath = substr($filePath, strlen($sourcePath) + 1);
if (is_dir($file)) {
$zip->addEmptyDir($relativePath);
} else if (is_file($file)) {
if (is_readable($file)) {
$zip->addFile($filePath, $relativePath);
$fileCount++;
} else {
$skippedCount++;
}
}
}
} catch (Exception $e) {
$zip->close();
return "디렉토리 탐색 오류: " . $e->getMessage();
}
// Zip 파일 쓰기 완료 확인
if (!$zip->close()) {
return "압축 파일 저장(Close)에 실패했습니다. 디스크 공간이나 권한을 확인하세요.";
}
if ($fileCount === 0 && $skippedCount > 0) {
@unlink($destination);
return "읽을 수 있는 파일이 하나도 없습니다. ($skippedCount 개의 파일 접근 거부)";
}
// 파일이 실제로 생성되었는지 물리적 확인
if (!file_exists($destination)) {
return "파일 쓰기가 완료되었으나 물리적 경로에서 파일을 찾을 수 없습니다.";
}
// FTP에서 보일 수 있도록 권한 강제 조정
@chmod($destination, 0644);
return true;
}
$message = "";
$status = "";
$target_root = "/home/www";
$backup_root = "/data/backup";
// 3. 압축 실행 요청 처리
if (isset($_POST['action']) && $_POST['action'] == 'compress') {
$target_dir = isset($_POST['target_dir']) ? trim($_POST['target_dir']) : '';
$dest_dir = isset($_POST['dest_dir']) ? trim($_POST['dest_dir']) : $backup_root;
// 저장 경로 생성 및 권한 체크
if (!is_dir($dest_dir)) {
if (!@mkdir($dest_dir, 0707, true)) {
$status = "error";
$message = "저장 폴더를 생성할 권한이 없습니다: " . $dest_dir;
}
@chmod($dest_dir, 0707);
}
if (!$message && $target_dir && is_dir($target_dir)) {
$folder_name = basename($target_dir);
$file_name = $folder_name . "_" . date("Ymd_His") . ".zip";
$dest_path = rtrim($dest_dir, '/') . "/" . $file_name;
$result = zipDirectory($target_dir, $dest_path);
if ($result === true) {
$status = "success";
$message = "백업 성공! 파일이 생성되었습니다.<br><span class='text-[10px] font-mono opacity-70'>$dest_path</span>";
} else {
$status = "error";
$message = "백업 실패: " . $result;
}
} else if (!$message) {
$status = "error";
$message = "대상 디렉토리를 찾을 수 없습니다.";
}
}
// 4. 목록 가져오기 함수
function getDirectoryList($path) {
$dirs = [];
if (is_dir($path)) {
$items = @scandir($path);
if ($items) {
foreach ($items as $item) {
if ($item != "." && $item != ".." && is_dir($path . "/" . $item)) {
$dirs[] = $item;
}
}
}
}
return $dirs;
}
$source_dirs = getDirectoryList($target_root);
$dest_sub_dirs = getDirectoryList($backup_root);
// 5. 백업 파일 목록 검색 (하위 폴더 포함 전체 검색)
$backup_list = [];
if (is_dir($backup_root)) {
try {
$dir_iterator = new RecursiveDirectoryIterator($backup_root, RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
if ($file->isFile() && strtolower($file->getExtension()) == 'zip') {
$backup_list[] = [
'name' => $file->getFilename(),
'path' => str_replace($backup_root, '', $file->getPathname()),
'size' => round($file->getSize() / 1024 / 1024, 2) . ' MB',
'date' => date("Y-m-d H:i:s", $file->getMTime()),
'mtime' => $file->getMTime()
];
}
}
usort($backup_list, function($a, $b) { return $b['mtime'] - $a['mtime']; });
} catch (Exception $e) {}
}
?>
<title>Web Backup Tool - Dark Edition</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Pretendard:wght@400;700;900&display=swap');
body { font-family: 'Pretendard', sans-serif; background-color: #020617; margin: 0; padding: 0; color: #e2e8f0; overflow-x: hidden; }
/* 입구 애니메이션 */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fadeInUp { animation: fadeInUp 0.6s ease-out forwards; }
.Main_box { margin:50px; }
.card-blur { background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(20px); border: 1px solid rgba(51, 65, 85, 0.5); }
.custom-scrollbar::-webkit-scrollbar { width: 5px; }
.custom-scrollbar::-webkit-scrollbar-track { background: #0f172a; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #475569; }
input[readonly] { cursor: default; }
/* 버튼 상호작용 */
.source-btn, .dest-btn { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); }
.source-btn:active, .dest-btn:active { transform: scale(0.97); }
/*---------| BODY SCROLLBAR |---------*/
body::-webkit-scrollbar { width:10px; }
body::-webkit-scrollbar-thumb { background-color:#333; border:1px solid #222; }
body::-webkit-scrollbar-thumb:hover { background-color:#555; cursor:default; }
body::-webkit-scrollbar-track { background-color:#0b0e11; }
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script>
function setSourceDir(dirName, element) {
document.getElementById('target_dir_input').value = "<?php echo $target_root; ?>/" + dirName;
highlightBtn('source-btn', element);
}
function setDestDir(dirName, element) {
const path = dirName === 'root' ? "<?php echo $backup_root; ?>" : "<?php echo $backup_root; ?>/" + dirName;
document.getElementById('dest_dir_input').value = path;
highlightBtn('dest-btn', element);
}
function highlightBtn(className, element) {
document.querySelectorAll('.' + className).forEach(btn => {
btn.classList.remove('bg-indigo-950/50', 'border-indigo-500', 'ring-2', 'ring-indigo-500/20', 'text-indigo-300', 'shadow-lg', 'shadow-indigo-900/20');
});
element.classList.add('bg-indigo-950/50', 'border-indigo-500', 'ring-2', 'ring-indigo-500/20', 'text-indigo-300', 'shadow-lg', 'shadow-indigo-900/20');
}
// 실행 버튼 클릭 시 로딩 상태 표현
function startLoading() {
const btn = document.getElementById('exec-btn');
const icon = btn.querySelector('i');
const text = btn.querySelector('span');
btn.classList.add('opacity-80', 'cursor-wait');
icon.classList.remove('fa-bolt');
icon.classList.add('fa-spinner', 'fa-spin');
text.innerText = "프로세스 실행 중...";
}
</script>
<body class="items-center justify-center min-h-screen text-left">
<div class="Main_box">
<!-- 헤더 -->
<div class="flex items-center justify-between mb-10">
<div class="flex items-center space-x-5">
<div class="bg-indigo-600 p-4 rounded-[5px] text-white shadow-xl shadow-indigo-900/50">
<i class="fas fa-archive text-2xl"></i>
</div>
<div class="text-left">
<h1 class="text-2xl font-black text-white tracking-tight">Data Zipper Pro</h1>
<p class="text-[11px] text-indigo-400 font-extrabold uppercase tracking-widest text-left">Maria Volume Manager · Dark</p>
</div>
</div>
<button onclick="document.getElementById('history-panel').classList.toggle('hidden')"
class="bg-slate-800 hover:bg-slate-700 active:scale-95 text-slate-300 px-5 py-2.5 rounded-[5px] text-xs font-bold transition-all border border-slate-700 shadow-sm flex items-center">
<i class="fas fa-history mr-2 text-indigo-400"></i> 백업 내역 (<?php echo count($backup_list); ?>)
</button>
</div>
<?php if ($message): ?>
<div class="mb-8 p-6 rounded-[5px] flex items-center justify-between space-x-4 <?php echo $status == 'success' ? 'bg-emerald-950/30 text-emerald-400 border border-emerald-900/50' : 'bg-rose-950/30 text-rose-400 border border-rose-900/50'; ?> animate-fadeInUp">
<div class="flex items-center space-x-4">
<div class="<?php echo $status == 'success' ? 'bg-emerald-500' : 'bg-rose-500'; ?> p-2 rounded-[5px] text-white flex-shrink-0">
<i class="fas <?php echo $status == 'success' ? 'fa-check' : 'fa-exclamation'; ?> text-xs"></i>
</div>
<div class="text-sm font-bold text-left leading-relaxed"><?php echo $message; ?></div>
</div>
<a href="<?php echo $_SERVER['PHP_SELF']; ?>" class="bg-slate-800 hover:bg-slate-700 active:scale-95 text-slate-300 px-4 py-2 rounded-[5px] text-xs font-bold transition-all border border-slate-700 flex-shrink-0 shadow-lg">
<i class="fas fa-redo mr-1 text-emerald-400"></i> 처음으로
</a>
</div>
<?php endif; ?>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-10">
<!-- 1. 소스 선택 -->
<div class="bg-slate-900/50 rounded-[7px] p-6 border border-slate-800 text-left">
<h3 class="text-[10px] font-black text-slate-500 uppercase tracking-widest mb-4 flex items-center">
<i class="fas fa-file-export mr-2 text-indigo-500"></i> STEP 1. 소스 폴더 (/home/www)
</h3>
<div class="grid grid-cols-1 gap-2 max-h-52 overflow-y-auto custom-scrollbar pr-1">
<?php if (empty($source_dirs)): ?>
<p class="text-[11px] text-slate-600 italic py-4 text-center">폴더가 없거나 권한이 없습니다.</p>
<?php endif; ?>
<?php foreach ($source_dirs as $index => $dir): ?>
<button type="button" onclick="setSourceDir('<?php echo $dir; ?>', this)"
class="source-btn w-full flex items-center p-4 bg-slate-800/40 border border-slate-700 rounded-[5px] text-[12px] font-bold text-slate-400 hover:border-indigo-500/50 hover:bg-slate-800 transition-all text-left">
<i class="fas fa-folder text-amber-500/80 mr-3"></i> <?php echo $dir; ?>
</button>
<?php endforeach; ?>
</div>
</div>
<!-- 2. 저장 위치 선택 -->
<div class="bg-slate-900/50 rounded-[7px] p-6 border border-slate-800 text-left">
<h3 class="text-[10px] font-black text-slate-500 uppercase tracking-widest mb-4 flex items-center">
<i class="fas fa-file-import mr-2 text-emerald-500"></i> STEP 2. 저장 위치 (/data/backup)
</h3>
<div class="grid grid-cols-1 gap-2 max-h-52 overflow-y-auto custom-scrollbar pr-1">
<button type="button" onclick="setDestDir('root', this)"
class="dest-btn w-full flex items-center p-4 bg-slate-800/40 border border-slate-700 rounded-[5px] text-[12px] font-bold text-slate-400 hover:border-emerald-500/50 hover:bg-slate-800 transition-all text-left">
<i class="fas fa-hdd text-slate-500 mr-3"></i> [기본] /data/backup
</button>
<?php foreach ($dest_sub_dirs as $dir): ?>
<button type="button" onclick="setDestDir('<?php echo $dir; ?>', this)"
class="dest-btn w-full flex items-center p-4 bg-slate-800/40 border border-slate-700 rounded-[5px] text-[12px] font-bold text-slate-400 hover:border-emerald-500/50 hover:bg-slate-800 transition-all text-left">
<i class="fas fa-folder text-emerald-500/70 mr-3"></i> <?php echo $dir; ?>
</button>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 백업 내역 패널 -->
<div id="history-panel" class="hidden mb-10 bg-black/40 rounded-[7px] border border-slate-800 overflow-hidden shadow-2xl text-left">
<div class="px-6 py-4 bg-slate-900 border-b border-slate-800 flex justify-between items-center text-left">
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest text-left">Recent Backups</span>
<button onclick="location.reload()" class="text-indigo-400 hover:rotate-180 transition-transform duration-500"><i class="fas fa-sync text-xs"></i></button>
</div>
<div class="max-h-72 overflow-y-auto custom-scrollbar">
<?php if (empty($backup_list)): ?>
<div class="p-12 text-center text-slate-600 text-xs italic">백업 내역이 없습니다.</div>
<?php else: ?>
<div class="divide-y divide-slate-800/50">
<?php foreach ($backup_list as $file): ?>
<div class="px-6 py-4 flex justify-between items-center hover:bg-slate-800/30 transition-all text-left group">
<div class="flex flex-col text-left overflow-hidden">
<span class="text-[12px] font-bold text-slate-300 truncate text-left group-hover:text-indigo-400 transition-colors" title="<?php echo $file['path']; ?>"><?php echo $file['name']; ?></span>
<span class="text-[9px] text-slate-500 text-left mt-0.5"><?php echo $file['date']; ?> <span class="ml-2 text-slate-700 text-left"><?php echo $file['path']; ?></span></span>
</div>
<span class="text-[11px] font-black text-indigo-400 bg-indigo-500/10 px-3 py-1 rounded-[5px] ml-4 flex-shrink-0"><?php echo $file['size']; ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- 실행 폼 -->
<form method="POST" class="space-y-5" onsubmit="startLoading()">
<input type="hidden" name="action" value="compress">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="text-left">
<label class="block text-[11px] font-black text-slate-500 uppercase ml-3 mb-2 tracking-tighter text-left">Source Folder (압축 대상)</label>
<input type="text" name="target_dir" id="target_dir_input" readonly
class="w-full px-5 py-4 rounded-[5px] border border-slate-800 bg-slate-900 text-[13px] font-bold text-slate-400 outline-none focus:border-indigo-500 transition-all shadow-inner text-left" placeholder="좌측 STEP 1에서 선택">
</div>
<div class="text-left">
<label class="block text-[11px] font-black text-slate-500 uppercase ml-3 mb-2 tracking-tighter text-left">Destination (저장 위치)</label>
<input type="text" name="dest_dir" id="dest_dir_input" readonly
class="w-full px-5 py-4 rounded-[5px] border border-slate-800 bg-slate-900 text-[13px] font-bold text-slate-400 outline-none focus:border-emerald-500 transition-all shadow-inner text-left" placeholder="우측 STEP 2에서 위치 선택">
</div>
</div>
<button type="submit" id="exec-btn"
class="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-black py-7 rounded-[5px] shadow-xl shadow-indigo-900/20 transition-all flex items-center justify-center space-x-4 group mt-6 border-b-4 border-indigo-800 active:border-b-0 active:translate-y-1">
<i class="fas fa-bolt text-yellow-300 group-hover:scale-125 transition-transform duration-300"></i>
<span class="text-xl tracking-tight uppercase">Start Backup Process</span>
</button>
</form>
<!-- 하단 정보 -->
<div class="mt-12 pt-8 border-t border-slate-800 grid grid-cols-3 gap-4">
<div class="bg-slate-900/30 p-4 rounded-[5px] text-center border border-slate-800/50">
<p class="text-[9px] font-black text-slate-600 uppercase mb-1.5 tracking-tighter text-center">Volume</p>
<span class="text-[11px] font-bold text-slate-400 text-center">196GB (vdb)</span>
</div>
<div class="bg-slate-900/30 p-4 rounded-[5px] text-center border border-slate-800/50">
<p class="text-[9px] font-black text-slate-600 uppercase mb-1.5 tracking-tighter text-center text-center">Source</p>
<span class="text-[11px] font-bold text-slate-400 text-center text-center">/home/www</span>
</div>
<div class="bg-slate-900/30 p-4 rounded-[5px] text-center border border-slate-800/50 text-center">
<p class="text-[9px] font-black text-slate-600 uppercase mb-1.5 tracking-tighter text-center text-center">Backup Area</p>
<span class="text-[11px] font-bold text-slate-400 italic text-center text-center">/data/backup</span>
</div>
</div>
</div>
</body>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>