<?php
require_once __DIR__ . '/../base_api.php';

class FileAPI extends BaseAPI {
    protected $site_root;
    protected $user_name;
    
    // 允许上传的文件扩展名白名单（虚拟空间需要支持PHP等网站文件）
    protected $allowed_extensions = array(
        // 图片（更全面的支持）
        'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico',
        'tiff', 'tif', 'raw', 'cr2', 'nef', 'arw', 'dng',  // 相机原始格式
        'psd', 'ai', 'eps',  // 设计文件
        'heic', 'heif',  // 苹果格式
        // 视频
        'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm',
        // 音频
        'mp3', 'wav', 'ogg', 'flac', 'aac', 'wma', 'm4a',
        // 文档
        'txt', 'md', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
        'odt', 'ods', 'odp', 'rtf',
        // 压缩包
        'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz',
        // 网站代码文件
        'html', 'htm', 'css', 'js', 'json', 'xml', 'yaml', 'yml',
        'php', 'php3', 'php4', 'php5', 'php7', 'phtml',
        'asp', 'aspx', 'jsp', 'py', 'rb', 'pl',
        // 数据库
        'sql', 'db', 'sqlite', 'mdb', 'accdb',
        // 字体
        'ttf', 'otf', 'woff', 'woff2', 'eot',
        // 其他
        'log', 'csv', 'htaccess', 'htpasswd'
    );
    
    // 图片扩展名（这些文件不需要PHP代码检测）
    protected $image_extensions = array(
        'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico',
        'tiff', 'tif', 'raw', 'cr2', 'nef', 'arw', 'dng',
        'psd', 'ai', 'eps',
        'heic', 'heif'
    );
    
    // 危险扩展名黑名单（绝对禁止的可执行文件）
    protected $dangerous_extensions = array(
        // Windows可执行文件
        'exe', 'com', 'bat', 'cmd', 'scr', 'msi',
        'dll', 'sys', 'drv', 'bin',
        // 脚本文件（服务器端执行）
        'sh', 'bash', 'zsh', 'csh', 'tcsh',
        // Windows脚本
        'reg', 'vbs', 'vbe', 'jse', 'wsf', 'wsh',
        // 其他危险文件
        'cgi'
    );
    
    public function __construct() {
        parent::__construct();
        // 获取用户名
        $username = isset($this->user->user) ? $this->user->user : (isset($this->user['user']) ? $this->user['user'] : null);
        
        if (!$username) {
            $this->error('用户名不存在');
        }
        $this->user_name = $username;
        
        // 获取系统配置中的建站目录（使用$GLOBALS获取全局变量）
        $conf = isset($GLOBALS['conf']) ? $GLOBALS['conf'] : null;
        if (!$conf) {
            global $DB;
            $conf = $DB->get_row("SELECT * FROM `config` WHERE `id`=1");
        }
        $linux_path = isset($conf['hxi']) && !empty($conf['hxi']) ? $conf['hxi'] : '/www/wwwroot';
        $windows_path = isset($conf['hxo']) && !empty($conf['hxo']) ? $conf['hxo'] : 'D:/wwwroot';
        
        // 获取当前用户的宝塔服务器信息，判断操作系统类型
        $bt = $this->getBtServer();
        $os_type = isset($bt['czzt']) ? $bt['czzt'] : 'Linux';
        
        // 根据操作系统选择建站目录
        $this->site_root = ($os_type == 'Linux' ? $linux_path : $windows_path) . '/' . $username;
        
        // 确保用户目录存在
        if (!is_dir($this->site_root)) {
            @mkdir($this->site_root, 0777, true);
            // 再次检查
            if (!is_dir($this->site_root)) {
                $create_result = $this->bt_api->CreateDirectory($this->site_root);
            }
        }
        
    }
    
    /**
     * 获取当前用户的宝塔服务器信息
     */
    private function getBtServer() {
        global $DB;
        // 兼容对象和数组格式的用户数据
        $btid = isset($this->user->btid) ? $this->user->btid : (isset($this->user['btid']) ? $this->user['btid'] : 0);
        if (!$btid) {
            return array('czzt' => 'Linux');
        }
        $bt = $DB->get_row("SELECT * FROM `bt` WHERE `id`=" . intval($btid));
        return $bt ? (array)$bt : array('czzt' => 'Linux');
    }

    /**
     * 验证文件扩展名是否允许上传
     * @param string $filename 文件名
     * @return bool 是否允许上传
     */
    protected function isAllowedFile($filename) {
        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        
        // 1. 检查是否在危险扩展名黑名单中
        if (in_array($ext, $this->dangerous_extensions)) {
            return false;
        }
        
        // 2. 检查是否在允许的白名单中
        if (!in_array($ext, $this->allowed_extensions)) {
            return false;
        }
        
        return true;
    }

    /**
     * 检查文件内容是否包含PHP代码
     * @param string $filepath 文件路径
     * @return bool 是否包含PHP代码
     */
    protected function containsPHPCode($filepath) {
        $content = @file_get_contents($filepath);
        if ($content === false) {
            return false;
        }
        
        // 复用字符串检测逻辑
        return $this->containsPHPCodeString($content);
    }

    /**
     * 安全清理和验证路径
     * @param string $path 输入路径
     * @return string 清理后的相对路径（以/开头）
     */
    protected function sanitizePath($path) {
        // 1. 统一使用正斜杠
        $path = str_replace('\\', '/', $path);
        
        // 2. 解码URL编码（防止%2e%2e%2f绕过）
        $path = urldecode($path);
        
        // 3. 移除空字节（防止空字节截断）
        $path = str_replace("\0", '', $path);
        
        // 4. 移除双写绕过（....// -> ../）
        while (strpos($path, '../') !== false || strpos($path, '..\\') !== false) {
            $path = str_replace('../', '/', $path);
            $path = str_replace('..\\', '/', $path);
        }
        
        // 5. 移除 .. 结尾
        $path = preg_replace('#\.\.$#', '', $path);
        
        // 6. 规范化路径（移除多余的斜杠）
        $path = preg_replace('#/{2,}#', '/', $path);
        
        // 7. 确保以/开头
        $path = '/' . ltrim($path, '/');
        
        return $path;
    }
    
    /**
     * 验证路径是否在用户根目录下
     * @param string $full_path 完整路径（可能是文件或目录）
     * @return bool 是否合法
     */
    protected function validatePath($full_path) {
        // 直接使用字符串比较，不使用realpath
        // 这样可以处理不存在的目录
        
        // 检查路径是否以用户根目录开头
        if (strpos($full_path, $this->site_root) === 0) {
            return true;
        }
        
        // 如果路径不以用户根目录开头，检查是否是相对路径
        if (strpos($full_path, '/') === 0) {
            // 绝对路径，但不以用户根目录开头
            return false;
        }
        
        // 相对路径，认为是合法的（在用户根目录下）
        return true;
    }

    /**
     * 安全获取用户目录下的文件列表
     * 完全重写，确保只能访问当前用户的目录
     */
    public function getFiles($dir) {
        
        // 设置执行时间限制，避免超时
        @set_time_limit(30);
        
        // 1. 清理和验证输入路径
        $dir = $this->sanitizePath($dir);
        
        // 2. 计算完整路径
        $full_path = $dir === '/' ? $this->site_root : $this->site_root . $dir;
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        
        // 3. 严格验证路径是否在用户根目录下
        if (!$this->validatePath($full_path)) {
            $full_path = $this->site_root;
            $dir = '/';
        }
        
        // 4. 检查用户根目录是否存在，如果不存在则创建

        // 设置超时时间，避免宝塔API响应慢导致超时
        // 宝塔API已设置3秒超时，这里设置3.5秒作为判断标准
        $bt_timeout = 3.5;
        $start_time = microtime(true);

        $result = $this->bt_api->GetHostDirectory($full_path);

        $bt_time = microtime(true) - $start_time;

        // 如果宝塔API超时或失败，直接使用本地文件系统扫描
        if (!$result || !isset($result['DIR'])) {
            if ($bt_time > $bt_timeout) {
            } else {
            }
            $result = $this->scanLocalDirectory($full_path);
        } else {
            // 对于远程服务器，完全信任宝塔API的结果，不进行本地验证
            // 因为本地文件系统可能不是最新的
        }
        
        $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : null;
        $per_page = isset($_POST['per_page']) ? max(1, min(500, intval($_POST['per_page']))) : null;
        $this->processFileList($result, $dir, $page, $per_page);
    }
    
    /**
     * 扫描本地文件系统
     * @param string $path 要扫描的目录路径
     * @return array 返回与宝塔API相同格式的结果
     */
    private function scanLocalDirectory($path) {
        
        $result = array(
            'DIR' => array(),
            'FILES' => array()
        );
        
        if (!is_dir($path)) {
            return $result;
        }
        
        $items = @scandir($path);
        if ($items === false) {
            return $result;
        }
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $full_path = $path . '/' . $item;
            
            try {
                if (is_dir($full_path)) {
                    $stat = @stat($full_path);
                    if ($stat === false) {
                        continue;
                    }
                    $perms = substr(sprintf('%o', $stat['mode']), -4);
                    $owner = function_exists('posix_getpwuid') ? @posix_getpwuid($stat['uid'])['name'] : 'www';
                    $owner = $owner ?: 'www';
                    $result['DIR'][] = implode(';', array(
                        $item,
                        0,
                        $stat['mtime'],
                        $perms,
                        $owner
                    ));
                } elseif (is_file($full_path)) {
                    $stat = @stat($full_path);
                    if ($stat === false) {
                        continue;
                    }
                    $perms = substr(sprintf('%o', $stat['mode']), -4);
                    $owner = function_exists('posix_getpwuid') ? @posix_getpwuid($stat['uid'])['name'] : 'www';
                    $owner = $owner ?: 'www';
                    $result['FILES'][] = implode(';', array(
                        $item,
                        $stat['size'],
                        $stat['mtime'],
                        $perms,
                        $owner
                    ));
                }
            } catch (Exception $e) {
                continue;
            }
        }
        
        return $result;
    }
    
    /**
     * 解析文件大小字符串（如 "118.27 KB"）为字节数
     * @param string $size_str 文件大小字符串
     * @return int 字节数
     */
    private function parseSize($size_str) {
        $size_str = trim($size_str);
        if (is_numeric($size_str)) {
            return intval($size_str);
        }
        
        $units = array(
            'B' => 1,
            'KB' => 1024,
            'MB' => 1024 * 1024,
            'GB' => 1024 * 1024 * 1024,
            'TB' => 1024 * 1024 * 1024 * 1024
        );
        
        foreach ($units as $unit => $multiplier) {
            if (stripos($size_str, $unit) !== false) {
                $number = floatval(trim(str_ireplace($unit, '', $size_str)));
                return intval($number * $multiplier);
            }
        }
        
        return intval($size_str);
    }
    
    /**
     * 处理文件列表结果（宝塔 GetDir 返回 DIR/FILES 为 "name;size;mtime;mode;owner;..." 格式）
     * @param array $result 宝塔 API 返回
     * @param string $currentDir 当前请求的相对路径，如 / 或 /子目录
     * @param int|null $page 页码（从1开始），null 表示不分页
     * @param int|null $per_page 每页条数，null 表示不分页
     */
    private function processFileList($result, $currentDir = '/', $page = null, $per_page = null) {
        $files = array();
        $dirs = isset($result['DIR']) ? $result['DIR'] : array();
        $file_items = isset($result['FILES']) ? $result['FILES'] : array();
        $prefix = (trim($currentDir, '/') === '') ? '/' : rtrim($currentDir, '/') . '/';
        
        
        // 处理目录列表（宝塔返回的 name 为当前目录下的子项名，不含路径）
        foreach ($dirs as $item) {
            if (is_string($item)) {
                $parts = explode(';', $item);
                $name = isset($parts[0]) ? trim($parts[0]) : '';
            } elseif (is_array($item)) {
                $name = isset($item['name']) ? $item['name'] : '';
            } else {
                $name = '';
            }
            
            if (empty($name) || $name == '.' || $name == '..') continue;
            
            $size = 0;
            $mtime = time();
            $perm = '';
            $owner = 'www';
            if (is_string($item)) {
                $parts = explode(';', $item);
                if (isset($parts[1])) {
                    if (is_numeric($parts[1])) {
                        $size = intval($parts[1]);
                    } else {
                        $size = $this->parseSize($parts[1]);
                    }
                }
                if (isset($parts[2]) && is_numeric($parts[2])) $mtime = intval($parts[2]);
                if (isset($parts[3])) $perm = trim($parts[3]);
                if (isset($parts[4])) $owner = trim($parts[4]);
            } elseif (is_array($item)) {
                if (isset($item['size'])) {
                    if (is_numeric($item['size'])) {
                        $size = intval($item['size']);
                    } else {
                        $size = $this->parseSize($item['size']);
                    }
                }
                if (isset($item['mtime'])) $mtime = $item['mtime'];
                elseif (isset($item['time'])) $mtime = $item['time'];
                if (isset($item['access'])) $perm = $item['access'];
                if (isset($item['owner'])) $owner = $item['owner'];
            }
            
            $relative_path = $prefix . $name;
            if (strpos($relative_path, '//') !== false) {
                $relative_path = preg_replace('#/+#', '/', $relative_path);
            }
            
            
            $files[] = array(
                'name' => $name,
                'path' => $relative_path,
                'size' => 0,
                'modified' => date('Y-m-d H:i:s', $mtime),
                'mtime' => $mtime,
                'type' => 'dir',
                'is_dir' => true,
                'perm' => $perm,
                'owner' => $owner
            );
        }
        
        // 处理文件列表
        foreach ($file_items as $item) {
            if (is_string($item)) {
                $parts = explode(';', $item);
                $name = isset($parts[0]) ? trim($parts[0]) : '';
            } elseif (is_array($item)) {
                $name = isset($item['name']) ? $item['name'] : '';
            } else {
                $name = '';
            }
            
            if (empty($name) || $name == '.' || $name == '..') continue;
            
            $size = 0;
            $mtime = time();
            $perm = '';
            $owner = 'www';
            if (is_string($item)) {
                $parts = explode(';', $item);
                if (isset($parts[1]) && is_numeric($parts[1])) $size = intval($parts[1]);
                if (isset($parts[2]) && is_numeric($parts[2])) $mtime = intval($parts[2]);
                if (isset($parts[3])) $perm = trim($parts[3]);
                if (isset($parts[4])) $owner = trim($parts[4]);
            } elseif (is_array($item)) {
                if (isset($item['size']) && is_numeric($item['size'])) $size = intval($item['size']);
                if (isset($item['mtime'])) $mtime = $item['mtime'];
                elseif (isset($item['time'])) $mtime = $item['time'];
                if (isset($item['access'])) $perm = $item['access'];
                if (isset($item['owner'])) $owner = $item['owner'];
            }
            
            $relative_path = $prefix . $name;
            if (strpos($relative_path, '//') !== false) {
                $relative_path = preg_replace('#/+#', '/', $relative_path);
            }
            
            
            $files[] = array(
                'name' => $name,
                'path' => $relative_path,
                'size' => $size,
                'modified' => date('Y-m-d H:i:s', $mtime),
                'mtime' => $mtime,
                'type' => 'file',
                'is_dir' => false,
                'perm' => $perm,
                'owner' => $owner
            );
        }
        
        // 按类型和名称排序
        usort($files, function($a, $b) {
            if ($a['is_dir'] != $b['is_dir']) return $a['is_dir'] ? -1 : 1;
            return strcmp($a['name'], $b['name']);
        });
        
        // 合并备注（从本地存储读取）
        $remarks = $this->loadRemarks();
        foreach ($files as &$f) {
            $f['remark'] = isset($remarks[$f['path']]) ? $remarks[$f['path']] : '';
            if (!isset($f['owner'])) $f['owner'] = 'www';
        }
        unset($f);
        
        $total = count($files);
        if ($page !== null && $per_page !== null) {
            $offset = ($page - 1) * $per_page;
            $files = array_slice($files, $offset, $per_page);
        }
        
        $this->success(array('files' => $files, 'total' => $total));
    }
    
    /**
     * 备注存储文件路径
     */
    private function getRemarksFilePath() {
        $cache_dir = $this->getCacheDir();
        $dir = $cache_dir . '/file_remarks';
        if (!@is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        return $dir . '/remarks_' . md5($this->user_name) . '.json';
    }
    
    /**
     * 加载当前用户所有备注
     */
    private function loadRemarks() {
        $file = $this->getRemarksFilePath();
        if (!@file_exists($file)) return array();
        $data = @file_get_contents($file);
        if ($data === false) return array();
        $json = json_decode($data, true);
        return is_array($json) ? $json : array();
    }
    
    /**
     * 保存单条备注
     */
    public function saveRemark($path, $remark) {
        $path = $this->sanitizePath($path);
        $full_path = $this->site_root . $path;
        
        if (!$this->validatePath($full_path)) {
            $this->error('非法路径');
        }
        
        $rel = $path;
        if ($rel === '' || $rel === '/') {
            $this->error('非法路径');
        }
        $remarks = $this->loadRemarks();
        $remarks[$rel] = $remark;
        $file = $this->getRemarksFilePath();
        $dir = dirname($file);
        if (!@is_dir($dir)) @mkdir($dir, 0755, true);
        if (@file_put_contents($file, json_encode($remarks)) === false) {
            $this->error('保存备注失败');
        }
        $this->success(array('msg' => '保存成功'));
    }
    
    /**
     * 检查路径是否属于当前用户（已废弃，使用validatePath替代）
     */
    private function isUserPath($path) {
        return $this->validatePath($path);
    }
    
    /**
     * 检查文件是否是图片
     */
    private function isImageFile($filename) {
        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        return in_array($ext, $this->image_extensions);
    }
    
    /**
     * 检查文件是否是脚本文件（需要进行代码检测）
     */
    private function isScriptFile($filename) {
        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        return in_array($ext, array('php', 'php3', 'php4', 'php5', 'php7', 'phtml', 'asp', 'aspx', 'jsp', 'py', 'rb', 'pl'));
    }
    
    /**
     * 获取相对于用户根目录的路径
     */
    private function getRelativePath($full_path) {
        // 移除用户根目录前缀
        $relative = str_replace($this->site_root, '', $full_path);
        if (empty($relative)) $relative = '/';
        return $relative;
    }
    
    /**
     * 创建文件夹
     */
    public function createFolder($dir, $name) {
        
        // 安全清理路径
        $dir = $this->sanitizePath($dir);
        
        // 计算完整路径
        $full_path = $this->site_root . rtrim($dir, '/') . '/' . $name;
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        
        // 严格验证路径
        if (!$this->validatePath($full_path)) {
            $this->error('非法路径');
        }
        
        $result = $this->bt_api->CreateDirectory($full_path);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('创建文件夹', '创建文件夹: ' . $full_path);
            $this->success(array('msg' => '创建成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '创建失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 创建文件
     */
    public function createFile($dir, $name, $content) {
        
        // 安全清理路径
        $dir = $this->sanitizePath($dir);
        
        // 计算完整路径
        $full_path = $this->site_root . rtrim($dir, '/') . '/' . $name;
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        // 严格验证路径
        if (!$this->validatePath($full_path)) {
            $this->error('非法路径');
        }
        
        // 安全检查：验证文件类型
        if (!$this->isAllowedFile($name)) {
            $this->error('不允许创建此类型的文件');
        }
        
        // 安全检查：检查内容是否包含危险代码（在创建前检测）
        if (!empty($content) && $this->containsPHPCodeString($content)) {
            $this->error('文件内容包含危险代码（如eval、exec、system等），创建被拒绝');
        }
        
        $result = $this->bt_api->CreateFile($full_path);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            if ($content !== '' && $content !== null) {
                $save_result = $this->bt_api->SaveFileBody($full_path, $content);
            }
            $this->log('创建文件', '创建文件: ' . $full_path);
            $this->success(array('msg' => '创建成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '创建失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 检查字符串是否包含PHP代码
     * @param string $content 文件内容
     * @return bool 是否包含PHP代码
     */
    protected function containsPHPCodeString($content) {
        // 解码常见编码，防止绕过
        $decoded = $content;
        
        // Base64解码检测（最多解码3层）
        for ($i = 0; $i < 3; $i++) {
            $temp = base64_decode($decoded, true);
            if ($temp !== false && preg_match('/[a-zA-Z0-9\/\+]{20,}/', $decoded)) {
                $decoded = $temp;
            } else {
                break;
            }
        }
        
        // URL解码
        $decoded = urldecode($decoded);
        
        // 移除PHP注释，防止通过注释绕过
        $decoded = preg_replace('/\/\/.*$/m', '', $decoded);
        $decoded = preg_replace('/\/\*[\s\S]*?\*\//', '', $decoded);
        $decoded = preg_replace('/#.*$/m', '', $decoded);
        
        // 移除字符串中的空格，检测拼接绕过
        $no_space = preg_replace('/\s+/', '', $decoded);
        
        // 检测危险函数和模式（只检测真正危险的函数）
        $php_patterns = array(
            // ========== 直接代码执行（高危）==========
            '/eval\s*\(/i',
            '/assert\s*\(/i',
            '/preg_replace\s*\([\'"\/\s]*.*\/e/i',
            '/create_function\s*\(/i',
            
            // ========== 系统命令执行（高危）==========
            '/exec\s*\(/i',
            '/system\s*\(/i',
            '/shell_exec\s*\(/i',
            '/passthru\s*\(/i',
            '/proc_open\s*\(/i',
            '/popen\s*\(/i',
            '/pcntl_exec\s*\(/i',
            
            // ========== 反引号执行（高危）==========
            '/`[^`]+`/i',
            
            // ========== 反序列化漏洞（高危）==========
            '/unserialize\s*\(/i',
            
            // ========== 危险变量使用（高危）==========
            '/\$_(GET|POST|REQUEST)\s*\[[^\]]+\]\s*\(/i',
            
            // ========== 可变变量（高危）==========
            '/\$\$[a-zA-Z_]/i',
            '/\$\{[^}]+\}/i',
            
            // ========== 字符串拼接绕过检测（高危）==========
            '/\$[a-zA-Z_][a-zA-Z0-9_]*=[\'"][a-zA-Z]+[\'"];\$[a-zA-Z_][a-zA-Z0-9_]*=[\'"][a-zA-Z]+[\'"];\$[a-zA-Z_][a-zA-Z0-9_]*=\$[a-zA-Z_][a-zA-Z0-9_]*\.\$[a-zA-Z_][a-zA-Z0-9_]*;/i',
        );
        
        foreach ($php_patterns as $pattern) {
            if (preg_match($pattern, $decoded) || preg_match($pattern, $no_space)) {
                return true;
            }
        }
        
        // 检测通过$_REQUEST/$_GET/$_POST直接调用函数
        if (preg_match('/\$_(GET|POST|REQUEST)\s*\[[^\]]+\]\s*\(/i', $decoded)) {
            return true;
        }
        
        // 检测base64_decode后接函数调用
        if (preg_match('/base64_decode\s*\([^)]+\)\s*\(/i', $decoded)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * 上传文件
     */
    public function uploadFile($dir) {
        
        // 安全清理路径
        $dir = $this->sanitizePath($dir);
        
        // 计算基础路径
        $base_path = $this->site_root . rtrim($dir, '/');
        $base_path = preg_replace('#/{2,}#', '/', $base_path);
        
        // 获取相对路径（用于文件夹上传）
        $relative_path = isset($_POST['relativePath']) ? daddslashes($_POST['relativePath']) : '';
        
        
        // 构建最终保存路径
        if (!empty($relative_path)) {
            // 文件夹上传，保留目录结构
            $relative_path = $this->sanitizePath($relative_path);
            $relative_path = trim($relative_path, '/');
            $save_path = $base_path . '/' . dirname($relative_path);
            $filename = basename($relative_path);
        } else {
            $save_path = $base_path;
            $filename = '';
        }
        
        $save_path = preg_replace('#/{2,}#', '/', $save_path);
        
        
        // 验证目标路径是否在用户根目录下
        $real_save_path = realpath($save_path);
        $real_site_root = realpath($this->site_root);
        
        if ($real_save_path === false) {
            // 目录不存在，检查父目录
            $parent_dir = dirname($save_path);
            $real_parent = realpath($parent_dir);
            
            if ($real_parent === false || strpos($real_parent, $real_site_root) !== 0) {
                $this->error('非法路径');
            }
        } else {
            // 目录存在，检查是否在用户根目录下
            if (strpos($real_save_path, $real_site_root) !== 0) {
                $this->error('非法路径');
            }
        }
        
        // 如果目标目录不存在，尝试创建
        $bt_server = $this->getBtServer();
        $is_local = in_array($bt_server['ip'], ['127.0.0.1', 'localhost', $_SERVER['SERVER_ADDR'] ?? '']);
        
        if ($is_local) {
            // 本地服务器：创建本地目录
            if (!is_dir($save_path)) {
                if (!@mkdir($save_path, 0755, true)) {
                    $this->error('无法创建上传目录');
                }
            }
        } else {
            // 远程服务器：通过宝塔API创建目录
            $create_result = $this->bt_api->CreateDirectory($save_path);
        }
        
        if (!empty($_FILES) && isset($_FILES['files'])) {
            $file = !empty($filename) ? $filename : $_FILES['files']['name'][0];
            $tmp_name = $_FILES['files']['tmp_name'][0];
            $full_path = $save_path . '/' . $file;
            
            
            // 安全检查：验证文件类型
            if (!$this->isAllowedFile($file)) {
                $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
                $this->error('不允许上传 .' . $ext . ' 类型的文件。禁止上传可执行文件和系统文件');
            }
            
            // 安全检查：检查文件内容是否包含危险代码（仅对脚本文件，图片等二进制文件跳过）
            if ($this->isScriptFile($file)) {
                if ($this->containsPHPCode($tmp_name)) {
                    $this->error('文件内容包含危险的系统调用代码（如eval、exec、system等），上传被拒绝');
                }
            }
            
            // 调用宝塔API上传文件
            // UploadFile($path, $file) - 第一个参数是目标目录，第二个参数是本地文件路径
            $target_dir = dirname($full_path);
            
            $result = $this->bt_api->UploadFile($target_dir, $tmp_name);
            
            
            if ($result && isset($result['status']) && $result['status'] == true) {
                $this->log('上传文件', '上传文件: ' . $full_path);
                $this->success(array('msg' => '上传成功'));
            } else {
                $error_msg = isset($result['msg']) ? $result['msg'] : '上传失败';
                $this->error($error_msg);
            }
        } else {
            $this->error('没有上传文件');
        }
    }

    /**
     * 上传多个文件
     */
    public function uploadFiles($path, $files) {

        // 1. 安全检查和路径构建
        $path = $this->sanitizePath($path);
        $destination = $this->site_root . $path;
        $destination = rtrim($destination, '/');
        

        // 验证目标路径是否在用户根目录下
        // 使用字符串比较而不是realpath，避免目录不存在时的问题
        if (strpos($destination, $this->site_root) !== 0) {
            $this->error('非法上传路径');
        }
        
        // 如果目标目录不存在，尝试创建（仅对本地目录）
        // 对于远程宝塔服务器，跳过本地目录创建检查
        $bt_server = $this->getBtServer();
        $is_local = in_array($bt_server['ip'], ['127.0.0.1', 'localhost', $_SERVER['SERVER_ADDR'] ?? '']);
        
        if ($is_local) {
            if (!is_dir($destination)) {
                if (!@mkdir($destination, 0777, true)) {
                    $this->error('无法创建上传目录');
                }
            }
            
            // 检查并修复目录权限
            if (!is_writable($destination)) {
                @chmod($destination, 0777);
                if (!is_writable($destination)) {
                    $this->error('上传目录不可写');
                }
            }
        } else {
        }

        if (!isset($files['files']) || !is_array($files['files'])) {
            $this->error('没有文件被上传或上传格式不正确');
        }

        $upload_count = 0;
        $errors = [];

        // 检查 `name` 是否是数组，以确定是单个还是多个文件上传
        if (is_array($files['files']['name'])) {
            $file_count = count($files['files']['name']);
            for ($i = 0; $i < $file_count; $i++) {
                if ($files['files']['error'][$i] === UPLOAD_ERR_OK) {
                    $tmp_name = $files['files']['tmp_name'][$i];
                    $name = $files['files']['name'][$i];
                    $target_path = $destination . '/' . $name;

                    // 安全检查：验证文件类型
                    if (!$this->isAllowedFile($name)) {
                        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
                        $errors[] = "文件 '{$name}' 类型不允许上传";
                        continue;
                    }
                    
                    // 安全检查：检查文件内容是否包含危险代码（仅对脚本文件，图片等二进制文件跳过）
                    if ($this->isScriptFile($name)) {
                        if ($this->containsPHPCode($tmp_name)) {
                            $errors[] = "文件 '{$name}' 包含危险代码，上传被拒绝";
                            continue;
                        }
                    }

                    if ($is_local) {
                        // 本地服务器：直接移动到目标位置
                        if (move_uploaded_file($tmp_name, $target_path)) {
                            $this->bt_api->SetFileAccess($target_path, 'www', 644);
                            $this->log('上传文件', '上传文件: ' . $target_path);
                            $upload_count++;
                        } else {
                            $errors[] = "文件 '{$name}' 上传失败。";
                        }
                    } else {
                        // 远程服务器：使用宝塔API上传文件
                        $result = $this->bt_api->UploadFile($destination, $tmp_name, $name);
                        
                        if ($result && isset($result['status']) && $result['status'] == true) {
                            $this->log('上传文件', '上传文件: ' . $target_path);
                            $upload_count++;
                        } else {
                            $error_msg = isset($result['msg']) ? $result['msg'] : '上传失败';
                            $errors[] = "文件 '{$name}' 上传失败: " . $error_msg;
                        }
                    }
                } else {
                    $errors[] = "文件 '{$files['files']['name'][$i]}' 上传时发生错误，代码: " . $files['files']['error'][$i];
                }
            }
        } else {
            // 单个文件上传的情况
            if ($files['files']['error'] === UPLOAD_ERR_OK) {
                $tmp_name = $files['files']['tmp_name'];
                $name = $files['files']['name'];
                $target_path = $destination . '/' . $name;

                // 安全检查：验证文件类型
                if (!$this->isAllowedFile($name)) {
                    $errors[] = "文件 '{$name}' 类型不允许上传";
                } else if ($this->isScriptFile($name) && $this->containsPHPCode($tmp_name)) {
                    $errors[] = "文件 '{$name}' 包含危险代码，上传被拒绝";
                } else if ($is_local) {
                    // 本地服务器：直接移动
                    if (move_uploaded_file($tmp_name, $target_path)) {
                        $this->bt_api->SetFileAccess($target_path, 'www', 644);
                        $this->log('上传文件', '上传文件: ' . $target_path);
                        $upload_count++;
                    } else {
                        $errors[] = "文件 '{$name}' 上传失败。";
                    }
                } else {
                    // 远程服务器：使用宝塔API上传
                    $result = $this->bt_api->UploadFile($destination, $tmp_name, $name);
                    
                    if ($result && isset($result['status']) && $result['status'] == true) {
                        $this->log('上传文件', '上传文件: ' . $target_path);
                        $upload_count++;
                    } else {
                        $error_msg = isset($result['msg']) ? $result['msg'] : '上传失败';
                        $errors[] = "文件 '{$name}' 上传失败: " . $error_msg;
                    }
                }
            } else {
                $errors[] = "文件 '{$files['files']['name']}' 上传时发生错误，代码: " . $files['files']['error'];
            }
        }


        if ($upload_count > 0 && empty($errors)) {
            // 等待文件系统同步，确保宝塔API能够读取到新上传的文件
            sleep(1);
            $this->success(array('msg' => "成功上传 {$upload_count} 个文件。"));
        } elseif ($upload_count > 0) {
            sleep(1);
            $this->success(array('msg' => "成功上传 {$upload_count} 个文件，但有错误发生: " . implode(' ', $errors)));
        } else {
            $this->error('文件上传失败: ' . implode(' ', $errors));
        }
    }
    
    
    /**
     * 检查分片文件是否存在（用于断点续传）
     */
    public function checkChunkExists() {
        
        $filename = isset($_POST['filename']) ? $_POST['filename'] : '';
        if (empty($filename)) {
            $this->error('文件名不能为空');
        }
        
        // 安全检查
        $filename = str_replace('\\', '/', $filename);
        $filename = str_replace('..', '', $filename);
        
        // 构建完整路径
        $full_path = $this->site_root . $filename;
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        $result = $this->bt_api->CheckChunkExists($full_path);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->success($result);
        } else {
            $this->error('检查分片失败');
        }
    }
    
    /**
     * 分片上传文件
     */
    public function uploadChunk($dir) {
        
        // 安全检查
        $dir = str_replace('\\', '/', $dir);
        $dir = str_replace('..', '', $dir);
        
        // 计算完整路径
        if (strpos($dir, $this->site_root) === 0) {
            $save_path = rtrim($dir, '/');
        } else {
            $save_path = $this->site_root . rtrim($dir, '/');
        }
        
        // 验证
        if (strpos($save_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        // 获取分片参数
        $fileName = isset($_POST['fileName']) ? $_POST['fileName'] : '';
        $fileSize = isset($_POST['fileSize']) ? intval($_POST['fileSize']) : 0;
        $chunkIndex = isset($_POST['chunkIndex']) ? intval($_POST['chunkIndex']) : 0;
        $chunkSize = isset($_POST['chunkSize']) ? intval($_POST['chunkSize']) : 0;
        $fileType = isset($_POST['fileType']) ? $_POST['fileType'] : '';
        
        if (empty($fileName) || $fileSize <= 0) {
            $this->error('参数错误');
        }
        
        // 安全检查：验证文件类型
        if (!$this->isAllowedFile($fileName)) {
            $this->error('不允许上传此类型的文件');
        }
        
        if (empty($_FILES) || !isset($_FILES['chunk'])) {
            $this->error('没有上传文件块');
        }
        
        $tmp_name = $_FILES['chunk']['tmp_name'][0];
        $full_path = $save_path . '/' . $fileName;
        $chunkStart = $chunkIndex * $chunkSize;
        
        
        $result = $this->bt_api->UploadChunk($save_path, $tmp_name, $fileName, $fileSize, $chunkStart, $fileType);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('分片上传', '分片上传文件: ' . $full_path . ' (分片 ' . $chunkIndex . ')');
            $this->success(array(
                'msg' => '分片上传成功',
                'chunkIndex' => $chunkIndex,
                'fileName' => $fileName
            ));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '分片上传失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 获取文件内容
     */
    public function getFileContent($path) {
        
        // 安全检查
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . $path;
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        // 验证
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        $result = $this->bt_api->GetFileContent($full_path);
        // 宝塔 GetFileBody 返回 { status, data, encoding, ... }，内容在 data 字段
        if ($result !== false && (!is_array($result) || (isset($result['status']) && $result['status']))) {
            $content = is_array($result) && isset($result['data']) ? $result['data'] : (is_string($result) ? $result : '');
            $this->success(array('content' => $content));
        } else {
            $msg = is_array($result) && isset($result['msg']) ? $result['msg'] : '读取文件失败';
            $this->error($msg);
        }
    }
    
    /**
     * 保存文件
     */
    public function saveFile($path, $content) {
        
        // 安全检查
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . $path;
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        
        // 验证
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        // 安全检查：验证文件类型
        if (!$this->isAllowedFile(basename($full_path))) {
            $this->error('不允许保存此类型的文件');
        }
        
        // 安全检查：检查内容是否包含PHP代码
        if (!empty($content) && $this->containsPHPCodeString($content)) {
            $this->error('文件内容包含危险代码，保存被拒绝');
        }
        
        // 尝试使用宝塔API保存
        $result = $this->bt_api->SaveFileBody($full_path, $content);
        
        
        // 支持多种status格式：布尔true、字符串"true"、数字1
        $is_success = false;
        if ($result && isset($result['status'])) {
            $status = $result['status'];
            $is_success = ($status === true || $status === 'true' || $status === 1 || $status === '1');
        }
        
        // 如果宝塔API失败，尝试直接使用PHP写入
        if (!$is_success) {
            
            // 检查目录是否存在
            $dir = dirname($full_path);
            if (!is_dir($dir)) {
                $this->error('目录不存在: ' . $dir);
            }
            
            // 检查文件是否可写
            if (file_exists($full_path) && !is_writable($full_path)) {
                $this->error('文件没有写入权限: ' . $full_path);
            }
            
            // 尝试写入文件
            $write_result = file_put_contents($full_path, $content);
            
            if ($write_result !== false) {
                $this->log('保存文件', '保存文件(PHP): ' . $full_path);
                $this->success(array('msg' => '保存成功'));
                return;
            } else {
            }
        } else {
            $this->log('保存文件', '保存文件: ' . $full_path);
            $this->success(array('msg' => '保存成功'));
            return;
        }
        
        $error_msg = isset($result['msg']) ? $result['msg'] : (isset($result['error']) ? $result['error'] : '保存失败');
        $this->error($error_msg);
    }
    
    /**
     * 重命名文件
     */
    public function renameFile($path, $newName) {
        
        // 安全检查
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . $path;
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        $new_path = dirname($full_path) . '/' . $newName;
        
        $result = $this->bt_api->RenameFile($full_path, $new_path);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('重命名文件', '重命名: ' . $full_path . ' -> ' . $new_path);
            $this->success(array('msg' => '重命名成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '重命名失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 删除文件（支持软删除到回收站）
     * POST soft=1 时移动到回收站，否则真正删除
     */
    public function deleteFile($path) {
        
        // 安全检查
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        
        // 标准化路径：确保以/开头，但不要重复/
        if (empty($path) || $path === '/') {
            $this->error('路径不能为空');
        }
        
        // 确保路径以/开头
        if (strpos($path, '/') !== 0) {
            $path = '/' . $path;
        }
        
        // 移除重复的斜杠
        $path = preg_replace('#/+#', '/', $path);
        
        
        // 构建完整路径
        $full_path = $this->site_root . $path;
        $full_path = preg_replace('#/+#', '/', $full_path);
        
        
        // 验证路径是否在用户根目录下
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        // 检查文件是否存在
        // 对于远程服务器，直接使用宝塔API检查，不使用本地file_exists
        $bt_server = $this->getBtServer();
        $is_local = in_array($bt_server['ip'], ['127.0.0.1', 'localhost', $_SERVER['SERVER_ADDR'] ?? '']);
        
        if ($is_local) {
            // 本地服务器：使用本地file_exists检查
            $file_exists = @file_exists($full_path);
        } else {
            // 远程服务器：直接使用宝塔API检查
            $file_exists = true; // 假设文件存在，让宝塔API去验证
        }
        
        if (!$file_exists) {
            // 如果本地检查失败，尝试使用宝塔API检查
            $check_result = $this->bt_api->GetFileAttribute($full_path);
            if (!$check_result || (isset($check_result['status']) && !$check_result['status'])) {
                // 文件已经不存在，返回成功（可能是缓存问题）
                $this->success(array('msg' => '文件已不存在', 'already_deleted' => true));
            }
        }
        
        $soft = isset($_POST['soft']) && $_POST['soft'] == '1';
        if ($soft) {
            $this->moveToRecycleBin($full_path, $path);
            return;
        }
        
        // 判断是文件还是文件夹，调用不同的API
        $is_dir = @is_dir($full_path);
        
        if ($is_dir) {
            $result = $this->bt_api->DeleteDir($full_path);
        } else {
            $result = $this->bt_api->DeleteFile($full_path);
        }
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('删除文件', '删除: ' . $full_path);
            $this->success(array('msg' => '删除成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '删除失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 回收站目录与元数据文件
     */
    private function getRecycleBinPath() {
        return $this->site_root . '/.recycle_bin';
    }
    
    private function getRecycleMetaPath() {
        return $this->site_root . '/.recycle_bin/.meta.json';
    }
    
    private function loadRecycleMeta() {
        $file = $this->getRecycleMetaPath();
        $result = $this->bt_api->GetFileContent($file);
        if (!$result || (is_array($result) && isset($result['status']) && !$result['status'])) return array();
        $data = is_array($result) && isset($result['data']) ? $result['data'] : (is_string($result) ? $result : '');
        $json = json_decode($data, true);
        return is_array($json) ? $json : array();
    }
    
    private function saveRecycleMeta($meta) {
        $file = $this->getRecycleMetaPath();
        $result = $this->bt_api->SaveFileBody($file, json_encode($meta));
        if (!$result || !isset($result['status']) || !$result['status']) {
        }
    }
    
    /**
     * 移动到回收站
     */
    private function moveToRecycleBin($full_path, $rel_path) {
        $recycle_dir = $this->getRecycleBinPath();
        $result = $this->bt_api->CreateDirectory($recycle_dir);
        if (!$result || !isset($result['status']) || !$result['status']) {
            $this->error('无法创建回收站');
        }
        $unique = md5($rel_path . microtime(true)) . '_' . basename($full_path);
        $dest = $recycle_dir . '/' . $unique;
        $move = $this->bt_api->MoveFile($full_path, $dest);
        if (!$move || !isset($move['status']) || !$move['status']) {
            $this->error(isset($move['msg']) ? $move['msg'] : '移至回收站失败');
        }
        $meta = $this->loadRecycleMeta();
        $meta[$unique] = array('path' => $rel_path, 'time' => time());
        $this->saveRecycleMeta($meta);
        $this->log('移至回收站', $rel_path);
        $this->success(array('msg' => '已移至回收站'));
    }
    
    /**
     * 获取回收站列表
     */
    public function getRecycleBinList() {
        $recycle_dir = $this->getRecycleBinPath();
        $result = $this->bt_api->GetHostDirectory($recycle_dir);
        if (!$result || !isset($result['DIR']) && !isset($result['FILES'])) {
            $this->success(array('list' => array()));
            return;
        }
        $meta = $this->loadRecycleMeta();
        $list = array();
        $dirs = isset($result['DIR']) ? $result['DIR'] : array();
        $files = isset($result['FILES']) ? $result['FILES'] : array();
        foreach (array_merge($dirs, $files) as $item) {
            $name = is_array($item) ? (isset($item['name']) ? $item['name'] : '') : (is_string($item) ? trim(explode(';', $item)[0]) : '');
            if ($name === '' || $name === '.' || $name === '..' || $name === '.meta.json') continue;
            $info = isset($meta[$name]) ? $meta[$name] : array('path' => '?', 'time' => 0);
            $list[] = array(
                'name' => $name,
                'original_path' => $info['path'],
                'deleted_time' => $info['time']
            );
        }
        $this->success(array('list' => $list));
    }
    
    /**
     * 从回收站恢复
     */
    public function restoreFromRecycleBin($recycle_name) {
        $recycle_name = basename(str_replace('..', '', $recycle_name));
        $recycle_dir = $this->getRecycleBinPath();
        $meta = $this->loadRecycleMeta();
        if (!isset($meta[$recycle_name])) {
            $this->error('回收站项不存在');
        }
        $original = $meta[$recycle_name]['path'];
        $full_src = $recycle_dir . '/' . $recycle_name;
        $full_dest = $this->site_root . (strpos($original, '/') === 0 ? $original : '/' . $original);
        $result = $this->bt_api->MoveFile($full_src, $full_dest);
        if (!$result || !isset($result['status']) || !$result['status']) {
            $this->error(isset($result['msg']) ? $result['msg'] : '恢复失败');
        }
        unset($meta[$recycle_name]);
        $this->saveRecycleMeta($meta);
        $this->success(array('msg' => '已恢复'));
    }
    
    /**
     * 清空回收站
     */
    public function emptyRecycleBin() {
        $recycle_dir = $this->getRecycleBinPath();
        $result = $this->bt_api->GetHostDirectory($recycle_dir);
        if (!$result) {
            $this->success(array('msg' => '已清空'));
            return;
        }
        $meta = $this->loadRecycleMeta();
        $to_delete = array_keys($meta);
        foreach ($to_delete as $name) {
            $full = $recycle_dir . '/' . $name;
            $this->bt_api->DeleteFile($full);
        }
        $this->saveRecycleMeta(array());
        $this->success(array('msg' => '回收站已清空'));
    }
    
    /**
     * 批量删除文件
     */
    public function deleteFiles($files) {
        if (!is_array($files) || empty($files)) {
            $this->error('没有选择文件');
        }
        
        $success = 0;
        $failed = 0;
        
        foreach ($files as $path) {
            // 安全检查
            $path = str_replace('\\', '/', $path);
            $path = str_replace('..', '', $path);
            
            if (strpos($path, $this->site_root) === 0) {
                $full_path = $path;
            } else {
                $full_path = $this->site_root . $path;
            }
            $full_path = preg_replace('#/{2,}#', '/', $full_path);
            
            // 验证
            if (strpos($full_path, $this->site_root) !== 0) {
                $failed++;
                continue;
            }
            
            $result = $this->bt_api->DeleteFile($full_path);
            
            if ($result && isset($result['status']) && $result['status'] == true) {
                $success++;
            } else {
                $failed++;
            }
        }
        
        $this->log('批量删除', '成功: ' . $success . ', 失败: ' . $failed);
        $this->success(array('msg' => '删除成功: ' . $success . ', 失败: ' . $failed));
    }
    
    /**
     * 下载文件
     * 使用宝塔的直接下载接口 /download?filename=
     * 参考备份下载的实现方式
     */
    public function downloadFile($path) {
        
        // 安全检查
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . $path;
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        
        // 验证
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        // 注意：文件在远程宝塔服务器上，不在本地，不能使用 file_exists 检查
        // 使用宝塔API获取下载URL
        $result = $this->bt_api->DownloadFile($full_path);
        
        
        if ($result && isset($result['status']) && $result['status']) {
            // 如果返回了下载URL，使用代理方式下载
            if (isset($result['url'])) {
                $this->proxyDownload($result['url'], basename($full_path));
                exit;
            }
        }
        
        $this->error('下载失败，无法获取文件');
    }
    
    /**
     * 代理下载文件（从宝塔服务器下载并转发给用户）
     */
    private function proxyDownload($url, $filename) {
        
        // 直接使用宝塔返回的URL（已经包含认证信息）
        $download_url = $url;
        
        // 获取文件内容
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $download_url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 300);
        
        $content = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $content_length = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
        $error = curl_error($ch);
        curl_close($ch);
        
        if ($error) {
            $this->error('下载失败: ' . $error);
        }
        
        if ($http_code != 200) {
            $this->error('下载失败，HTTP状态码: ' . $http_code);
        }
        
        
        // 设置下载头并输出内容
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Content-Length: ' . strlen($content));
        header('Cache-Control: no-cache, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: 0');
        
        echo $content;
        exit;
    }
    
    /**
     * 复制文件
     */
    public function copyFile($source, $target) {
        
        // 安全检查
        $source = str_replace('\\', '/', $source);
        $source = str_replace('..', '', $source);
        $target = str_replace('\\', '/', $target);
        $target = str_replace('..', '', $target);
        
        if (strpos($source, $this->site_root) === 0) {
            $source_path = $source;
        } else {
            $source_path = $this->site_root . $source;
        }
        $source_path = preg_replace('#/{2,}#', '/', $source_path);
        
        if (strpos($target, $this->site_root) === 0) {
            $target_path = $target;
        } else {
            $target_path = $this->site_root . $target;
        }
        $target_path = preg_replace('#/{2,}#', '/', $target_path);
        
        // 验证
        if (strpos($source_path, $this->site_root) !== 0 || strpos($target_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        $result = $this->bt_api->CopyFile($source_path, $target_path);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('复制文件', '复制: ' . $source_path . ' -> ' . $target_path);
            $this->success(array('msg' => '复制成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '复制失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 移动文件
     */
    public function moveFile($source, $target) {
        
        // 安全检查
        $source = str_replace('\\', '/', $source);
        $source = str_replace('..', '', $source);
        $target = str_replace('\\', '/', $target);
        $target = str_replace('..', '', $target);
        
        if (strpos($source, $this->site_root) === 0) {
            $source_path = $source;
        } else {
            $source_path = $this->site_root . $source;
        }
        $source_path = preg_replace('#/{2,}#', '/', $source_path);
        
        if (strpos($target, $this->site_root) === 0) {
            $target_path = $target;
        } else {
            $target_path = $this->site_root . $target;
        }
        $target_path = preg_replace('#/{2,}#', '/', $target_path);
        
        // 验证
        if (strpos($source_path, $this->site_root) !== 0 || strpos($target_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        $result = $this->bt_api->MoveFile($source_path, $target_path);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('移动文件', '移动: ' . $source_path . ' -> ' . $target_path);
            $this->success(array('msg' => '移动成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '移动失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 修改权限
     */
    public function chmodFile($path, $mode, $owner = 'www', $recursive = 0) {
        
        // 安全清理路径
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        // 计算完整路径
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . $path;
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        
        // 验证路径是否在用户根目录下（使用字符串比较，因为文件在远程服务器）
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        // 验证权限值
        if (!preg_match('/^\d{3,4}$/', $mode)) {
            $this->error('无效的权限值');
        }
        
        // 验证所有者 - 只允许www用户
        if ($owner !== 'www') {
            $this->error('只允许设置 www 用户为所有者');
        }
        
        // 确保mode是整数类型
        $mode = intval($mode);
        
        
        // 先设置主目录权限
        $result = $this->bt_api->SetFileAccess($full_path, $mode, $owner, 'False');
        
        if (!$result || !isset($result['status']) || $result['status'] != true) {
            $error_msg = isset($result['msg']) ? $result['msg'] : '修改失败';
            $this->error($error_msg);
            return;
        }
        
        // 如果需要递归设置子目录和文件
        if ($recursive) {
            $this->chmodRecursiveRemote($full_path, $mode, $owner);
        }
        
        $this->log('修改权限', '修改权限: ' . $full_path . ' -> ' . $mode . ' (所有者: ' . $owner . ', 递归: ' . ($recursive ? '是' : '否') . ')');
        $this->success(array('msg' => '权限修改成功'));
    }
    
    /**
     * 递归修改权限（远程服务器版本）
     * 使用宝塔API获取文件列表并逐个设置权限
     */
    private function chmodRecursiveRemote($path, $mode, $owner) {
        
        // 使用宝塔API获取目录内容
        $result = $this->bt_api->GetDir($path);
        
        if (!$result || !isset($result['DIR']) || !is_array($result['DIR'])) {
            return;
        }
        
        $count = 0;
        
        // 处理目录中的文件和子目录
        // GetDir返回的DIR数组格式: "name;size;time;chmod;chown;group;isdir;..."
        foreach ($result['DIR'] as $item) {
            // 解析分号分隔的字符串
            $parts = explode(';', $item);
            if (count($parts) < 8) continue;
            
            $item_name = $parts[0];
            $item_path = $path . '/' . $item_name;
            $is_dir = ($parts[6] == '1'); // isdir字段
            
            // 跳过特殊目录
            if ($item_name === '.' || $item_name === '..') continue;
            
            // 设置权限
            $this->bt_api->SetFileAccess($item_path, $mode, $owner, 'False');
            $count++;
            
            // 如果是子目录，递归处理
            if ($is_dir) {
                $this->chmodRecursiveRemote($item_path, $mode, $owner);
            }
            
            // 限制处理数量，避免超时
            if ($count >= 500) {
                break;
            }
        }
        
        // 处理FILES数组（文件）
        if (isset($result['FILES']) && is_array($result['FILES'])) {
            foreach ($result['FILES'] as $item) {
                $parts = explode(';', $item);
                if (count($parts) < 8) continue;
                
                $item_name = $parts[0];
                $item_path = $path . '/' . $item_name;
                
                // 设置权限
                $this->bt_api->SetFileAccess($item_path, $mode, $owner, 'False');
                $count++;
                
                // 限制处理数量，避免超时
                if ($count >= 500) {
                    break;
                }
            }
        }
        
    }
    
    /**
     * 递归修改权限
     */
    private function chmodRecursive($path, $mode, $owner) {
        
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );
        
        $count = 0;
        foreach ($iterator as $file) {
            $file_path = $file->getPathname();
            // SetFileAccess只接受path和access两个参数
            $this->bt_api->SetFileAccess($file_path, $mode);
            $count++;
            
            // 限制递归文件数量，避免超时
            if ($count >= 1000) {
                break;
            }
        }
        
    }
    
    /**
     * URL链接下载文件
     * 使用宝塔API的RemoteDownload接口
     */
    public function downloadFromUrl($urls, $path, $filename = '') {
        
        // 安全检查
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        // 构建完整路径
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . $path;
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        // 验证路径
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        $success_count = 0;
        $failed_urls = [];
        
        foreach ($urls as $url) {
            $url = trim($url);
            if (empty($url)) continue;
            
            // 验证URL
            if (!filter_var($url, FILTER_VALIDATE_URL)) {
                $failed_urls[] = $url . ' (无效URL)';
                continue;
            }
            
            // 获取文件名
            if (empty($filename)) {
                // 从URL中提取文件名
                $parsed_url = parse_url($url);
                $basename = basename($parsed_url['path'] ?? '');
                if (empty($basename) || strpos($basename, '.') === false) {
                    $basename = 'download_' . time() . '_' . rand(1000, 9999);
                }
                $save_filename = $basename;
            } else {
                $save_filename = $filename;
                // 如果有多个URL，添加序号
                if (count($urls) > 1) {
                    $info = pathinfo($save_filename);
                    $ext = isset($info['extension']) ? '.' . $info['extension'] : '';
                    $save_filename = $info['filename'] . '_' . ($success_count + 1) . $ext;
                }
            }
            
            // 构建保存路径（包含文件名）
            $save_path = $full_path . '/' . $save_filename;
            $save_path = preg_replace('#/{2,}#', '/', $save_path);
            
            
            // 使用宝塔API下载文件（传递目录和文件名）
            $result = $this->bt_api->RemoteDownload($url, $save_path, $save_filename);
            
            if ($result && isset($result['status']) && $result['status'] === true) {
                $success_count++;
                $this->log('URL下载', '下载文件: ' . $url . ' -> ' . $save_path);
            } else {
                $error_msg = isset($result['msg']) ? $result['msg'] : '下载失败';
                $failed_urls[] = $url . ' (' . $error_msg . ')';
            }
        }
        
        
        if ($success_count > 0) {
            if (count($failed_urls) > 0) {
                $this->success([
                    'msg' => '成功下载 ' . $success_count . ' 个文件，' . count($failed_urls) . ' 个失败',
                    'failed' => $failed_urls
                ]);
            } else {
                $this->success(['msg' => '成功下载 ' . $success_count . ' 个文件']);
            }
        } else {
            $this->error('所有文件下载失败: ' . implode(', ', $failed_urls));
        }
    }
    
    /**
     * 压缩文件
     * 使用宝塔API的Zip接口
     */
    public function zipFiles($dir, $files, $zipname = '') {
        
        // 安全检查
        $dir = str_replace('\\', '/', $dir);
        $dir = str_replace('..', '', $dir);
        
        if (strpos($dir, $this->site_root) === 0) {
            $path = rtrim($dir, '/');
        } else {
            $path = $this->site_root . rtrim($dir, '/');
        }
        
        // 验证
        if (strpos($path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        // 处理文件名
        if (empty($zipname)) {
            $zipname = 'archive_' . date('YmdHis') . '.zip';
        }
        
        // 确保文件名安全
        $zipname = basename($zipname);
        if (empty($zipname) || $zipname === '.' || $zipname === '..') {
            $zipname = 'archive_' . date('YmdHis') . '.zip';
        }
        
        // 确保有扩展名
        if (!preg_match('/\.(zip|tar\.gz|tar)$/i', $zipname)) {
            $zipname .= '.zip';
        }
        
        $dfile = $path . '/' . $zipname;
        
        
        // 构建要压缩的文件列表（逗号分隔，这是宝塔API需要的格式）
        $file_list = array();
        foreach ($files as $file) {
            // 安全检查
            $file = str_replace('..', '', $file);
            $file = str_replace('\\', '/', $file);
            $file = ltrim($file, '/');
            $file_list[] = $file;
        }
        $sfile = implode(',', $file_list);
        
        
        // 调用宝塔API的Zip方法
        $result = $this->bt_api->Zip($path, $sfile, $dfile);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('压缩文件', '压缩 ' . count($files) . ' 个项目到: ' . $dfile);
            $this->success(array('msg' => '压缩成功', 'filename' => $zipname));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '压缩失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 解压文件
     * @param string $dir 源文件所在目录
     * @param string $file 要解压的文件名
     * @param string $target_dir 解压目标目录（可选，默认为源文件所在目录）
     */
    public function unzipFile($dir, $file, $target_dir = '') {

        // 安全清理路径
        $dir = $this->sanitizePath($dir);
        $target_dir = $this->sanitizePath($target_dir);

        // 如果没有指定目标目录，则使用源文件所在目录
        if (empty($target_dir)) {
            $target_dir = $dir;
        }

        // 计算完整路径
        $source_path = $this->site_root . $dir;
        $source_path = preg_replace('#/{2,}#', '/', $source_path);
        $source_path = rtrim($source_path, '/');

        $target_path = $this->site_root . $target_dir;
        $target_path = preg_replace('#/{2,}#', '/', $target_path);
        $target_path = rtrim($target_path, '/');


        // 验证路径是否在用户根目录下
        // 对于远程服务器，使用字符串比较而不是realpath
        $bt_server = $this->getBtServer();
        $is_local = in_array($bt_server['ip'], ['127.0.0.1', 'localhost', $_SERVER['SERVER_ADDR'] ?? '']);

        // 验证源路径
        if ($is_local) {
            $real_source_path = realpath($source_path);
            $real_site_root = realpath($this->site_root);

            if ($real_source_path === false) {
                $parent_dir = dirname($source_path);
                $real_parent = realpath($parent_dir);
                if ($real_parent === false || strpos($real_parent, $real_site_root) !== 0) {
                    $this->error('非法路径');
                }
            } else {
                if (strpos($real_source_path, $real_site_root) !== 0) {
                    $this->error('非法路径');
                }
            }
        } else {
            if (strpos($source_path, $this->site_root) !== 0) {
                $this->error('非法路径');
            }
        }

        // 验证目标路径
        if (strpos($target_path, $this->site_root) !== 0) {
            $this->error('非法目标路径');
        }

        // 验证文件是否在允许列表中
        if (!$this->isAllowedFile($file)) {
            $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
            $this->error('不允许解压 .' . $ext . ' 类型的文件');
        }

        // 验证ZIP文件完整性
        $zip_file_path = $source_path . '/' . $file;

        if ($is_local) {
            if (!file_exists($zip_file_path)) {
                $this->error('压缩文件不存在');
            }

            $fp = fopen($zip_file_path, 'rb');
            if ($fp) {
                $header = fread($fp, 4);
                fclose($fp);

                if ($header !== "PK\x03\x04" && $header !== "PK\x05\x06" && $header !== "PK\x07\x08") {
                    $this->error('压缩文件格式无效或已损坏，请重新上传');
                }
            }
        } else {
        }

        // 调用宝塔API解压，传入源文件路径和目标目录
        // sfile: 源压缩文件完整路径
        // dfile: 解压目标目录
        $result = $this->bt_api->UnZipFile($zip_file_path, $target_path);

        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('解压文件', '解压: ' . $zip_file_path . ' 到: ' . $target_path);
            $this->success(array('msg' => '解压成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '解压失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 远程下载
     */
    public function remoteDownload($url, $path) {
        
        // 安全检查
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        if (strpos($path, $this->site_root) === 0) {
            $save_path = rtrim($path, '/');
        } else {
            $save_path = $this->site_root . rtrim($path, '/');
        }
        
        // 验证
        if (strpos($save_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        $result = $this->bt_api->RemoteDownload($url, $save_path);
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('远程下载', '下载: ' . $url . ' -> ' . $save_path);
            $this->success(array('msg' => '下载成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '下载失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 搜索文件
     */
    public function searchFiles($path, $keyword) {

        $include_subdir = isset($_POST['include_subdir']) ? intval($_POST['include_subdir']) : 1;
        $suffix = isset($_POST['suffix']) ? trim((string)$_POST['suffix']) : '';
        $mtime_days = isset($_POST['mtime_days']) ? intval($_POST['mtime_days']) : 0;
        $size_max = isset($_POST['size_max']) ? intval($_POST['size_max']) : 0;
        $ignore_case = isset($_POST['ignore_case']) ? intval($_POST['ignore_case']) : 0;
        $match_case = isset($_POST['match_case']) ? intval($_POST['match_case']) : 0;
        $whole_word = isset($_POST['whole_word']) ? intval($_POST['whole_word']) : 0;
        $regex = isset($_POST['regex']) ? intval($_POST['regex']) : 0;

        $keyword = trim((string)$keyword);
        if ($keyword === '') {
            $this->success(array('files' => array()));
        }
        if ($ignore_case) $match_case = 0;
        
        // 安全检查
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . $path;
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        // 验证
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        $default_suffixes = array();
        if ($suffix !== '') {
            $parts = preg_split('/[\s,]+/', $suffix, -1, PREG_SPLIT_NO_EMPTY);
            foreach ($parts as $p) {
                $p = strtolower(trim($p));
                $p = ltrim($p, '.');
                if ($p !== '') $default_suffixes[$p] = true;
            }
        }

        $time_min = 0;
        if ($mtime_days > 0) {
            $time_min = time() - ($mtime_days * 86400);
        }

        // 定义匹配函数
        $matchName = function($name) use ($keyword, $match_case, $whole_word, $regex) {
            $name = (string)$name;
            if ($regex) {
                $mod = $match_case ? 'u' : 'iu';
                $pattern = '/' . $keyword . '/' . $mod;
                $ok = @preg_match($pattern, $name);
                if ($ok === false) {
                    return array(false, '正则表达式错误');
                }
                return array($ok === 1, null);
            }
            if ($whole_word) {
                $quoted = preg_quote($keyword, '/');
                $mod = $match_case ? 'u' : 'iu';
                $pattern = '/(?<![A-Za-z0-9_])' . $quoted . '(?![A-Za-z0-9_])/' . $mod;
                $ok = @preg_match($pattern, $name);
                if ($ok === false) {
                    return array(false, '匹配规则错误');
                }
                return array($ok === 1, null);
            }
            if ($match_case) return array(strpos($name, $keyword) !== false, null);
            return array(stripos($name, $keyword) !== false, null);
        };

        if (@is_dir($full_path) && @is_readable($full_path)) {
            $deadline = microtime(true) + 3.0;
            $max_scan = 8000;
            $max_match = 500;
            $scanned = 0;
            $limit_reached = false;
            $out = array();

            $base = rtrim($this->site_root, '/');
            $baseLen = strlen($base);

            $iter = null;
            try {
                if ($include_subdir) {
                    $iter = new RecursiveIteratorIterator(
                        new RecursiveDirectoryIterator($full_path, FilesystemIterator::SKIP_DOTS),
                        RecursiveIteratorIterator::SELF_FIRST
                    );
                } else {
                    $iter = new DirectoryIterator($full_path);
                }
            } catch (Exception $e) {
                $iter = null;
            }

            if ($iter !== null) {
                foreach ($iter as $f) {
                    if (microtime(true) > $deadline) { $limit_reached = true; break; }
                    if ($scanned >= $max_scan) { $limit_reached = true; break; }
                    $scanned++;

                    $name = $f->getFilename();
                    if ($name === '.' || $name === '..') continue;

                    $isDir = $f->isDir();
                    $full = $f->getPathname();
                    $full = str_replace('\\', '/', $full);
                    if (strpos($full, $base) !== 0) continue;

                    $mtime = 0;
                    $size = 0;
                    if (!$isDir) {
                        $size = intval(@filesize($full));
                    }
                    $mtime = intval(@filemtime($full));
                    if ($time_min > 0 && $mtime > 0 && $mtime < $time_min) continue;
                    if (!$isDir && $size_max > 0 && $size > $size_max) continue;

                    if (!empty($default_suffixes) && !$isDir) {
                        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
                        if (!isset($default_suffixes[$ext])) continue;
                    }

                    list($ok, $err) = $matchName($name);
                    if ($err) $this->error($err);
                    if (!$ok) continue;

                    $rel = substr($full, $baseLen);
                    $rel = preg_replace('#^/+#', '/', $rel);
                    $out[] = array(
                        'name' => $name,
                        'path' => $rel,
                        'size' => $isDir ? 0 : $size,
                        'mtime' => $mtime,
                        'modified' => $mtime ? date('Y-m-d H:i:s', $mtime) : '',
                        'type' => $isDir ? 'dir' : 'file',
                        'is_dir' => $isDir
                    );
                    if (count($out) >= $max_match) { $limit_reached = true; break; }
                }
            }

            $this->success(array('files' => $out, 'checked' => $scanned, 'limit_reached' => $limit_reached));
            return;
        }

        $parseFileItem = function($item) {
            $name = '';
            $size = 0;
            $mtime = 0;
            if (is_string($item)) {
                $parts = explode(';', $item);
                $name = isset($parts[0]) ? trim($parts[0]) : '';
                $size = isset($parts[1]) ? intval($parts[1]) : 0;
                $mtime = isset($parts[2]) ? intval($parts[2]) : 0;
            } elseif (is_array($item)) {
                $name = isset($item['name']) ? (string)$item['name'] : '';
                $size = isset($item['size']) ? intval($item['size']) : 0;
                $mtime = isset($item['mtime']) ? intval($item['mtime']) : (isset($item['time']) ? intval($item['time']) : 0);
            }
            return array($name, $size, $mtime);
        };

        $parseDirItem = function($item) {
            if (is_string($item)) {
                $parts = explode(';', $item);
                return isset($parts[0]) ? trim($parts[0]) : '';
            }
            if (is_array($item) && isset($item['name'])) {
                return trim((string)$item['name']);
            }
            return '';
        };

        $base_rel = str_replace($this->site_root, '', $full_path);
        $base_rel = preg_replace('#^/+#', '/', $base_rel);
        if ($base_rel === '') $base_rel = '/';
        if (substr($base_rel, -1) !== '/') $base_rel .= '/';

        $files = array();
        $scanned = 0;
        $max_scan = 3000;
        $max_match = 500;
        $stack = array(array('full' => $full_path, 'rel' => $base_rel));
        while (!empty($stack)) {
            $node = array_pop($stack);
            $dir_full = $node['full'];
            $dir_rel = $node['rel'];

            $res = $this->bt_api->GetHostDirectory($dir_full);
            if (!$res || (!isset($res['FILES']) && !isset($res['DIR']))) continue;

            $dir_items = isset($res['DIR']) ? $res['DIR'] : array();
            foreach ($dir_items as $d) {
                if ($scanned >= $max_scan || count($files) >= $max_match) break 2;
                $scanned++;
                $dname = trim($parseDirItem($d));
                if ($dname === '' || $dname === '.' || $dname === '..') continue;

                $d_mtime = 0;
                if (is_string($d)) {
                    $parts = explode(';', $d);
                    if (isset($parts[2]) && is_numeric($parts[2])) $d_mtime = intval($parts[2]);
                } elseif (is_array($d)) {
                    if (isset($d['mtime'])) $d_mtime = intval($d['mtime']);
                    elseif (isset($d['time'])) $d_mtime = intval($d['time']);
                }
                if ($time_min > 0 && $d_mtime > 0 && $d_mtime < $time_min) {
                    continue;
                }

                list($ok, $err) = $matchName($dname);
                if ($err) $this->error($err);
                if ($ok) {
                    $rel_path = preg_replace('#/+#', '/', $dir_rel . $dname);
                    $files[] = array(
                        'name' => $dname,
                        'path' => $rel_path,
                        'size' => 0,
                        'mtime' => $d_mtime,
                        'modified' => $d_mtime ? date('Y-m-d H:i:s', $d_mtime) : '',
                        'type' => 'dir',
                        'is_dir' => true
                    );
                }
            }

            $file_items = isset($res['FILES']) ? $res['FILES'] : array();
            foreach ($file_items as $it) {
                if ($scanned >= $max_scan || count($files) >= $max_match) break 2;
                $scanned++;
                list($name, $size, $mtime) = $parseFileItem($it);
                $name = trim($name);
                if ($name === '') continue;

                if (!empty($default_suffixes)) {
                    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
                    if (!isset($default_suffixes[$ext])) continue;
                }
                if ($size_max > 0 && $size > $size_max) continue;
                if ($time_min > 0 && $mtime > 0 && $mtime < $time_min) continue;

                list($ok, $err) = $matchName($name);
                if ($err) $this->error($err);
                if (!$ok) continue;

                $rel_path = preg_replace('#/+#', '/', $dir_rel . $name);
                $files[] = array(
                    'name' => $name,
                    'path' => $rel_path,
                    'size' => $size,
                    'mtime' => $mtime,
                    'modified' => $mtime ? date('Y-m-d H:i:s', $mtime) : '',
                    'type' => 'file',
                    'is_dir' => false
                );
            }

            if (!$include_subdir) continue;
            foreach ($dir_items as $d) {
                $dname = trim($parseDirItem($d));
                if ($dname === '' || $dname === '.' || $dname === '..') continue;
                if ($scanned >= $max_scan || count($files) >= $max_match) break 2;
                $next_full = preg_replace('#/{2,}#', '/', rtrim($dir_full, '/') . '/' . $dname);
                $next_rel = preg_replace('#/+#', '/', rtrim($dir_rel, '/') . '/' . $dname . '/');
                $stack[] = array('full' => $next_full, 'rel' => $next_rel);
            }
        }

        $this->success(array('files' => $files));
    }
    


    /**
     * 文件内容搜索（在当前目录及子目录的文本文件中搜索关键词，最多检查约50个文件）
     */
    private function _searchWithShell($path, $keyword, $match_case, $whole_word, $regex, $suffixes) {
        
        // 1. 路径安全检查
        $real_path = realpath($path);
        
        if ($real_path === false || strpos($real_path, $this->site_root) !== 0) {
            return array(false, '非法搜索路径');
        }

        $is_windows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
        $cmd = '';

        if ($is_windows) {
            // Windows: 使用 findstr
            $cmd_parts = ['findstr'];
            // 参数: /S 递归, /N 行号, /P 跳过非打印字符
            $cmd_parts[] = '/S /N /P';
            if (!$match_case) {
                $cmd_parts[] = '/I';
            }
            
            $search_str = $keyword;
            if ($regex) {
                $cmd_parts[] = '/R';
            } else {
                // 对于字面量搜索，使用 /C:"keyword" 格式更安全
                $search_str = '/C:"' . str_replace('"', '""', $keyword) . '"';
            }
            $cmd_parts[] = $search_str;

            // 构建文件路径. findstr 支持 "path\*.ext" "path\*.ext2"
            if (!empty($suffixes)) {
                $path_specs = [];
                foreach ($suffixes as $s => $val) {
                    $path_specs[] = escapeshellarg($real_path . '\\*.' . $s);
                }
                $cmd_parts[] = implode(' ', $path_specs);
            } else {
                $cmd_parts[] = escapeshellarg($real_path . '\\*.*');
            }
            $cmd = implode(' ', $cmd_parts);

        } else {
            // Linux/Unix: 使用 grep
            $cmd_parts = ['grep'];
            $cmd_parts[] = '-R';  // 递归搜索
            $cmd_parts[] = '-n';  // 显示行号
            $cmd_parts[] = '-I';  // 跳过二进制文件
            
            if (!$match_case) {
                $cmd_parts[] = '-i';  // 忽略大小写
            }
            if ($whole_word) {
                $cmd_parts[] = '-w';  // 全词匹配
            }
            if (!$regex) {
                $cmd_parts[] = '-F';  // 字面匹配（非正则）
            }
            
            // 添加文件后缀过滤器
            if (!empty($suffixes)) {
                foreach ($suffixes as $s => $val) {
                    // 使用 --include 选项过滤文件类型
                    $cmd_parts[] = '--include=' . escapeshellarg('*.' . $s);
                }
            }
            
            // 添加搜索关键词和路径
            $cmd_parts[] = escapeshellarg($keyword);
            $cmd_parts[] = escapeshellarg($real_path);
            
            $cmd = implode(' ', $cmd_parts);
        }


        // 3. 执行命令
        $result = $this->bt_api->ExecShell($cmd);
        

        // 4. 处理结果
        if (!$result || !isset($result['status'])) {
            return array(false, '执行命令失败，没有返回状态');
        }
        
        $output = isset($result['success']) ? $result['success'] : '';
        $error_output = isset($result['error']) ? $result['error'] : '';

        // status=false不一定代表失败。Grep无匹配时exit 1, findstr无匹配时exit 1
        if (!$result['status']) {
            // 如果有明确的错误信息（不是空），并且没有成功信息，则认为是错误
            if (!empty($error_output) && empty($output)) {
                 return array(false, '搜索执行出错: ' . $error_output);
            }
        }

        return array(true, $output);
    }

    public function searchFileContent($path, $keyword) {
        
        // 参数解析：支持字符串'true'/'false'和数字1/0两种格式
        $match_case = false;
        if (isset($_POST['match_case'])) {
            $mc = $_POST['match_case'];
            $match_case = ($mc === 'true' || $mc === true || $mc === '1' || $mc === 1);
        }
        
        $whole_word = false;
        if (isset($_POST['whole_word'])) {
            $ww = $_POST['whole_word'];
            $whole_word = ($ww === 'true' || $ww === true || $ww === '1' || $ww === 1);
        }
        
        $regex = false;
        if (isset($_POST['regex'])) {
            $rg = $_POST['regex'];
            $regex = ($rg === 'true' || $rg === true || $rg === '1' || $rg === 1);
        }
        
        $suffixes_str = isset($_POST['suffixes']) ? $_POST['suffixes'] : '';
        
        
        $keyword = trim((string)$keyword);
        if ($keyword === '') {
            $this->error('搜索内容不能为空');
        }

        // 规范化路径
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . (strpos($path, '/') === 0 ? $path : '/' . $path);
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }

        $suffixes = array();
        if (!empty($suffixes_str)) {
            $temp_suffixes = preg_split('/[\s,]+/', $suffixes_str, -1, PREG_SPLIT_NO_EMPTY);
            foreach ($temp_suffixes as $s) {
                $s = trim($s, ' .*');
                if (!empty($s)) {
                    $suffixes[$s] = true;
                }
            }
        }
        

        // 使用PHP原生搜索方法，不依赖Shell命令
        $search_result = $this->_searchWithPHP($full_path, $keyword, $match_case, $whole_word, $regex, $suffixes);
        
        $files = $search_result['files'];
        $checked = $search_result['checked'];
        
        $this->success(array('files' => $files, 'checked' => $checked, 'limit_reached' => false));
    }
    
    /**
     * 使用PHP原生方法搜索文件内容
     */
    private function _searchWithPHP($path, $keyword, $match_case, $whole_word, $regex, $suffixes) {
        
        $files = array();
        $max_files = 100;  // 最多搜索100个文件
        $max_size = 10 * 1024 * 1024;  // 最大搜索10MB的文件
        $checked = 0;
        
        // 递归遍历目录
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );
        
        foreach ($iterator as $file) {
            if ($checked >= $max_files) {
                break;
            }
            
            if (!$file->isFile()) continue;
            
            $file_path = $file->getPathname();
            $file_name = $file->getFilename();
            $file_size = $file->getSize();
            
            // 检查文件大小
            if ($file_size > $max_size) continue;
            
            // 检查文件后缀
            if (!empty($suffixes)) {
                $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
                if (!isset($suffixes[$ext])) continue;
            }
            
            $checked++;
            
            // 读取文件内容
            $content = @file_get_contents($file_path);
            if ($content === false) continue;
            
            // 跳过二进制文件
            if (!mb_check_encoding($content, 'UTF-8') && !mb_check_encoding($content, 'GB2312') && !mb_check_encoding($content, 'GBK')) {
                continue;
            }
            
            // 搜索关键词
            $found = false;
            if ($regex) {
                // 正则表达式搜索
                $pattern = $match_case ? "/$keyword/" : "/$keyword/i";
                $found = @preg_match($pattern, $content) > 0;
            } else if ($whole_word) {
                // 全词匹配
                $pattern = $match_case ? "/\\b" . preg_quote($keyword, '/') . "\\b/" : "/\\b" . preg_quote($keyword, '/') . "\\b/i";
                $found = preg_match($pattern, $content) > 0;
            } else {
                // 普通搜索
                if ($match_case) {
                    $found = strpos($content, $keyword) !== false;
                } else {
                    $found = stripos($content, $keyword) !== false;
                }
            }
            
            if ($found) {
                $normalized_path = str_replace('\\', '/', $file_path);
                $rel_path = '/' . ltrim(str_replace($this->site_root, '', $normalized_path), '/');
                
                $files[] = array(
                    'name' => $file_name,
                    'path' => $rel_path,
                    'size' => $file_size,
                    'mtime' => $file->getMTime(),
                    'modified' => date('Y-m-d H:i:s', $file->getMTime()),
                    'type' => 'file',
                    'is_dir' => false
                );
            }
        }
        
        return array('files' => $files, 'checked' => $checked);
    }
    
    /**
     * 获取路径大小（文件或目录）
     */
    public function getPathSize($path) {
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . (strpos($path, '/') === 0 ? $path : '/' . $path);
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        $result = $this->bt_api->GetPathSize($full_path);
        if ($result && isset($result['status']) && $result['status']) {
            $size = isset($result['size']) ? $result['size'] : (isset($result['data']['size']) ? $result['data']['size'] : 0);
            $this->success(array('size' => $size, 'data' => $result));
        } else {
            $this->error(isset($result['msg']) ? $result['msg'] : '获取失败');
        }
    }
    
    /**
     * 获取文件/目录属性
     */
    public function getFileAttribute($path) {
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . (strpos($path, '/') === 0 ? $path : '/' . $path);
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        $result = $this->bt_api->GetFileAttribute($full_path);
        if ($result && (isset($result['status']) && $result['status'] || isset($result['path']))) {
            $this->success($result);
        } else {
            $this->error(isset($result['msg']) ? $result['msg'] : '获取失败');
        }
    }
    
    /**
     * 获取文件哈希
     */
    public function getFileHash($path) {
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . (strpos($path, '/') === 0 ? $path : '/' . $path);
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        $result = $this->bt_api->GetFileHash($full_path);
        if ($result && (isset($result['status']) && $result['status'] || isset($result['md5']))) {
            $this->success($result);
        } else {
            $this->error(isset($result['msg']) ? $result['msg'] : '获取失败');
        }
    }
    
    /**
     * 创建下载链接（外链）
     */
    public function createDownloadUrl($path, $ps = '', $expire = 24) {
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        if (strpos($path, $this->site_root) === 0) {
            $full_path = $path;
        } else {
            $full_path = $this->site_root . (strpos($path, '/') === 0 ? $path : '/' . $path);
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        $result = $this->bt_api->CreateDownloadUrl($full_path, $ps ?: basename($full_path), '', (int)$expire);
        if ($result && isset($result['status']) && $result['status']) {
            $url = isset($result['url']) ? $result['url'] : (isset($result['download_url']) ? $result['download_url'] : (isset($result['link']) ? $result['link'] : ''));
            $this->success(array('url' => $url, 'msg' => '创建成功', 'data' => $result));
        } else {
            $this->error(isset($result['msg']) ? $result['msg'] : '创建失败');
        }
    }
    
    /**
     * 创建符号链接（软链接/硬链接）
     * @param string $path 源文件/目录相对路径
     * @param string $link_name 链接的相对路径（如 /当前目录/linkname）
     * @param string $type soft|hard
     */
    public function createLink($path, $link_name, $type = 'soft') {
        $path = str_replace('\\', '/', $path);
        $path = str_replace('..', '', $path);
        $link_name = str_replace('\\', '/', $link_name);
        $link_name = str_replace('..', '', $link_name);
        if (strpos($path, $this->site_root) === 0) {
            $source_path = $path;
        } else {
            $source_path = $this->site_root . (strpos($path, '/') === 0 ? $path : '/' . $path);
        }
        if (strpos($link_name, $this->site_root) === 0) {
            $link_path = $link_name;
        } else {
            $link_path = $this->site_root . (strpos($link_name, '/') === 0 ? $link_name : '/' . $link_name);
        }
        $source_path = preg_replace('#/{2,}#', '/', $source_path);
        $link_path = preg_replace('#/{2,}#', '/', $link_path);
        if (strpos($source_path, $this->site_root) !== 0 || strpos($link_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        $result = $this->bt_api->CreateLink($source_path, $link_path, $type === 'hard' ? 'hard' : 'soft');
        if ($result && isset($result['status']) && $result['status']) {
            $this->log('创建链接', '创建链接: ' . $source_path . ' -> ' . $link_path);
            $this->success(array('msg' => '创建成功'));
        } else {
            $this->error(isset($result['msg']) ? $result['msg'] : '创建失败');
        }
    }
    
    /**
     * 批量重命名
     * @param array $files 每项为 {path: '相对路径', newName: '新文件名'}
     */
    public function batchRename($files) {
        if (!is_array($files) || empty($files)) {
            $this->error('请提供要重命名的文件列表');
        }
        $errors = array();
        foreach ($files as $item) {
            $path = isset($item['path']) ? $item['path'] : '';
            $newName = isset($item['newName']) ? trim($item['newName']) : '';
            if ($path === '' || $newName === '') continue;
            $path = str_replace('\\', '/', $path);
            $path = str_replace('..', '', $path);
            $dir = dirname($path);
            $targetPath = ($dir === '.' || $dir === '') ? $newName : rtrim($dir, '/') . '/' . $newName;
            $full_old = (strpos($path, $this->site_root) === 0) ? $path : $this->site_root . (strpos($path, '/') === 0 ? $path : '/' . $path);
            $full_new = (strpos($targetPath, $this->site_root) === 0) ? $targetPath : $this->site_root . (strpos($targetPath, '/') === 0 ? $targetPath : '/' . $targetPath);
            $full_old = preg_replace('#/{2,}#', '/', $full_old);
            $full_new = preg_replace('#/{2,}#', '/', $full_new);
            if (strpos($full_old, $this->site_root) !== 0 || strpos($full_new, $this->site_root) !== 0) continue;
            $result = $this->bt_api->RenameFile($full_old, $full_new);
            if (!$result || !isset($result['status']) || !$result['status']) {
                $errors[] = $path . ': ' . (isset($result['msg']) ? $result['msg'] : '失败');
            }
        }
        if (!empty($errors)) {
            $this->error(implode('; ', $errors));
        }
        $this->log('批量重命名', '批量重命名 ' . count($files) . ' 项');
        $this->success(array('msg' => '批量重命名成功'));
    }
    
    /**
     * 压缩并下载
     */
    public function zipAndDownload($dir, $files) {
        $dir = str_replace('\\', '/', $dir);
        $dir = str_replace('..', '', $dir);
        if (strpos($dir, $this->site_root) === 0) {
            $path = rtrim($dir, '/');
        } else {
            $path = $this->site_root . rtrim($dir, '/');
        }
        $path = preg_replace('#/{2,}#', '/', $path);
        if (strpos($path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        $result = $this->bt_api->ZipAndDownload($path, $files);
        if ($result && isset($result['status']) && $result['status']) {
            $url = isset($result['url']) ? $result['url'] : (isset($result['download']) ? $result['download'] : '');
            $this->success(array('msg' => '压缩成功', 'url' => $url, 'data' => $result));
        } else {
            $this->error(isset($result['msg']) ? $result['msg'] : '压缩失败');
        }
    }
    
    /**
     * 获取目录列表（仅目录）
     */
    public function getDirectories($path) {

        // 1. 清理和验证路径
        $path = '/' . ltrim(str_replace('\\', '/', $path), '/');
        $path = str_replace('..', '', $path);
        $path = preg_replace('#/{2,}#', '/', $path);

        // 2. 构建完整路径
        $full_path = ($path === '/') ? $this->site_root : $this->site_root . $path;
        $full_path = rtrim($full_path, '/');

        // 3. 再次验证路径是否在用户根目录下
        if (strpos(realpath($full_path), realpath($this->site_root)) !== 0) {
            $this->error('非法访问目录');
        }

        // 4. 检查目录是否存在
        if (!is_dir($full_path)) {
            $this->error('目录不存在: ' . $path);
        }

        // 5. 扫描目录并筛选出子目录
        $items = scandir($full_path);
        $directories = [];
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            $item_full_path = $full_path . '/' . $item;
            if (is_dir($item_full_path)) {
                $directories[] = [
                    'name' => $item,
                    'path' => $path . '/' . $item,
                ];
            }
        }

        $this->success(['directories' => $directories]);
    }
    
    /**
     * 获取重写规则
     */
    public function getRewriteRules() {
        // 获取站点ID和名称
        $siteId = $this->getWebId();
        // 直接调用底层 bt_api 获取规则列表，传递站点信息
        $rules = $this->bt_api->GetRewriteRules($siteId, $this->site_name);
        if (!$rules || !is_array($rules)) {
            $this->error('获取伪静态规则失败');
        }
        $this->success(array('rules' => $rules));
    }
    
    /**
     * 获取重写内容
     */
    public function getRewriteContent($name) {
        if (!$name) {
            $this->error('规则名称不能为空');
        }
        $content = $this->bt_api->GetRewriteContent($name);
        if ($content === '' || $content === null) {
            $this->error('获取规则内容失败');
        }
        $this->success(array('content' => $content));
    }
    
    /**
     * 获取当前重写规则
     */
    public function getCurrentRewrite() {
        // 使用当前站点ID与站点名称获取伪静态
        $siteId = $this->getWebId();
        $info = $this->bt_api->GetCurrentRewriteRule($siteId, $this->site_name);
        
        // 如果没有获取到或内容为空，返回空内容（表示没有设置伪静态）
        if (!$info || !isset($info['content'])) {
            $this->success(array('rule' => array('name' => 'none', 'content' => '')));
            return;
        }
        
        $this->success(array('rule' => $info));
    }
    
    /**
     * 导入SQL文件到数据库
     */
    public function importSql($file, $db_name) {
        
        // 安全检查
        $file = str_replace('\\', '/', $file);
        $file = str_replace('..', '', $file);
        
        if (strpos($file, $this->site_root) === 0) {
            $full_path = $file;
        } else {
            $full_path = $this->site_root . $file;
        }
        $full_path = preg_replace('#/{2,}#', '/', $full_path);
        
        // 验证
        if (strpos($full_path, $this->site_root) !== 0) {
            $this->error('非法路径');
        }
        
        // 检查文件是否存在
        if (!file_exists($full_path)) {
            $this->error('SQL文件不存在');
        }
        
        // 检查文件扩展名
        $ext = strtolower(pathinfo($full_path, PATHINFO_EXTENSION));
        if ($ext !== 'sql') {
            $this->error('只能导入SQL文件');
        }
        
        // 调用宝塔API导入SQL
        $result = $this->bt_api->drsql(array($full_path, $db_name));
        
        
        if ($result && isset($result['status']) && $result['status'] == true) {
            $this->log('导入SQL', '导入SQL文件: ' . $full_path . ' 到数据库: ' . $db_name);
            $this->success(array('msg' => 'SQL导入成功'));
        } else {
            $error_msg = isset($result['msg']) ? $result['msg'] : '导入失败';
            $this->error($error_msg);
        }
    }
    
    /**
     * 获取数据库列表
     */
    public function getDatabaseList() {

        $result = $this->bt_api->sjlist('databases');


        if ($result && isset($result['data']) && is_array($result['data'])) {
            // 过滤出属于当前用户的数据库
            $user_dbs = array();
            foreach ($result['data'] as $db) {
                $db_name = isset($db['name']) ? $db['name'] : '';
                // 假设数据库名以用户名开头
                if (strpos($db_name, $this->user_name) === 0) {
                    $user_dbs[] = $db;
                }
            }
            $this->success(array('databases' => $user_dbs));
        } else {
            $this->success(array('databases' => array()));
        }
    }

    /**
     * 获取目录真实大小（包括子目录）
     * @param string $dir 相对路径，如 / 或 /folder
     */
    public function getDirSize($dir) {

        // 安全清理路径
        $dir = $this->sanitizePath($dir);

        // 计算完整路径
        $full_path = $dir === '/' ? $this->site_root : $this->site_root . $dir;
        $full_path = preg_replace('#/{2,}#', '/', $full_path);


        // 验证路径是否在用户根目录下
        if (!$this->validatePath($full_path)) {
            $this->error('非法路径');
        }

        // 判断是本地服务器还是远程服务器
        $bt_server = $this->getBtServer();
        $is_local = in_array($bt_server['ip'], ['127.0.0.1', 'localhost', $_SERVER['SERVER_ADDR'] ?? '']);
        
        $size = 0;
        
        if ($is_local) {
            // 本地服务器使用du命令
            $size = $this->getDirSizeViaDu($full_path);
        } else {
            // 远程服务器使用宝塔API
            $size = $this->getDirSizeViaBtApi($full_path);
        }
        
        if ($size > 0) {
            $this->success(array('size' => $size));
        } else {
            // 如果都失败，尝试PHP递归计算（仅本地）
            $local_size = $this->calculateDirSize($full_path);
            $this->success(array('size' => $local_size));
        }
    }
    
    /**
     * 使用宝塔API获取目录大小（用于远程服务器）
     * @param string $path 目录路径
     * @return int 字节数
     */
    private function getDirSizeViaBtApi($path) {
        // 调用宝塔API获取目录大小
        $result = $this->bt_api->GetPathSize($path);
        
        if ($result && is_array($result)) {
            // 处理不同格式的返回数据
            if (isset($result['status']) && $result['status'] === true && isset($result['size'])) {
                return intval($result['size']);
            } elseif (isset($result['size'])) {
                return intval($result['size']);
            }
        }
        
        return 0;
    }

    /**
     * 使用du命令获取目录大小
     * @param string $path 目录路径
     * @return int 字节数
     */
    private function getDirSizeViaDu($path) {
        if (!is_dir($path)) {
            return 0;
        }

        // 使用du -sb 获取字节数
        $command = 'du -sb ' . escapeshellarg($path) . ' 2>/dev/null';
        $output = shell_exec($command);
        
        if ($output) {
            // du -sb 输出格式: "12345 /path/to/dir"
            $parts = explode("\t", trim($output));
            if (isset($parts[0]) && is_numeric($parts[0])) {
                return intval($parts[0]);
            }
        }

        // 如果du -sb失败，尝试du -sk（KB）然后转换
        $command = 'du -sk ' . escapeshellarg($path) . ' 2>/dev/null';
        $output = shell_exec($command);
        
        if ($output) {
            $parts = explode("\t", trim($output));
            if (isset($parts[0]) && is_numeric($parts[0])) {
                return intval($parts[0]) * 1024; // KB转字节
            }
        }

        return 0;
    }

    /**
     * 本地计算目录大小（递归）
     * @param string $path 目录路径
     * @return int 字节数
     */
    private function calculateDirSize($path) {
        $size = 0;

        if (!is_dir($path)) {
            return 0;
        }

        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $size += $file->getSize();
            }
        }

        return $size;
    }
}
