<?php
/**
* Data Directory Explorer (PHP Version) - Multi Volume Support with Batch Delete Feature
*/
// 1. 볼륨 설정 (기존 코드 유지)
$volumes = [
'maria' => [
'name' => '마리아 (Maria Volume)',
'path' => '/data/backup'
],
'oldboy' => [
'name' => '올드보이 (Oldboy Volume)',
'path' => '/home/www/DATA/UPBIT/log'
]
];
$current_vol_id = isset($_GET['vol']) && isset($volumes[$_GET['vol']]) ? $_GET['vol'] : 'maria';
$root_path = $volumes[$current_vol_id]['path'];
$current_sub_dir = isset($_GET['dir']) ? $_GET['dir'] : '';
// 2. 경로 계산 및 보안 검사 (기존 코드 유지)
$target_dir = $root_path . ($current_sub_dir ? DIRECTORY_SEPARATOR . $current_sub_dir : '');
$real_target = realpath($target_dir);
$real_root = realpath($root_path);
if ($real_target === false || ($real_root !== false && strpos($real_target, $real_root) !== 0)) {
$target_dir = $real_root ?: $root_path;
$current_sub_dir = '';
} else {
$target_dir = $real_target;
}
// [기존 코드 유지] 디렉토리 용량 계산 함수
function getDirSize($path) {
$size = 0;
try {
if (is_dir($path)) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)) as $file) {
$size += $file->getSize();
}
}
} catch (Exception $e) { $size = 0; }
return $size;
}
// 3. 파일 삭제 로직 처리 (기존 코드 유지)
$messages = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'delete') {
$files_to_delete = [$_POST['file_name']];
} elseif ($_POST['action'] === 'delete_batch' && isset($_POST['selected_files'])) {
$files_to_delete = $_POST['selected_files'];
} else {
$files_to_delete = [];
}
$deleted_count = 0;
foreach ($files_to_delete as $file_name) {
$delete_path = $target_dir . DIRECTORY_SEPARATOR . $file_name;
$real_delete_path = realpath($delete_path);
if ($real_delete_path && is_file($real_delete_path) && strpos($real_delete_path, $real_root) === 0) {
if (unlink($real_delete_path)) {
$deleted_count++;
}
}
}
if ($deleted_count > 0) {
$messages[] = "성공적으로 " . $deleted_count . "개의 파일을 삭제했습니다.";
} elseif (!empty($files_to_delete)) {
$messages[] = "파일 삭제에 실패했거나 권한이 없습니다.";
}
}
// 4. 파일 목록 읽기 및 사이즈 계산 (기존 구조 유지)
$result = [];
$total_dir_bytes = 0;
if (is_dir($target_dir)) {
$files = array_diff(scandir($target_dir), ['.', '..']);
foreach ($files as $file) {
$path = $target_dir . DIRECTORY_SEPARATOR . $file;
$rel = ltrim(substr($path, strlen($real_root)), DIRECTORY_SEPARATOR);
if (file_exists($path)) {
$is_dir = is_dir($path);
$bytes = $is_dir ? getDirSize($path) : @filesize($path);
$total_dir_bytes += $bytes;
$result[] = [
'name' => $file,
'path' => $rel,
'is_dir' => $is_dir,
'size' => formatSize($bytes),
'ctime' => date("Y-m-d H:i:s", filectime($path)),
'mtime' => date("Y-m-d H:i:s", filemtime($path)),
];
}
}
usort($result, function($a, $b) {
if ($a['is_dir'] != $b['is_dir']) return $b['is_dir'] <=> $a['is_dir'];
return strcasecmp($a['name'], $b['name']);
});
}
function formatSize($bytes) {
if (!$bytes || $bytes <= 0) return "0 B";
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = floor(log($bytes, 1024));
return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
}
require_once '/home/www/GNU/_PAGE/head.php';
?>
<title>Explorer - <?php echo $volumes[$current_vol_id]['name']; ?></title>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap');
/* 기본 설정 */
:root {
--bg-deep: #020617;
--bg-side: #0f172a;
--slate-200: #e2e8f0;
--slate-400: #94a3b8;
--slate-500: #64748b;
--slate-800: rgba(30, 41, 59, 0.5);
--indigo-400: #818cf8;
--indigo-500: #6366f1;
--amber-500: #f59e0b;
--red-400: #f87171;
--emerald-400: #34d399;
}
body { font-family: 'Pretendard', sans-serif; overflow: hidden; font-size: 16px; margin: 0; background-color: var(--bg-deep); color: var(--slate-200); }
/* 레이아웃 구조 */
#layout-wrapper { display: flex; width: 100vw; height: 100vh; overflow: hidden; }
/* 사이드바 스타일 */
.sidebar { width: 320px; flex-shrink: 0; background-color: var(--bg-side); border-right: 1px solid var(--slate-800); display: flex; flex-direction: column; z-index: 20; }
.sidebar-header { padding: 2rem; border-bottom: 1px solid var(--slate-800); }
.logo-area { display: flex; align-items: center; gap: 0.75rem; color: var(--indigo-400); font-weight: 700; font-size: 1.5rem; letter-spacing: -0.025em; }
.logo-icon { padding: 0.625rem; background: rgba(99, 102, 241, 0.1); border-radius: 6px; }
.sidebar-nav { flex: 1; padding: 1.25rem; overflow-y: auto; }
.nav-label { font-size: 0.75rem; font-bold: 700; color: var(--slate-500); text-transform: uppercase; letter-spacing: 0.2em; padding: 0 1rem; margin-bottom: 1.25rem; opacity: 0.5; }
.nav-item { display: flex; align-items: center; gap: 1rem; padding: 1rem 1.25rem; border-radius: 7px; transition: all 0.3s; color: var(--slate-400); text-decoration: none; margin-bottom: 0.625rem; }
.nav-item:hover { background: rgba(30, 41, 59, 0.5); color: var(--slate-200); }
.nav-item.active { background: rgba(79, 70, 229, 0.2); color: var(--indigo-400); border: 1px solid rgba(99, 102, 241, 0.3); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); }
.sidebar-footer { padding: 1.5rem; border-top: 1px solid var(--slate-800); }
.mount-info { background: rgba(2, 6, 23, 0.5); border-radius: 6px; padding: 1.25rem; border: 1px solid var(--slate-800); }
.mount-label { font-size: 0.75rem; color: var(--slate-500); font-weight: 700; text-transform: uppercase; margin-bottom: 0.625rem; letter-spacing: 0.05em; }
.mount-path { font-family: monospace; font-size: 13px; color: rgba(129, 140, 248, 0.7); word-break: break-all; line-height: 1.6; }
/* 메인 영역 스타일 */
.main-container { flex: 1; display: flex; flex-direction: column; overflow: hidden; position: relative; }
.top-header { height: 6rem; background: rgba(2, 6, 23, 0.8); backdrop-filter: blur(12px); border-bottom: 1px solid var(--slate-800); display: flex; align-items: center; justify-content: space-between; padding: 0 2.5rem; z-index: 10; }
.path-box { display: flex; align-items: center; gap: 1rem; flex: 1; min-width: 0; }
.path-display { font-family: monospace; font-size: 0.875rem; color: #cbd5e1; background: rgba(15, 23, 42, 0.8); padding: 0.75rem 1.5rem; border-radius: 6px; border: 1px solid #1e293b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.path-label { color: var(--indigo-500); font-weight: 700; text-transform: uppercase; margin-right: 0.75rem; }
.size-badge { display: flex; align-items: center; gap: 0.75rem; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.2); padding: 0.75rem 1.25rem; border-radius: 6px; flex-shrink: 0; }
.size-label { font-size: 0.75rem; font-weight: 700; color: var(--slate-500); text-transform: uppercase; letter-spacing: 0.1em; }
.size-value { font-family: monospace; font-size: 0.875rem; color: var(--indigo-400); font-weight: 700; }
.refresh-btn { padding: 0.75rem; border-radius: 9999px; transition: all 0.7s; color: var(--slate-400); margin-left: 1rem; }
.refresh-btn:hover { background: #1e293b; color: #fff; border-color: #334155; transform: rotate(180deg); }
/* 스크롤 영역 및 테이블 */
.scroll-area { flex: 1; padding: 2.5rem; overflow-y: auto; }
.alert-box { margin-bottom: 2rem; padding: 1.25rem; background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.2); color: var(--emerald-400); border-radius: 7px; display: flex; align-items: center; justify-content: space-between; }
.action-bar { margin-bottom: 2rem; display: flex; align-items: center; justify-content: space-between; }
.breadcrumb { display: flex; align-items: center; gap: 0.75rem; font-size: 1rem; }
.breadcrumb a { text-decoration: none; color: var(--slate-500); transition: color 0.2s; font-weight: 500; }
.breadcrumb a:hover { color: var(--indigo-400); }
.breadcrumb .separator { color: #334155; }
.btn-batch { display: flex; align-items: center; gap: 0.625rem; padding: 0.75rem 1.5rem; background: rgba(239, 68, 68, 0.1); color: var(--red-400); border: 1px solid rgba(239, 68, 68, 0.2); border-radius: 6px; font-weight: 700; font-size: 0.875rem; transition: all 0.3s; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); }
.btn-batch:hover { background: #ef4444; color: #fff; }
.glass-panel { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 10px; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); }
.explorer-table { width: 100%; border-collapse: separate; border-spacing: 0; text-align: left; font-size: 15px; }
.explorer-table thead tr { background: rgba(30, 41, 59, 0.3); color: var(--slate-500); font-weight: 700; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.15em; }
.explorer-table th { padding: 1.75rem 1.5rem; border-bottom: 1px solid var(--slate-800); }
.explorer-table td { padding: 1.5rem; border-bottom: 1px solid var(--slate-800); }
.row-item { opacity: 0; transform: translateX(-10px); transition: all 0.3s ease; cursor: pointer; }
.row-item.visible { opacity: 1; transform: translateX(0); }
.row-item:hover { background-color: rgba(99, 102, 241, 0.08) !important; box-shadow: inset 5px 0 0 0 var(--indigo-500); }
.file-icon-box { padding: 0.75rem; border-radius: 7px; transition: transform 0.3s; display: flex; align-items: center; justify-content: center; }
.is-dir .file-icon-box { background: rgba(245, 158, 11, 0.1); color: var(--amber-500); }
.is-file .file-icon-box { background: rgba(51, 65, 85, 0.3); color: var(--slate-400); }
.row-item:hover .file-icon-box { transform: scale(1.1); }
.name-text { font-weight: 700; font-size: 1rem; transition: color 0.2s; }
.is-dir .name-text { color: var(--slate-200); }
.is-file .name-text { color: var(--slate-400); }
.row-item:hover .name-text { color: #fff; }
.size-text { font-family: monospace; font-size: 0.875rem; }
.is-dir .size-text { color: rgba(129, 140, 248, 0.8); }
.is-file .size-text { color: var(--slate-500); }
.time-text { font-size: 0.875rem; font-variant-numeric: tabular-nums; color: var(--slate-500); }
.mtime-text { font-size: 0.875rem; font-variant-numeric: tabular-nums; font-weight: 600; color: rgba(129, 140, 248, 0.7); }
.btn-delete { padding: 0.75rem; border-radius: 7px; color: var(--slate-500); transition: all 0.2s; }
.btn-delete:hover { color: var(--red-400); background: rgba(239, 68, 68, 0.1); }
/* 공통 애니메이션 및 기타 */
.animate-fadeInUp { animation: fadeInUp 0.5s ease forwards; opacity: 0; }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.delay-1 { animation-delay: 0.1s; }
.delay-2 { animation-delay: 0.2s; }
.custom-scrollbar::-webkit-scrollbar { width: 8px; }
.custom-scrollbar::-webkit-scrollbar-track { background: var(--bg-side); }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 7px; }
input[type="checkbox"] { width: 1.25rem; height: 1.25rem; border-radius: 4px; background: #0f172a; border: 1px solid #334155; cursor: pointer; accent-color: var(--indigo-500); }
/*---------| 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>
</head>
<body class="bg-[#020617] text-slate-200">
<div id="layout-wrapper">
<!-- 사이드바 -->
<aside class="sidebar">
<div class="sidebar-header">
<h1 class="logo-area animate-fadeInUp">
<div class="logo-icon">
<i data-lucide="layers" class="w-7 h-7"></i>
</div>
<span>DATA CORE</span>
</h1>
</div>
<nav class="sidebar-nav">
<div class="nav-label">Storage Volumes</div>
<?php foreach ($volumes as $id => $vol): ?>
<a href="?vol=<?php echo $id; ?>"
class="nav-item <?php echo $current_vol_id === $id ? 'active' : ''; ?>">
<i data-lucide="<?php echo $id === 'maria' ? 'database' : 'hard-drive'; ?>" class="w-6 h-6"></i>
<span style="font-weight: 600; font-size: 15px;"><?php echo $vol['name']; ?></span>
</a>
<?php endforeach; ?>
</nav>
<div class="sidebar-footer">
<div class="mount-info">
<p class="mount-label">Base Mount Point</p>
<p class="mount-path"><?php echo $root_path; ?></p>
</div>
</div>
</aside>
<!-- 메인 컨텐츠 -->
<main class="main-container">
<header class="top-header">
<div class="path-box animate-fadeInUp">
<div id="pathDisplay" class="path-display">
<span class="path-label">Path:</span>
<?php echo $root_path . ($current_sub_dir ? DIRECTORY_SEPARATOR . $current_sub_dir : ''); ?>
</div>
<div class="size-badge">
<i data-lucide="database" class="w-4 h-4 text-indigo-400"></i>
<span class="size-label hidden-md">Total Size:</span>
<span class="size-value"><?php echo formatSize($total_dir_bytes); ?></span>
</div>
</div>
<button onclick="location.reload()" class="refresh-btn">
<i data-lucide="refresh-cw" class="w-6 h-6"></i>
</button>
</header>
<div class="scroll-area custom-scrollbar">
<?php foreach ($messages as $msg): ?>
<div class="alert-box animate-fadeInUp">
<div style="display: flex; align-items: center; gap: 1rem;">
<i data-lucide="check-circle" class="w-6 h-6"></i>
<span style="font-weight: 500; font-size: 15px;"><?php echo htmlspecialchars($msg); ?></span>
</div>
</div>
<?php endforeach; ?>
<div class="action-bar animate-fadeInUp delay-1">
<nav class="breadcrumb">
<a href="?vol=<?php echo $current_vol_id; ?>" style="display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="home" class="w-5 h-5"></i>
<span>root</span>
</a>
<?php
if ($current_sub_dir) {
$parts = explode(DIRECTORY_SEPARATOR, trim($current_sub_dir, DIRECTORY_SEPARATOR));
$path_acc = '';
foreach ($parts as $part) {
if (!$part) continue;
$path_acc .= ($path_acc ? DIRECTORY_SEPARATOR : '') . $part;
echo '<i data-lucide="chevron-right" class="w-5 h-5 separator"></i>';
echo '<a href="?vol=' . $current_vol_id . '&dir=' . urlencode($path_acc) . '">' . htmlspecialchars($part) . '</a>';
}
}
?>
</nav>
<button type="button" onclick="submitBatchDelete()" id="batchDeleteBtn" style="display: none;" class="btn-batch animate-fadeInUp">
<i data-lucide="trash-2" class="w-5 h-5"></i> Selected Delete
</button>
</div>
<form id="mainForm" method="POST" class="animate-fadeInUp delay-2">
<input type="hidden" name="action" id="formAction" value="delete_batch">
<div class="glass-panel">
<table class="explorer-table">
<thead>
<tr>
<th style="width: 4rem; text-align: center;">
<input type="checkbox" id="selectAll">
</th>
<th>Name</th>
<th>Size</th>
<th>Created</th>
<th>Modified</th>
<th style="text-align: center;">Action</th>
</tr>
</thead>
<tbody id="fileTableBody">
<?php if ($current_sub_dir !== ''): ?>
<?php
$parent = dirname($current_sub_dir);
if ($parent === '.' || $parent === DIRECTORY_SEPARATOR) $parent = '';
?>
<tr class="row-item" onclick="location.href='?vol=<?php echo $current_vol_id; ?>&dir=<?php echo urlencode($parent); ?>'">
<td style="text-align: center;">
<i data-lucide="arrow-up" style="color: #475569;"></i>
</td>
<td colspan="5" style="color: rgba(129, 140, 248, 0.8); font-weight: 700; font-style: italic;">
Parent Directory
</td>
</tr>
<?php endif; ?>
<?php foreach ($result as $index => $item): ?>
<tr class="row-item <?php echo $item['is_dir'] ? 'is-dir' : 'is-file'; ?>">
<td style="text-align: center;" onclick="event.stopPropagation();">
<?php if (!$item['is_dir']): ?>
<input type="checkbox" name="selected_files[]" value="<?php echo htmlspecialchars($item['name']); ?>" class="file-checkbox">
<?php else: ?>
<i data-lucide="folder" style="color: rgba(245, 158, 11, 0.4);"></i>
<?php endif; ?>
</td>
<td style="display: flex; align-items: center; gap: 1.25rem;" onclick="if(<?php echo $item['is_dir']?'true':'false'; ?>) location.href='?vol=<?php echo $current_vol_id; ?>&dir=<?php echo urlencode($item['path']); ?>'">
<div class="file-icon-box">
<i data-lucide="<?php echo $item['is_dir'] ? 'folder' : 'file'; ?>" class="w-6 h-6"></i>
</div>
<span class="name-text"><?php echo htmlspecialchars($item['name']); ?></span>
</td>
<td class="size-text"><?php echo $item['size']; ?></td>
<td class="time-text"><?php echo $item['ctime']; ?></td>
<td class="mtime-text"><?php echo $item['mtime']; ?></td>
<td style="text-align: center;" onclick="event.stopPropagation();">
<?php if (!$item['is_dir']): ?>
<button type="button" onclick="singleDelete('<?php echo addslashes($item['name']); ?>')" class="btn-delete">
<i data-lucide="trash-2" class="w-5 h-5"></i>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<input type="hidden" name="file_name" id="singleDeleteFileName">
</form>
</div>
</main>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
const rows = document.querySelectorAll('.row-item');
rows.forEach((row, index) => {
setTimeout(() => { row.classList.add('visible'); }, index * 40);
});
const selectAll = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.file-checkbox');
const batchBtn = document.getElementById('batchDeleteBtn');
if (selectAll) {
selectAll.addEventListener('change', () => {
checkboxes.forEach(cb => cb.checked = selectAll.checked);
toggleBatchButton();
});
}
checkboxes.forEach(cb => {
cb.addEventListener('change', toggleBatchButton);
});
function toggleBatchButton() {
const checkedCount = document.querySelectorAll('.file-checkbox:checked').length;
batchBtn.style.display = checkedCount > 0 ? 'flex' : 'none';
}
window.submitBatchDelete = function() {
if (confirm(`선택한 항목들을 영구 삭제하시겠습니까?`)) {
document.getElementById('formAction').value = 'delete_batch';
document.getElementById('mainForm').submit();
}
}
window.singleDelete = function(fileName) {
if (confirm(`'${fileName}' 파일을 삭제하시겠습니까?`)) {
document.getElementById('formAction').value = 'delete';
document.getElementById('singleDeleteFileName').value = fileName;
document.getElementById('mainForm').submit();
}
}
});
</script>
</body>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>