<?php
/**
* file_manager_v2.php
* - 구조 변경 없음
* - ZIP 파일인 경우: [삭제] 버튼으로 동작
*/
$root_dir = '/home/www/GNU/skin/board';
$req_dir = isset($_GET['dir']) ? $_GET['dir'] : '';
// 상위 경로 이동 방지
if (strpos($req_dir, '..') !== false) $req_dir = '';
$current_path = realpath($root_dir . '/' . $req_dir);
// 경로 유효성 및 생성
if (!$current_path || strpos($current_path, realpath($root_dir)) !== 0) {
if (!file_exists($root_dir)) @mkdir($root_dir, 0707, true);
$current_path = realpath($root_dir);
$req_dir = '';
}
// ---------------------------------------------------------
// [1] 다운로드 처리
// ---------------------------------------------------------
if (isset($_GET['download'])) {
$file = basename($_GET['download']);
$file_path = $current_path . '/' . $file;
if (file_exists($file_path) && is_file($file_path)) {
if (ob_get_level()) ob_end_clean();
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $file . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file_path));
readfile($file_path);
exit;
} else {
echo "<script>alert('파일이 없습니다.'); history.back();</script>";
exit;
}
}
// ---------------------------------------------------------
// [2] 액션 처리 (압축 & 삭제)
// ---------------------------------------------------------
$message = "";
if (isset($_POST['action']) && $_POST['action'] === 'zip') {
$target = $_POST['target'];
$source_path = $current_path . '/' . $target;
if (!class_exists('ZipArchive')) {
$message = "❌ PHP Zip 모듈 미설치";
} elseif (!is_writable($current_path)) {
$message = "❌ 쓰기 권한 없음";
} else {
$zip_name = $target . '_' . date('Ymd_His') . '.zip';
$zip_full_path = $current_path . '/' . $zip_name;
$zip = new ZipArchive();
if ($zip->open($zip_full_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
if (is_dir($source_path)) {
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source_path), RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($files as $file) {
if (!$file->isDir()) {
$filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($source_path) + 1);
$zip->addFile($filePath, $target . '/' . $relativePath);
}
}
} else {
$zip->addFile($source_path, basename($source_path));
}
$zip->close();
$message = "✅ 압축 성공: $zip_name";
} else {
$message = "❌ 압축 실패";
}
}
}
if (isset($_POST['action']) && $_POST['action'] === 'delete') {
$target = $_POST['target'];
$target_path = $current_path . '/' . $target;
if (file_exists($target_path) && is_file($target_path)) {
if (unlink($target_path)) {
$message = "🗑️ 삭제 완료: $target";
} else {
$message = "❌ 삭제 실패";
}
}
}
// 리스트 읽기
$list = [];
if (is_dir($current_path)) {
$items = scandir($current_path);
$dirs = [];
$files = [];
foreach ($items as $item) {
if ($item === '.') continue;
if ($item === '..' && $req_dir === '') continue;
$full_path = $current_path . '/' . $item;
if (is_dir($full_path)) $dirs[] = $item;
else $files[] = $item;
}
$list = array_merge($dirs, $files);
}
require_once '/home/www/GNU/_PAGE/head.php';
?>
<title>Advanced Daemon File Manager</title>
<!-- 웹 폰트 및 아이콘 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--bg: #0b0e11;
--card-bg: #181a20;
--border: #2b3139;
--text-main: #eaecef;
--text-dim: #848e9c;
--melon: #CBFF75;
--binance-yellow: #f0b90b;
--danger: #f6465d;
--blue: #38bdf8;
}
/* 로딩 애니메이션 */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes progress {
0% { width: 0; }
100% { width: 100%; }
}
#loader-bar {
position: fixed; top: 0; left: 0; height: 3px; background: var(--melon);
z-index: 9999; animation: progress 0.8s ease-in-out forwards;
}
body {
background-color: var(--bg);
color: var(--text-main);
font-family: 'Pretendard', sans-serif;
margin: 0;
line-height: 1.5;
}
.container {
max-width: 1200px;
margin: 0 auto;
animation: fadeInUp 0.8s ease-out;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 50px;
}
h2 {
margin: 0;
font-size: 2.5rem;
color: var(--melon);
display: flex;
align-items: center;
gap: 12px;
}
.breadcrumb {
background: var(--card-bg);
padding: 10px 20px;
border-radius: 4px;
border: 1px solid var(--border);
margin-bottom: 20px;
font-size: 0.9rem;
color: var(--text-dim);
}
.msg {
background: rgba(203, 255, 117, 0.1);
border-left: 4px solid var(--melon);
padding: 15px 20px;
margin-bottom: 25px;
color: var(--melon);
border-radius: 4px;
animation: fadeInUp 0.5s ease-out;
}
.file-card {
background: var(--card-bg);
border-radius: 4px;
border: 1px solid var(--border);
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: rgba(0,0,0,0.2);
padding: 15px;
text-align: left;
font-size: 0.85rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid var(--border);
}
td {
padding: 14px 15px;
border-bottom: 1px solid var(--border);
transition: background 0.2s;
}
tr:hover td {
background: rgba(255, 255, 255, 0.02);
}
/* 아이콘 색상 */
.fa-folder { color: var(--binance-yellow); }
.fa-file-code { color: var(--blue); }
.fa-file-zipper { color: var(--melon); }
.fa-arrow-up { color: var(--text-dim); }
a { color: inherit; text-decoration: none; transition: 0.2s; }
a.dir-link:hover { color: var(--binance-yellow); }
.size-info {
font-family: 'Consolas', monospace;
font-size: 0.85rem;
color: var(--text-dim);
}
/* 버튼 그룹 */
.action-group {
display: flex;
gap: 8px;
}
.btn {
padding: 6px 12px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.75rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
color: #fff;
}
.btn-zip { background: #474d57; }
.btn-zip:hover { background: #5a6270; }
.btn-down { background: var(--blue); }
.btn-down:hover { opacity: 0.8; }
.btn-del { background: var(--danger); }
.btn-del:hover { opacity: 0.8; }
@media (max-width: 768px) {
body { padding: 20px; }
th:nth-child(2), td:nth-child(2) { display: none; }
}
/*---------| 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>
<div id="loader-bar"></div>
<div class="container">
<header>
<h2><i class="fa-solid fa-folder-tree"></i> SKIN FILE EXPLORER</h2>
<div style="font-size: 0.8rem; color: var(--text-dim);">Root: /DATA/UPBIT/daemon</div>
</header>
<div class="breadcrumb">
<i class="fa-solid fa-house" style="margin-right:8px;"></i>
<?php echo h($req_dir ? str_replace('/', ' <i class="fa-solid fa-chevron-right" style="font-size:0.7rem; margin:0 5px;"></i> ', $req_dir) : 'ROOT'); ?>
</div>
<?php if ($message): ?>
<div class="msg"><i class="fa-solid fa-circle-info" style="margin-right:10px;"></i> <?php echo $message; ?></div>
<?php endif; ?>
<div class="file-card">
<table>
<thead>
<tr>
<th>이름</th>
<th>크기</th>
<th style="text-align:right;">작업</th>
</tr>
</thead>
<tbody>
<?php foreach ($list as $index => $item):
if ($item === '..') {
$link = "?dir=" . urlencode(dirname($req_dir) === '.' ? '' : dirname($req_dir));
echo "<tr><td colspan='3'><a href='$link' class='dir-link'><i class='fa-solid fa-arrow-up'></i> <span style='margin-left:10px;'>상위 폴더로 이동</span></a></td></tr>";
continue;
}
$full_path = $current_path . '/' . $item;
$is_dir = is_dir($full_path);
$new_dir = $req_dir ? $req_dir . '/' . $item : $item;
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
$size = $is_dir ? '-' : number_format(filesize($full_path)) . ' B';
// 아이콘 결정
$icon = $is_dir ? 'fa-folder' : ($ext === 'zip' ? 'fa-file-zipper' : 'fa-file-code');
?>
<tr style="animation: fadeInUp 0.4s ease-out <?php echo $index * 0.05; ?>s both;">
<td>
<i class="fa-solid <?php echo $icon; ?>" style="width: 20px;"></i>
<?php if ($is_dir): ?>
<a href="?dir=<?php echo urlencode($new_dir); ?>" class="dir-link" style="margin-left:8px;"><?php echo $item; ?></a>
<?php else: ?>
<span style="margin-left:8px;"><?php echo $item; ?></span>
<?php endif; ?>
</td>
<td class="size-info"><?php echo $size; ?></td>
<td>
<div class="action-group" style="justify-content: flex-end;">
<?php if ($ext !== 'zip'): ?>
<form method="post" style="margin:0;">
<input type="hidden" name="target" value="<?php echo h($item); ?>">
<button type="submit" name="action" value="zip" class="btn btn-zip">
<i class="fa-solid fa-file-archive"></i> 압축
</button>
</form>
<?php endif; ?>
<?php if (!$is_dir && $ext === 'zip'): ?>
<form method="post" style="margin:0;" onsubmit="return confirm('정말 삭제하시겠습니까?');">
<input type="hidden" name="target" value="<?php echo h($item); ?>">
<button type="submit" name="action" value="delete" class="btn btn-del">
<i class="fa-solid fa-trash-can"></i> 삭제
</button>
</form>
<?php endif; ?>
<?php if (!$is_dir): ?>
<a href="?dir=<?php echo urlencode($req_dir); ?>&download=<?php echo urlencode($item); ?>" class="btn btn-down">
<i class="fa-solid fa-download"></i> 다운
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<script>
// 페이지 로드 시 로더 바 제거
window.onload = function() {
const loader = document.getElementById('loader-bar');
setTimeout(() => {
loader.style.opacity = '0';
setTimeout(() => loader.remove(), 500);
}, 300);
};
</script>
<?php
function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
?>
</body>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>