<?php
//分片版APK分发面板｜修复大文件下载Http Data Error｜流式下载｜PHP7.4
$db_host = 'localhost';
$db_port = 3306;
$db_user = 'y2nbbqn55q';
$db_pass = 'a29dp0ooyh';
$db_name = 'y2nbbqn55q';
$raw_admin_pwd = '12345678910';

$uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'uploads';
$chunkDir  = __DIR__ . DIRECTORY_SEPARATOR . 'chunks';

session_start();

if (!is_dir($uploadDir)) @mkdir($uploadDir, 0755, true);
if (!is_dir($chunkDir))  @mkdir($chunkDir, 0755, true);

$pdo = null;
try {
    $pdo = new PDO("mysql:host={$db_host};port={$db_port};dbname={$db_name};charset=utf8mb4", $db_user, $db_pass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]);

    $pdo->exec("
    CREATE TABLE IF NOT EXISTS apk_files (
        id INT AUTO_INCREMENT PRIMARY KEY,
        file_name VARCHAR(255) NOT NULL,
        stored_name VARCHAR(255) NOT NULL,
        file_size BIGINT NOT NULL DEFAULT 0,
        mime_type VARCHAR(128) NOT NULL DEFAULT 'application/vnd.android.package-archive',
        upload_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    ");

    $checkCol = $pdo->query("SHOW COLUMNS FROM apk_files LIKE 'stored_name'");
    if (!$checkCol->fetch()) {
        $pdo->exec("ALTER TABLE apk_files ADD COLUMN stored_name VARCHAR(255) NOT NULL AFTER file_name");
    }

    $checkOld = $pdo->query("SHOW COLUMNS FROM apk_files LIKE 'file_data'");
    if ($checkOld->fetch()) {
        $pdo->exec("ALTER TABLE apk_files DROP COLUMN file_data");
    }

    $pdo->exec("
    CREATE TABLE IF NOT EXISTS site_config (
        id INT AUTO_INCREMENT PRIMARY KEY,
        cfg_key VARCHAR(50) NOT NULL UNIQUE,
        cfg_value TEXT NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    ");

    $stmtCfg = $pdo->prepare("SELECT cfg_value FROM site_config WHERE cfg_key = ? LIMIT 1");
    $stmtCfg->execute(['admin_hash']);
    $hashRow = $stmtCfg->fetch(PDO::FETCH_ASSOC);
    if (!$hashRow) {
        $newHash = password_hash($raw_admin_pwd, PASSWORD_DEFAULT);
        $insCfg = $pdo->prepare("INSERT INTO site_config(cfg_key,cfg_value) VALUES(?,?)");
        $insCfg->execute(['admin_hash', $newHash]);
        $adminHash = $newHash;
    } else {
        $adminHash = $hashRow['cfg_value'];
    }
} catch (Exception $e) {
    die("数据库连接失败：" . htmlspecialchars($e->getMessage()));
}

$action = $_GET['act'] ?? 'home';
$isAdmin = isset($_SESSION['admin']) && $_SESSION['admin'] === true;

//分片上传接口
if ($action === 'ajax_upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    try {
        if (!$isAdmin) {
            echo json_encode(['ok' => false, 'msg' => '无管理员权限']);
            exit;
        }

        $countStmt = $pdo->query("SELECT COUNT(*) AS total FROM apk_files");
        $countRow = $countStmt->fetch(PDO::FETCH_ASSOC);
        if ((int)$countRow['total'] >= 5) {
            echo json_encode(['ok' => false, 'msg' => '最多存放5个APK，请先删除旧包']);
            exit;
        }

        $fileName  = $_POST['filename'] ?? '';
        $fileKey   = $_POST['filekey'] ?? '';
        $index     = (int)($_POST['index'] ?? 0);
        $totalPart = (int)($_POST['total'] ?? 0);
        $fileSize  = (int)($_POST['filesize'] ?? 0);

        if ($fileSize > 2147483648) {
            echo json_encode(['ok' => false, 'msg' => '文件不能超过2GB']);
            exit;
        }
        if (!isset($_FILES['chunk']) || $_FILES['chunk']['error'] !== UPLOAD_ERR_OK) {
            echo json_encode(['ok' => false, 'msg' => '分片接收失败']);
            exit;
        }

        $chunkPath = $chunkDir . DIRECTORY_SEPARATOR . $fileKey . '_' . $index;
        move_uploaded_file($_FILES['chunk']['tmp_name'], $chunkPath);

        //最后一片，合并
        if ($index === $totalPart - 1) {
            $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
            if ($ext !== 'apk') {
                for ($i = 0; $i < $totalPart; $i++) {
                    @unlink($chunkDir . DIRECTORY_SEPARATOR . $fileKey . '_' . $i);
                }
                echo json_encode(['ok' => false, 'msg' => '仅允许APK文件']);
                exit;
            }

            $storedName = uniqid('apk_', true) . '.apk';
            $targetPath = $uploadDir . DIRECTORY_SEPARATOR . $storedName;
            $out = fopen($targetPath, 'wb');
            for ($i = 0; $i < $totalPart; $i++) {
                $partFile = $chunkDir . DIRECTORY_SEPARATOR . $fileKey . '_' . $i;
                $in = fopen($partFile, 'rb');
                stream_copy_to_stream($in, $out);
                fclose($in);
                @unlink($partFile);
            }
            fclose($out);

            $insFile = $pdo->prepare("INSERT INTO apk_files(file_name,stored_name,file_size,mime_type) VALUES(?,?,?,?)");
            $insFile->execute([$fileName, $storedName, $fileSize, 'application/vnd.android.package-archive']);

            echo json_encode(['ok' => true, 'msg' => 'APK上传完成']);
            exit;
        }

        echo json_encode(['ok' => true, 'msg' => '分片已接收']);
        exit;
    } catch (Exception $ex) {
        echo json_encode(['ok' => false, 'msg' => '服务器异常：' . $ex->getMessage()]);
        exit;
    }
}

//流式下载（修复Http Data Error）
if ($action === 'download') {
    @set_time_limit(0);
    @ignore_user_abort(true);
    if (ob_get_length()) ob_end_clean();

    $id = (int)($_GET['id'] ?? 0);
    $downStmt = $pdo->prepare("SELECT file_name,stored_name,mime_type,file_size FROM apk_files WHERE id = ? LIMIT 1");
    $downStmt->execute([$id]);
    $fileRow = $downStmt->fetch(PDO::FETCH_ASSOC);
    if (!$fileRow) die("文件不存在");

    $realPath = $uploadDir . DIRECTORY_SEPARATOR . $fileRow['stored_name'];
    if (!is_file($realPath)) die("文件丢失");

    $size = filesize($realPath);
    header("Content-Type: " . $fileRow['mime_type']);
    header('Content-Disposition: attachment; filename="' . rawurlencode($fileRow['file_name']) . '"');
    header("Content-Length: " . $size);
    header("Accept-Ranges: bytes");

    $fp = fopen($realPath, "rb");
    if ($fp) {
        $step = 1024 * 1024;
        while (!feof($fp)) {
            echo fread($fp, $step);
            flush();
        }
        fclose($fp);
    }
    exit;
}

//删除APK
if ($action === 'delete' && $isAdmin) {
    $delId = (int)($_GET['id'] ?? 0);
    $selStmt = $pdo->prepare("SELECT stored_name FROM apk_files WHERE id = ? LIMIT 1");
    $selStmt->execute([$delId]);
    $delRow = $selStmt->fetch(PDO::FETCH_ASSOC);
    if ($delRow) {
        $delFile = $uploadDir . DIRECTORY_SEPARATOR . $delRow['stored_name'];
        if (is_file($delFile)) @unlink($delFile);
        $pdo->prepare("DELETE FROM apk_files WHERE id = ?")->execute([$delId]);
    }
    header("Location: index.php");
    exit;
}

//管理员登录
if ($action === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $pwd = trim($_POST['pwd'] ?? '');
    if (password_verify($pwd, $adminHash)) {
        $_SESSION['admin'] = true;
        header("Location: index.php");
        exit;
    } else {
        $errMsg = "密码错误";
    }
}

//退出登录
if ($action === 'logout') {
    unset($_SESSION['admin']);
    session_destroy();
    header("Location: index.php");
    exit;
}

//读取APK列表
$apkListStmt = $pdo->query("SELECT id,file_name,file_size,upload_time FROM apk_files ORDER BY id DESC");
$apkList = $apkListStmt->fetchAll(PDO::FETCH_ASSOC);

function formatSize($bytes)
{
    if ($bytes < 1024) return $bytes . " B";
    if ($bytes < 1048576) return round($bytes / 1024, 2) . " KB";
    if ($bytes < 1073741824) return round($bytes / 1048576, 2) . " MB";
    return round($bytes / 1073741824, 2) . " GB";
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APK分发面板</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:system-ui}
body{background:#0f172a;color:#e2e8f0;padding:20px}
.wrap{max-width:900px;margin:0 auto}
h1{text-align:center;margin-bottom:24px;font-size:28px}
.card{background:#1e293b;border-radius:12px;padding:20px;margin-bottom:18px}
.apk-item{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;padding:14px 0;border-bottom:1px solid #334155}
.apk-item:last-child{border-bottom:none}
.apk-name{word-break:break-all;font-size:16px;font-weight:500}
.apk-meta{font-size:13px;color:#94a3b8;margin-top:4px}
.btn{display:inline-block;padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:14px;text-decoration:none}
.btn-down{background:#2563eb;color:#fff}
.btn-del{background:#dc2626;color:#fff}
.btn-primary{background:#16a34a;color:#fff}
.upload-row{text-align:center}
#apkFile{margin:14px 0;color:#fff}
.progress-box{width:100%;height:14px;background:#0f172a;border-radius:8px;overflow:hidden;margin:12px 0;display:none}
.progress-bar{height:100%;width:0%;background:#22c55e;transition:width .2s}
.progress-text{text-align:center;margin-bottom:10px;display:none}
.login-box{max-width:380px;margin:40px auto;background:#1e293b;padding:24px;border-radius:12px}
.login-box input{width:100%;padding:12px;border-radius:8px;border:1px solid #475569;background:#0f172a;color:#fff;margin-bottom:14px;font-size:15px}
.err{color:#f87171;text-align:center;margin-bottom:10px}
.logout-link{text-align:right;margin-bottom:10px}
.logout-link a{color:#94a3b8}
.empty{text-align:center;padding:30px;color:#94a3b8}
</style>
</head>
<body>
<div class="wrap">
    <h1>APK分发面板</h1>
    <?php if (!$isAdmin): ?>
        <div class="card login-box">
            <h2 style="text-align:center;margin-bottom:16px">管理员登录</h2>
            <?php if (isset($errMsg)): ?><div class="err"><?php echo htmlspecialchars($errMsg); ?></div><?php endif; ?>
            <form method="post" action="index.php?act=login">
                <input type="password" name="pwd" placeholder="管理员密码" required>
                <button class="btn btn-primary" style="width:100%">登录</button>
            </form>
        </div>
    <?php else: ?>
        <div class="logout-link"><a href="index.php?act=logout">退出登录</a></div>
        <div class="card upload-row">
            <h2 style="margin-bottom:10px">分片上传APK（上限2GB）</h2>
            <input type="file" id="apkFile" accept=".apk">
            <br>
            <button id="uploadBtn" class="btn btn-primary">开始上传</button>
            <div class="progress-box"><div class="progress-bar" id="bar"></div></div>
            <div class="progress-text" id="ptext">0%</div>
            <p style="margin-top:8px;color:#94a3b8;font-size:13px">单片15MB，规避Nginx 413限制，上传完成等待合并，不要刷新页面</p>
        </div>
    <?php endif; ?>

    <div class="card">
        <h2 style="margin-bottom:10px">APK下载列表</h2>
        <?php if (count($apkList) === 0): ?>
            <div class="empty">暂无APK</div>
        <?php else: ?>
            <?php foreach ($apkList as $apk): ?>
                <div class="apk-item">
                    <div>
                        <div class="apk-name"><?php echo htmlspecialchars($apk['file_name']); ?></div>
                        <div class="apk-meta">大小：<?php echo formatSize($apk['file_size']); ?>　上传时间：<?php echo htmlspecialchars($apk['upload_time']); ?></div>
                    </div>
                    <div>
                        <a class="btn btn-down" href="index.php?act=download&id=<?php echo (int)$apk['id']; ?>">下载</a>
                        <?php if ($isAdmin): ?>
                            <a class="btn btn-del" href="index.php?act=delete&id=<?php echo (int)$apk['id']; ?>" onclick="return confirm('确认删除？')">删除</a>
                        <?php endif; ?>
                    </div>
                </div>
            <?php endforeach; ?>
        <?php endif; ?>
    </div>
</div>

<script>
const CHUNK_SIZE = 15 * 1024 * 1024;
const uploadBtn = document.getElementById('uploadBtn');
const apkFile = document.getElementById('apkFile');
const bar = document.getElementById('bar');
const ptext = document.getElementById('ptext');

if(uploadBtn){
    uploadBtn.addEventListener('click', async function(){
        const file = apkFile.files[0];
        if(!file){
            alert("请选择APK文件");
            return;
        }
        uploadBtn.disabled = true;
        document.querySelector('.progress-box').style.display = 'block';
        ptext.style.display = 'block';

        const total = Math.ceil(file.size / CHUNK_SIZE);
        const fileKey = file.name + "_" + file.size + "_" + file.lastModified;
        try{
            for(let i = 0; i < total; i++){
                const start = i * CHUNK_SIZE;
                const end = Math.min(start + CHUNK_SIZE, file.size);
                const blob = file.slice(start, end);
                const fd = new FormData();
                fd.append('chunk', blob);
                fd.append('filename', file.name);
                fd.append('filekey', fileKey);
                fd.append('index', i);
                fd.append('total', total);
                fd.append('filesize', file.size);

                await new Promise((resolve, reject)=>{
                    const xhr = new XMLHttpRequest();
                    xhr.open('POST', 'index.php?act=ajax_upload', true);
                    xhr.onload = function(){
                        try{
                            const res = JSON.parse(xhr.responseText);
                            if(res.ok){
                                const percent = Math.round(((i + 1) / total) * 100);
                                bar.style.width = percent + "%";
                                ptext.innerText = percent + "%";
                                resolve();
                            }else{
                                reject(new Error(res.msg));
                            }
                        }catch(e){
                            reject(new Error(xhr.responseText));
                        }
                    };
                    xhr.onerror = function(){ reject(new Error("分片网络错误")); };
                    xhr.send(fd);
                });
            }
            ptext.innerText = "100%，合并APK中，请稍候…";
            setTimeout(()=>{ window.location.reload(); }, 1500);
        }catch(err){
            alert(err.message);
            bar.style.width = "0%";
            ptext.innerText = "0%";
            document.querySelector('.progress-box').style.display = 'none';
            ptext.style.display = 'none';
        }
        uploadBtn.disabled = false;
    });
}
</script>
</body>
</html>
