×

原创PHP代码:一款轻量高效的服务器端PHP文件打包工具

hqy hqy 发表于2025-10-25 01:39:23 浏览77 评论0

抢沙发发表评论

轻量高效的服务器端PHP文件打包工具:从功能到使用全指南

在日常开发或服务器管理中,我们经常会遇到这样的场景:需要从服务器上下载多个分散的文件(如PHP脚本、HTML页面、配置文件),却不想通过复杂的FTP工具逐个传输;想要快速打包某一目录下的代码文件分享给同事,却发现服务器上没有预装压缩工具;需要核对服务器上的文件结构和内容,避免下载后才发现漏选或错选文件……这时,一款能在服务器端直接运行、可视化操作的文件打包工具就成了刚需。

今天要介绍的这款PHP单文件文件打包工具,由本人开发,只需要新建一个php文件,复制代码进去,通过url访问该文件即可,就可轻易解决这些痛点。它无需复杂部署,只需将单个PHP文件上传到服务器目录,通过浏览器访问即可实现目录扫描、文件选择、多格式打包、内容预览等功能,尤其适合开发者、运维人员在无本地压缩工具或FTP权限受限的场景下使用。下面,我们将从工具核心功能、使用方法、优势亮点、注意事项等方面,进行全面且通俗的讲解。


图片


一、工具核心功能:贴合实际需求,覆盖打包全流程

这款工具的所有功能都基于PHP开发,完全在服务器端运行,无需依赖本地软件。从代码逻辑来看,它的核心能力可分为“文件扫描与管理”“多格式打包”“内容预览与下载”“状态监控与统计”四大模块,每个模块都精准对应实际使用中的需求。


图片


1. 智能目录扫描:自动识别文件,跳过冗余内容

工具的第一步是“找到文件”,这依赖于代码中的 scanDirectoryForStructure() 函数。当你访问工具页面时,它会递归扫描当前目录及所有子目录,自动识别文件和文件夹,同时规避不必要的冗余内容——这是它的一大贴心设计。

  • 自动跳过关键文件:代码中预设了 $skipItems 数组,会自动跳过工具自身相关的文件(如生成的打包文件 files.txt 、工具本体 file.php 、服务器配置文件 .user.ini ),避免打包时把工具文件也包含进去,减少冗余。
  • 兼容不同服务器环境:扫描过程中会先判断目录是否“存在且可读”(通过 is_dir() 和 is_readable() 函数),如果某目录因权限问题无法访问,会自动跳过并继续扫描其他目录,不会导致工具崩溃。
  • 清晰区分文件类型:扫描后会给每个条目标记“file”(文件)或“folder”(文件夹)类型,同时记录文件的大小(通过 filesize() 函数),后续在界面上会用不同图标区分(如文件夹用?,PHP文件用?,图片用?️),直观易懂。

举个例子:如果你的服务器目录下有“src”“config”“public”三个子目录,以及“index.php”“README.md”两个文件,工具会自动把这些内容全部扫描出来,并用层级结构展示,就像在本地文件管理器里一样清晰。


图片


2. 灵活的文件选择:批量操作,减少重复劳动

找到文件后,下一步就是“选对文件”。工具提供了多种文件选择方式,彻底告别“逐个勾选”的繁琐,这部分功能由前端JS逻辑与后端PHP判断协同实现。

  • 全选/取消全选:界面顶部的“全选”按钮会自动勾选当前扫描到的所有文件(包括子目录下的文件),“取消全选”则一键清空选择,适合需要打包大部分或全部文件的场景。
  • 文件夹级选择:勾选文件夹前的复选框时,工具会自动选中该文件夹下的所有子文件和子目录(通过 handleFolderSelection() 函数实现),比如勾选“src”文件夹,就会自动选中“src”下的 api.php “utils”子目录及其中的 helper.php ,无需逐个展开勾选。
  • 折叠/展开目录:文件夹前的“▶”“▼”按钮可以折叠或展开子目录,当目录层级较多(如嵌套5-6层)时,折叠无关目录能让界面更简洁,避免视觉混乱。
  • 精准筛选文件:如果只需打包某几个零散文件(如“config/db.php”“public/style.css”),可以直接展开对应目录,单独勾选目标文件,灵活度拉满。

比如你需要打包“config”目录下的所有配置文件和“public”目录下的HTML、CSS文件,只需勾选“config”文件夹和“public”文件夹,工具会自动处理其中的所有子文件,无需手动逐个选择。

3. 多格式打包:适配不同场景,兼容多数服务器

打包功能是工具的核心,代码中的 getSupportedFormats() 函数会先检测服务器环境,自动启用支持的压缩格式,避免出现“点击打包却提示不支持”的尴尬。目前工具支持的格式分为“基础格式”和“扩展格式”两类,覆盖绝大多数使用场景。

(1)默认支持:TXT文本格式(带内容预览)

TXT格式是工具的“保底选项”,无论服务器环境如何,都会支持。它的特殊之处在于不仅能打包文件列表,还能包含文件的具体内容,对应代码中的 generateTxtFile() 函数。

生成的TXT文件会分为三个部分:

  • 文件结构:列出所有选中文件的路径和总数量,比如“总文件数: 3”“- config/db.php”“- public/index.html”,让你快速了解打包范围;
  • 文件内容:按路径顺序展示每个文件的具体内容,比如“config/db.php”的数据库配置代码、“public/index.html”的HTML结构,中间用“==...==”分隔,清晰区分不同文件;
  • 处理统计:最后会显示“总计处理文件: 3 个”,确认是否所有选中文件都已成功处理。

更实用的是,TXT格式支持浏览器预览——生成后不会直接下载,而是在页面上显示预览框,你可以先核对文件内容是否正确(比如确认配置文件没有敏感信息),再点击“下载文件”保存到本地,避免下载后才发现漏选或内容错误。

(2)扩展支持:多种压缩格式(直接下载)

除了TXT,工具还会根据服务器环境自动启用压缩格式,常见的包括:

  • ZIP格式:最通用的压缩格式,几乎所有操作系统(Windows、macOS、Linux)都能直接解压。代码中通过 ZipArchive 类实现,只要服务器PHP环境开启了该扩展(大部分主流服务器都会开启),就能使用;
  • GZ格式(tar.gz):在Linux/macOS环境下常用的压缩格式,代码中会先将文件打包成tar格式,再通过 gzencode() 函数压缩为tar.gz,适合需要在服务器端进一步处理压缩包的场景;
  • RAR格式:需服务器安装RAR扩展( RarArchive 类),压缩率略高于ZIP,适合习惯使用WinRAR的用户;
  • 7Z格式:压缩率最高的格式之一,但需要服务器上预装7z二进制文件(通过 exec() 函数检测“which 7z”),适合打包大型文件(如多个图片、视频)以节省存储空间。

这些压缩格式的打包逻辑各有优化,比如ZIP格式会保留文件的相对路径(打包后解压仍能保持“config/db.php”的层级),GZ格式会严格遵循tar标准以确保兼容性,避免出现“解压后文件混乱”的问题。

4. 可视化状态监控:进度清晰,统计直观

很多打包工具在生成文件时会让用户“盲目等待”,而这款工具通过“进度条”和“统计面板”,让整个过程可视化,对应代码中的 showStats() 和进度条模拟逻辑。

  • 实时进度条:点击“生成文件”后,页面会显示一个从0%到100%的进度条,虽然是模拟进度(实际生成速度取决于文件大小和数量),但能有效缓解“不知道还要等多久”的焦虑,尤其在打包大量文件时更实用;
  • 核心统计面板:界面下方的统计区域会实时显示4个关键数据:
  1. 总文件数:当前目录下所有可识别的文件总数(不含文件夹);
  2. 总文件夹数:当前目录下所有子目录的总数;
  3. 已选择文件:你当前勾选的文件数量,避免漏选或多选;
  4. 支持格式:服务器当前支持的打包格式数量(如显示“3”,代表支持TXT、ZIP、GZ)。

比如你扫描到服务器上有12个文件、3个文件夹,勾选了8个文件,支持4种格式,这些数据会实时显示在统计面板上,让你对当前操作范围和工具能力一目了然。

5. 人性化下载体验:自定义文件名,适配不同需求

工具在下载环节也做了细节优化,避免“每次下载都是默认名”的麻烦:

  • 自定义输出文件名:在“打包选项”中,你可以输入自定义文件名(如“20240510_网站代码备份”),工具会自动添加对应格式的后缀(如ZIP格式会变成“20240510_网站代码备份.zip”,TXT格式会变成“20240510_网站代码备份.txt”);
  • 区分下载逻辑:TXT格式会先预览再提供下载,压缩格式(ZIP、GZ等)则在生成后直接触发浏览器下载,无需额外点击;
  • 兼容性下载:代码中通过 Blob 对象和 URL.createObjectURL() 实现前端下载,支持Chrome、Firefox、Edge、Safari等主流浏览器,不会出现“下载按钮无效”的问题。

二、工具使用教程:3步上手,无需技术背景

这款工具的最大优势之一就是“零门槛使用”,无论你是资深开发者还是刚接触服务器的新手,只需3步就能完成从部署到打包的全过程。

第一步:部署工具到服务器

  1. 准备工具文件:将工具的PHP文件(假设文件名为 filepacker.php ,即代码中的 file.php )保存到本地;
  2. 上传文件:通过FTP工具(如FileZilla)或服务器面板(如宝塔面板),将 filepacker.php 上传到你需要打包文件的目标目录(比如你想打包“/www/wwwroot/mywebsite”下的文件,就把 filepacker.php 上传到这个目录);
  3. 确认权限:确保 filepacker.php 所在目录有“读取”权限(服务器上通常默认开启),否则工具无法扫描目录下的文件。

第二步:扫描并选择文件

  1. 访问工具:打开浏览器,在地址栏输入“服务器域名/目录路径/filepacker.php”(比如“http://www.mywebsite.com/filepacker.php”),加载完成后工具会自动开始扫描目录;
  2. 查看文件列表:扫描完成后,页面会以层级结构显示所有文件和文件夹,文件夹前有“▶”按钮,点击可展开子目录,文件旁会显示大小(如“index.php 2.1 KB”);
  3. 选择目标文件:
  • 如需打包全部文件:点击顶部“全选”按钮;
  • 如需打包部分目录:勾选目标文件夹(如“src”“config”),工具会自动选中子文件;
  • 如需打包零散文件:展开对应目录,单独勾选目标文件(如“public/style.css”);
  • 选错了?点击“取消全选”重新选择,或直接取消单个文件的勾选。

第三步:设置打包选项并下载

  1. 选择输出格式:在“打包选项”的“输出格式”中,选择你需要的格式:
  • 想预览内容或只需简单打包:选“TXT (文本预览)”;
  • 想压缩节省空间或分享给他人:选“ZIP”(兼容性最好);
  • 服务器环境支持且需要高压缩率:选“7Z”或“GZ”;
  1. 自定义文件名:在“输出文件名”输入框中,填写你想要的文件名(如“mywebsite_backup_202405”),无需手动加后缀;
  2. 生成文件:点击“生成文件”按钮,此时会显示进度条,等待进度条完成;
  3. 下载文件:
  • 若选择TXT格式:进度条完成后会显示“文件内容预览”,核对内容无误后,点击“下载文件”保存;
  • 若选择压缩格式(ZIP/GZ等):进度条完成后会自动触发下载,文件会保存到浏览器默认的下载目录。

三、工具优势亮点:为什么选择这款工具?

在众多文件打包方案(如本地压缩后上传、服务器命令行压缩、FTP逐个下载)中,这款PHP工具的优势非常明显,尤其适合特定场景下的需求:

1. 单文件部署:无需安装,即传即用

工具仅包含一个PHP文件,无需解压、配置环境变量或安装依赖(除部分压缩格式需服务器预装扩展外),上传到目标目录后直接通过浏览器访问即可使用。相比需要部署后端服务的工具(如基于Node.js的打包工具),它的部署成本几乎为零,哪怕是新手也能在1分钟内完成部署。

2. 服务器端运行:突破本地环境限制

所有操作都在服务器端完成,无需依赖本地电脑的软件环境:

  • 无需安装压缩软件(如WinRAR、7-Zip),哪怕你在公共电脑上操作,只要能访问浏览器就能打包;
  • 避免FTP传输的繁琐:如果需要打包100个小文件,用FTP逐个下载需要反复点击,而用工具打包后只需下载一个压缩包,节省大量时间;
  • 适配弱网环境:压缩后的文件体积更小,在网络速度较慢时,下载一个10MB的ZIP包比下载100个100KB的零散文件快得多。

3. 可视化操作:比命令行更易用

对于不熟悉服务器命令行(如Linux的 zip / tar 命令)的用户,可视化界面的优势不言而喻:

  • 无需记忆复杂命令(如 zip -r backup.zip src/ config/ ),用鼠标点击就能完成选择和打包;
  • 实时反馈状态:文件是否选中、格式是否支持、生成进度如何,都能在界面上直观看到,避免命令行“黑盒操作”导致的错误(如输错目录路径导致打包失败)。

4. 安全跳过关键文件:避免泄露敏感信息

代码中预设了跳过工具自身文件和服务器配置文件(如 .user.ini )的逻辑,这些文件通常包含工具运行信息或服务器配置,打包后如果分享给他人,可能存在安全风险。工具的自动跳过功能,能减少“误打包敏感文件”的概率,提升使用安全性。

5. 兼容性强:适配多数PHP环境

工具基于PHP 5.6+开发(兼容PHP 7.x、8.x),支持绝大多数主流服务器环境(如Apache、Nginx、IIS),只要服务器开启了基础的PHP运行环境(这是网站服务器的标配),就能正常使用。对于压缩格式的依赖(如 ZipArchive 类),大部分云服务器(如阿里云、腾讯云)的默认PHP配置都会开启,无需额外手动配置。

四、注意事项与常见问题

虽然工具易用且稳定,但在使用过程中,仍有一些细节需要注意,以避免出现问题;同时,我们也整理了一些常见问题的解决方案:

注意事项

1. 权限问题:确保目录可读取

工具扫描文件时需要目录有“读取权限”(服务器上通常用 chmod 755 设置目录权限, chmod 644 设置文件权限)。如果某目录显示“没有找到文件或目录不可读”,可能是该目录权限不足,需要通过服务器面板或FTP工具调整权限。

2. 敏感文件:谨慎选择打包范围

服务器目录中可能包含数据库密码(如 config/db.php )、API密钥等敏感信息,打包前务必确认选中的文件中没有这些内容,避免将压缩包分享给他人时泄露信息。如果需要分享代码,建议先删除敏感信息或替换为占位符。

3. 文件大小:避免打包超大文件

虽然工具支持打包大文件,但生成压缩包时会占用服务器内存和磁盘空间。如果需要打包单个超过1GB的文件(如视频、大型数据库备份),建议直接通过FTP下载,避免因服务器内存不足导致打包失败。

4. 安全访问:限制工具访问权限

工具能扫描和打包目录下的文件,存在一定的安全风险(如被未授权用户访问,导致文件泄露)。使用完成后,建议及时删除服务器上的 filepacker.php 文件;如果需要长期使用,可通过服务器面板设置访问密码(如Nginx的 auth_basic 认证),仅允许指定用户访问。

常见问题(FAQ)

Q1:为什么界面上没有显示RAR/7Z格式的选项?

A:这是因为服务器环境不支持对应的格式:

  • 没有RAR选项:服务器PHP未安装 rar 扩展,需联系服务器管理员开启;
  • 没有7Z选项:服务器未预装7z二进制文件(Linux需通过 yum install p7zip 或 apt install p7zip-full 安装,Windows需手动安装并配置环境变量)。 如果无法修改服务器配置,建议选择ZIP格式(兼容性最好)。

Q2:扫描文件时提示“没有找到文件或目录不可读”,怎么办?

A:首先检查工具所在目录的权限,确保目录有“读取”权限;其次,确认该目录下确实有文件(如果是刚创建的空目录,扫描结果也会为空);最后,如果目录下有符号链接(软链接),工具可能无法识别,建议直接访问实际目录。

Q3:生成TXT文件后,预览内容显示乱码,怎么解决?

A:这是因为文件编码与浏览器默认编码不一致(如文件是GBK编码,而浏览器用UTF-8解码)。解决方案:生成TXT文件后,用记事本打开,点击“文件”→“另存为”,在“编码”选项中选择“UTF-8”,保存后即可正常显示。

Q4:生成ZIP文件后,解压时提示“压缩包损坏”,是什么原因?

A:可能有两种原因:

  1. 生成过程中网络中断:导致下载的压缩包不完整,重新生成并下载即可;
  2. 服务器内存不足:打包大量文件时,服务器内存不够导致压缩过程中断,建议分多次打包(如先打包“src”目录,再打包“config”目录),或删除部分不必要的文件后再打包。

Q5:可以自定义跳过的文件吗?比如我想跳过“log”目录下的日志文件。

A:目前工具的跳过列表( 数组)是在代码中预设的,无法通过界面自定义。如果需要跳过特定文件或目录,可手动修改代码:在skipItems 数组中添加需要跳过的路径(如 'log/' 代表跳过“log”目录, 'tmp/cache.php' 代表跳过“tmp”目录下的 cache.php ),修改后保存并重新上传到服务器即可。

五、总结

这款PHP文件打包工具虽然轻量,但功能却十分精准地覆盖了服务器端文件打包的核心需求:从智能扫描目录、灵活选择文件,到多格式打包、可视化预览,再到人性化的下载体验,每一个功能都围绕“简单、高效、易用”的目标设计。

无论是开发者需要备份服务器上的代码文件,运维人员需要打包日志文件,还是普通用户需要分享服务器上的零散文件,这款工具都能成为得力助手。它无需复杂部署,突破本地环境限制,让文件打包从“繁琐操作”变成“一键完成”,尤其适合在无本地工具、弱网环境或命令行不熟练的场景下使用。

如果你经常需要在服务器上处理文件打包需求,不妨试试这款工具——只需一个PHP文件,就能解决你的所有烦恼。代码分享如下:




























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































<?php// 设置输出文件名$outputFile = 'files.txt';
// 要跳过的文件和文件夹列表$skipItems = [    $outputFile,    'file.php',    '.user.ini',];
// 检查服务器支持的压缩格式function getSupportedFormats() {    $formats = ['txt']; // TXT总是支持的
    // 检查Zip支持    if (class_exists('ZipArchive')) {        $formats[] = 'zip';    }
    // 检查Gz支持    if (function_exists('gzencode')) {        $formats[] = 'gz';    }
    // 检查Rar支持(需要rar扩展)    if (class_exists('RarArchive')) {        $formats[] = 'rar';    }
    // 检查7z支持(需要7zip二进制文件)    if (function_exists('exec')) {        $output = [];        $returnCode = 0;        @exec('which 7z 2>/dev/null', $output, $returnCode);        if ($returnCode === 0 && !empty($output)) {            $formats[] = '7z';        }    }
    return $formats;}
// 递归遍历目录并收集文件结构function scanDirectoryForStructure($dir, $baseDir = '', $skipItems = []) {    $structure = [];
    // 检查目录是否存在且可读    if (!is_dir($dir) || !is_readable($dir)) {        return $structure;    }
    $files = @scandir($dir);    if ($files === false) {        return $structure;    }
    foreach ($files as $file) {        if ($file == '.' || $file == '..') {            continue;        }
        $filePath = $dir . DIRECTORY_SEPARATOR . $file;        $relativePath = ($baseDir ? $baseDir . DIRECTORY_SEPARATOR : '') . $file;
        // 检查是否需要跳过该文件或文件夹        $shouldSkip = false;        foreach ($skipItems as $skipItem) {            if (basename($filePath) === $skipItem) {                $shouldSkip = true;                break;            }        }
        if ($shouldSkip) {            continue;        }
        if (is_dir($filePath)) {            $item = [                'name' => $file,                'path' => $relativePath,                'type' => 'folder',                'children' => scanDirectoryForStructure($filePath, $relativePath, $skipItems)            ];            $structure[] = $item;        } else if (is_file($filePath) && is_readable($filePath)) {            $item = [                'name' => $file,                'path' => $relativePath,                'type' => 'file',                'size' => filesize($filePath)            ];            $structure[] = $item;        }    }
    return $structure;}
// 根据选择的文件生成TXT文件function generateTxtFile($selectedFiles) {    global $skipItems;
    $outputContent = "=== 文件结构 ===\n";    $outputContent .= "总文件数: " . count($selectedFiles) . "\n\n";
    foreach ($selectedFiles as $file) {        $outputContent .= "- " . $file . "\n";    }
    $outputContent .= "\n=== 文件内容 ===\n\n";
    $processedCount = 0;    foreach ($selectedFiles as $file) {        if (is_file($file) && is_readable($file)) {            $outputContent .= "==...==\n";            $outputContent .= $file . "\n";            $content = @file_get_contents($file);            if ($content !== false) {                $outputContent .= $content . "\n\n";                $processedCount++;            }        }    }
    $outputContent .= "=== 文件内容结束 ===\n";    $outputContent .= "总计处理文件: " . $processedCount . " 个\n";
    return $outputContent;}
// 修复的ZIP文件生成函数function generateZipFile($selectedFiles) {    $zip = new ZipArchive();
    // 创建内存中的ZIP文件    $tempFile = tempnam(sys_get_temp_dir(), 'zip');
    if ($zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {        foreach ($selectedFiles as $file) {            if (is_file($file) && is_readable($file)) {                // 使用相对路径作为ZIP内的路径                $zip->addFile($file, $file);            }        }
        if (!$zip->close()) {            return false;        }
        // 读取文件内容        $content = file_get_contents($tempFile);        unlink($tempFile);
        return $content;    }
    if (file_exists($tempFile)) {        unlink($tempFile);    }
    return false;}
// 修复的GZ文件生成函数 - 使用更标准的tar格式function generateGzFile($selectedFiles) {    $tarContent = '';
    foreach ($selectedFiles as $file) {        if (is_file($file) && is_readable($file)) {            $content = file_get_contents($file);            if ($content !== false) {                $fileInfo = stat($file);                $mode = 0644;                $uid = 0;                $gid = 0;                $size = strlen($content);                $mtime = time();                $typeflag = '0'; // 普通文件                $linkname = '';                $magic = "ustar";                $version = "00";                $uname = "root";                $gname = "root";                $devmajor = 0;                $devminor = 0;                $prefix = '';
                // 文件名处理(不超过100字符)                $name = $file;                if (strlen($name) > 100) {                    $prefix = substr($name, 0, 155);                    $name = substr($name, -100);                }
                // 创建tar头部                $header = pack("a100a8a8a8a12a12a8a1a100a6a2a32a32a8a8a155a12",                    $name, // 文件名                    sprintf("%07o", $mode), // 文件模式                    sprintf("%07o", $uid), // 用户ID                    sprintf("%07o", $gid), // 组ID                    sprintf("%011o", $size), // 文件大小                    sprintf("%011o", $mtime), // 修改时间                    "        ", // 校验和占位符                    $typeflag, // 文件类型                    $linkname, // 链接名                    $magic, // magic                    $version, // 版本                    $uname, // 用户名                    $gname, // 组名                    sprintf("%07o", $devmajor), // 主设备号                    sprintf("%07o", $devminor), // 次设备号                    $prefix, // 前缀                    "" // 填充                );
                // 计算校验和                $checksum = 0;                for ($i = 0; $i < 512; $i++) {                    $checksum += ord($header[$i]);                }
                // 写入校验和                $header = substr_replace($header, sprintf("%07o", $checksum) . "\0", 148, 8);
                $tarContent .= $header;                $tarContent .= $content;
                // 填充到512字节边界                $padding = 512 - ($size % 512);                if ($padding < 512) {                    $tarContent .= str_repeat("\0", $padding);                }            }        }    }
    // 添加结束标记(两个512字节的零块)    $tarContent .= str_repeat("\0", 1024);
    // 压缩为gz    return gzencode($tarContent, 9);}
// 处理AJAX请求function handleAjaxRequest() {    global $skipItems;
    if (isset($_GET['action']) && $_GET['action'] === 'scan') {        // 返回文件结构        $fileStructure = scanDirectoryForStructure('.', '', $GLOBALS['skipItems']);        $supportedFormats = getSupportedFormats();
        header('Content-Type: application/json; charset=utf-8');        echo json_encode([            'success' => true,            'fileStructure' => $fileStructure,            'supportedFormats' => $supportedFormats        ]);        return true;    }
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {        $input = json_decode(file_get_contents('php://input'), true);
        if (isset($input['action']) && $input['action'] === 'generate') {            $selectedFiles = $input['files'] ?? [];            $format = $input['format'] ?? 'txt';            $filename = $input['filename'] ?? '打包文件';
            if (empty($selectedFiles)) {                header('Content-Type: application/json');                echo json_encode(['success' => false, 'message' => '没有选择文件']);                return true;            }
            $content = '';            $contentType = '';            $downloadFilename = '';
            switch ($format) {                case 'txt':                    $content = generateTxtFile($selectedFiles);                    $contentType = 'text/plain; charset=utf-8';                    $downloadFilename = $filename . '.txt';                    break;
                case 'zip':                    if (class_exists('ZipArchive')) {                        $content = generateZipFile($selectedFiles);                        if ($content === false) {                            header('Content-Type: application/json');                            echo json_encode(['success' => false, 'message' => '生成ZIP文件失败']);                            return true;                        }                        $contentType = 'application/zip';                        $downloadFilename = $filename . '.zip';                    } else {                        header('Content-Type: application/json');                        echo json_encode(['success' => false, 'message' => '服务器不支持ZIP格式']);                        return true;                    }                    break;
                case 'gz':                    if (function_exists('gzencode')) {                        $content = generateGzFile($selectedFiles);                        $contentType = 'application/gzip';                        $downloadFilename = $filename . '.tar.gz';                    } else {                        header('Content-Type: application/json');                        echo json_encode(['success' => false, 'message' => '服务器不支持GZ格式']);                        return true;                    }                    break;
                default:                    header('Content-Type: application/json');                    echo json_encode(['success' => false, 'message' => '不支持的格式: ' . $format]);                    return true;            }
            if ($content === false) {                header('Content-Type: application/json');                echo json_encode(['success' => false, 'message' => '生成文件失败']);                return true;            }
            // 对于TXT格式,返回JSON包含内容用于预览            if ($format === 'txt') {                header('Content-Type: application/json');                echo json_encode([                    'success' => true,                    'format' => 'txt',                    'filename' => $downloadFilename,                    'content' => $content                ]);            } else {                // 对于压缩格式,直接输出文件                header('Content-Type: ' . $contentType);                header('Content-Disposition: attachment; filename="' . $downloadFilename . '"');                header('Content-Length: ' . strlen($content));                echo $content;            }            return true;        }    }
    return false;}
// 主执行逻辑try {    // 首先处理AJAX请求    if (handleAjaxRequest()) {        exit;    }} catch (Exception $e) {    header('Content-Type: application/json');    echo json_encode([        'success' => false,        'message' => '服务器错误: ' . $e->getMessage()    ]);    exit;}
// HTML界面保持不变...?>
<!DOCTYPE html><html lang="zh-CN"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>文件打包工具</title>    <style>        /* 样式保持不变... */        :root {            --primary-color#3498db;            --secondary-color#2980b9;            --success-color#2ecc71;            --danger-color#e74c3c;            --light-color#f8f9fa;            --dark-color#343a40;            --border-radius4px;        }
        * {            box-sizing: border-box;            margin0;            padding0;        }
        body {            font-family'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;            line-height1.6;            color#333;            background-color#f5f7fa;            padding20px;        }
        .container {            max-width1200px;            margin0 auto;            background: white;            border-radiusvar(--border-radius);            box-shadow0 2px 10px rgba(0000.1);            padding30px;        }
        h1h2h3 {            colorvar(--dark-color);            margin-bottom20px;        }
        h1 {            text-align: center;            margin-bottom30px;            colorvar(--primary-color);        }
        .panel {            backgroundvar(--light-color);            border-radiusvar(--border-radius);            padding20px;            margin-bottom20px;        }
        .file-list {            max-height500px;            overflow-y: auto;            border1px solid #ddd;            border-radiusvar(--border-radius);            padding15px;            background: white;        }
        .file-item {            display: flex;            align-items: center;            padding8px 0;            border-bottom1px solid #eee;        }
        .file-item:last-child {            border-bottom: none;        }
        .file-item input[type="checkbox"] {            margin-right10px;        }
        .file-icon {            margin-right8px;            colorvar(--primary-color);        }
        .folder > .file-name {            font-weight: bold;            colorvar(--secondary-color);        }
        .folder .children {            margin-left20px;            display: none;        }
        .folder.expanded .children {            display: block;        }
        .folder-toggle {            cursor: pointer;            margin-right5px;            colorvar(--secondary-color);        }
        .options {            display: flex;            flex-wrap: wrap;            gap20px;            margin20px 0;        }
        .option-group {            flex1;            min-width200px;        }
        select, buttoninput[type="text"] {            width100%;            padding10px;            border1px solid #ddd;            border-radiusvar(--border-radius);            font-size16px;        }
        button {            background-colorvar(--primary-color);            color: white;            border: none;            cursor: pointer;            transition: background-color 0.3s;        }
        button:hover {            background-colorvar(--secondary-color);        }
        button:disabled {            background-color#95a5a6;            cursor: not-allowed;        }
        .format-option {            display: flex;            align-items: center;            margin-bottom10px;        }
        .format-option input {            margin-right10px;        }
        .status {            margin-top20px;            padding15px;            border-radiusvar(--border-radius);            display: none;        }
        .status.success {            background-color#d4edda;            color#155724;            border1px solid #c3e6cb;        }
        .status.error {            background-color#f8d7da;            color#721c24;            border1px solid #f5c6cb;        }
        .status.info {            background-color#d1ecf1;            color#0c5460;            border1px solid #bee5eb;        }
        .progress {            width100%;            height20px;            background-color#e9ecef;            border-radiusvar(--border-radius);            margin10px 0;            overflow: hidden;            display: none;        }
        .progress-bar {            height100%;            background-colorvar(--primary-color);            width0%;            transition: width 0.3s;        }
        .stats {            display: grid;            grid-template-columnsrepeat(auto-fit, minmax(200px1fr));            gap15px;            margin-top20px;        }
        .stat-item {            backgroundvar(--light-color);            padding15px;            border-radiusvar(--border-radius);            text-align: center;        }
        .stat-value {            font-size24px;            font-weight: bold;            colorvar(--primary-color);        }
        .header-actions {            display: flex;            justify-content: space-between;            margin-bottom20px;            flex-wrap: wrap;            gap10px;        }
        .btn {            padding8px 15px;            border-radiusvar(--border-radius);            cursor: pointer;            border: none;            font-size14px;        }
        .btn-primary {            background-colorvar(--primary-color);            color: white;        }
        .btn-secondary {            background-color#6c757d;            color: white;        }
        .preview-area {            margin-top20px;            border1px solid #ddd;            border-radiusvar(--border-radius);            padding15px;            background: white;            display: none;        }
        .preview-content {            width100%;            height300px;            font-family: monospace;            border1px solid #ddd;            border-radiusvar(--border-radius);            padding10px;            resize: vertical;        }
        @media (max-width768px) {            .options {                flex-direction: column;            }
            .header-actions {                flex-direction: column;            }        }    </style></head><body>    <div class="container">        <h1>文件打包工具</h1>
        <div class="header-actions">            <button id="select-all" class="btn btn-primary">全选</button>            <button id="deselect-all" class="btn btn-secondary">取消全选</button>            <button id="expand-all" class="btn btn-secondary">展开所有</button>            <button id="collapse-all" class="btn btn-secondary">折叠所有</button>        </div>
        <div class="panel">            <h2>选择要打包的文件</h2>            <div class="file-list" id="file-list">                <div class="file-item">                    <span>正在加载文件列表...</span>                </div>            </div>        </div>
        <div class="panel">            <h2>打包选项</h2>            <div class="options">                <div class="option-group">                    <h3>输出格式</h3>                    <div id="format-options">                        <div class="format-option">                            <input type="radio" name="format" id="format-txt" value="txt" checked>                            <label for="format-txt">TXT (文本预览)</label>                        </div>                    </div>                </div>
                <div class="option-group">                    <h3>输出文件名</h3>                    <input type="text" id="output-filename" placeholder="请输入输出文件名" value="打包文件">                </div>            </div>
            <button id="generate-btn">生成文件</button>
            <div class="progress" id="progress-container">                <div class="progress-bar" id="progress-bar"></div>            </div>
            <div class="status" id="status-message"></div>
            <div class="preview-area" id="preview-area">                <h3 id="preview-title">文件内容预览</h3>                <textarea class="preview-content" id="preview-content" readonly></textarea>                <button id="download-preview" class="btn btn-primary" style="margin-top: 10px;">下载文件</button>            </div>
            <div class="stats" id="stats-container">                <div class="stat-item">                    <div class="stat-label">总文件数</div>                    <div class="stat-value" id="total-files">0</div>                </div>                <div class="stat-item">                    <div class="stat-label">总文件夹数</div>                    <div class="stat-value" id="total-folders">0</div>                </div>                <div class="stat-item">                    <div class="stat-label">已选择文件</div>                    <div class="stat-value" id="selected-count">0</div>                </div>                <div class="stat-item">                    <div class="stat-label">支持格式</div>                    <div class="stat-value" id="supported-formats">1</div>                </div>            </div>        </div>    </div>
    <script>        // 全局变量        let fileStructure = [];        let selectedFiles = [];        let supportedFormats = [];
        // 页面加载完成后初始化        document.addEventListener('DOMContentLoaded'function() {            // 获取文件结构            fetchFileStructure();
            // 绑定事件            document.getElementById('select-all').addEventListener('click', selectAll);            document.getElementById('deselect-all').addEventListener('click', deselectAll);            document.getElementById('expand-all').addEventListener('click', expandAll);            document.getElementById('collapse-all').addEventListener('click', collapseAll);            document.getElementById('generate-btn').addEventListener('click', generateFile);            document.getElementById('download-preview').addEventListener('click', downloadPreview);        });
        // 获取文件结构        function fetchFileStructure() {            showStatus('正在扫描文件结构...''info');
            // 使用AJAX获取文件结构            fetch('?action=scan')                .then(response => {                    if (!response.ok) {                        throw new Error('网络响应不正常');                    }                    return response.json();                })                .then(data => {                    if (data.success) {                        fileStructure = data.fileStructure;                        supportedFormats = data.supportedFormats;
                        // 渲染文件列表                        renderFileList();
                        // 渲染格式选项                        renderFormatOptions();
                        // 显示统计信息                        showStats();
                        showStatus('文件结构扫描完成''success');                    } else {                        throw new Error(data.message || '扫描失败');                    }                })                .catch(error => {                    console.error('Error:', error);                    showStatus('扫描文件结构时出错: ' + error.message'error');                });        }
        // 渲染文件列表        function renderFileList() {            const fileListContainer = document.getElementById('file-list');            fileListContainer.innerHTML = '';
            if (fileStructure.length === 0) {                fileListContainer.innerHTML = '<div><span>没有找到文件或目录不可读</span></div>';                return;            }
            fileStructure.forEach(item => {                fileListContainer.appendChild(createFileItem(item));            });        }
        // 创建文件项        function createFileItem(item, level = 0) {            const itemDiv = document.createElement('div');            itemDiv.className = 'file-item';            itemDiv.style.paddingLeft = (level * 20) + 'px';
            if (item.type === 'folder') {                itemDiv.classList.add('folder');
                const toggleSpan = document.createElement('span');                toggleSpan.className = 'folder-toggle';                toggleSpan.innerHTML = '▶';                toggleSpan.addEventListener('click'function() {                    itemDiv.classList.toggle('expanded');                    toggleSpan.innerHTML = itemDiv.classList.contains('expanded') ? '▼' : '▶';                });
                const checkbox = document.createElement('input');                checkbox.type = 'checkbox';                checkbox.id = 'item-' + item.path;                checkbox.dataset.path = item.path;                checkbox.dataset.type = 'folder';                checkbox.addEventListener('change', handleFolderSelection);
                const iconSpan = document.createElement('span');                iconSpan.className = 'file-icon';                iconSpan.innerHTML = '?';
                const nameSpan = document.createElement('span');                nameSpan.className = 'file-name';                nameSpan.textContent = item.name;
                itemDiv.appendChild(toggleSpan);                itemDiv.appendChild(checkbox);                itemDiv.appendChild(iconSpan);                itemDiv.appendChild(nameSpan);
                // 添加子项容器                const childrenDiv = document.createElement('div');                childrenDiv.className = 'children';
                if (item.children && item.children.length > 0) {                    item.children.forEach(child => {                        childrenDiv.appendChild(createFileItem(child, level + 1));                    });                }
                itemDiv.appendChild(childrenDiv);            } else {                const checkbox = document.createElement('input');                checkbox.type = 'checkbox';                checkbox.id = 'item-' + item.path;                checkbox.dataset.path = item.path;                checkbox.dataset.type = 'file';                checkbox.addEventListener('change', handleFileSelection);
                const iconSpan = document.createElement('span');                iconSpan.className = 'file-icon';                iconSpan.innerHTML = getFileIcon(item.name);
                const nameSpan = document.createElement('span');                nameSpan.className = 'file-name';                nameSpan.textContent = item.name;
                // 添加文件大小                if (item.size !== undefined) {                    const sizeSpan = document.createElement('span');                    sizeSpan.style.marginLeft = '10px';                    sizeSpan.style.color = '#666';                    sizeSpan.style.fontSize = '0.9em';                    sizeSpan.textContent = formatFileSize(item.size);                    nameSpan.appendChild(sizeSpan);                }
                itemDiv.appendChild(checkbox);                itemDiv.appendChild(iconSpan);                itemDiv.appendChild(nameSpan);            }
            return itemDiv;        }
        // 获取文件图标        function getFileIcon(filename) {            const ext = filename.split('.').pop().toLowerCase();            const iconMap = {                'php''?',                'html''?',                'css''?',                'js''?',                'json''?',                'txt''?',                'md''?',                'xml''?',                'sql''?️',                'jpg''?️',                'jpeg''?️',                'png''?️',                'gif''?️',                'pdf''?',                'zip''?',                'rar''?',                '7z''?'            };
            return iconMap[ext] || '?';        }
        // 格式化文件大小        function formatFileSize(bytes) {            if (bytes === 0return '0 B';            const k = 1024;            const sizes = ['B''KB''MB''GB'];            const i = Math.floor(Math.log(bytes) / Math.log(k));            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];        }
        // 处理文件夹选择        function handleFolderSelection(event) {            const checkbox = event.target;            const folderPath = checkbox.dataset.path;            const isChecked = checkbox.checked;
            // 找到所有属于这个文件夹的子项            const children = document.querySelectorAll(`[data-path^="${folderPath}/"]`);
            children.forEach(child => {                child.checked = isChecked;                if (child.dataset.type === 'file') {                    updateSelectedFiles(child.dataset.path, isChecked);                }            });
            updateSelectedFiles(folderPath, isChecked);        }
        // 处理文件选择        function handleFileSelection(event) {            const checkbox = event.target;            const filePath = checkbox.dataset.path;            const isChecked = checkbox.checked;
            updateSelectedFiles(filePath, isChecked);        }
        // 更新选中的文件列表        function updateSelectedFiles(path, isSelected) {            if (isSelected) {                if (!selectedFiles.includes(path)) {                    selectedFiles.push(path);                }            } else {                const index = selectedFiles.indexOf(path);                if (index > -1) {                    selectedFiles.splice(index, 1);                }            }
            updateStats();        }
        // 全选        function selectAll() {            const checkboxes = document.querySelectorAll('#file-list input[type="checkbox"]');            checkboxes.forEach(checkbox => {                checkbox.checked = true;                if (checkbox.dataset.type === 'file') {                    updateSelectedFiles(checkbox.dataset.pathtrue);                }            });
            updateStats();        }
        // 取消全选        function deselectAll() {            const checkboxes = document.querySelectorAll('#file-list input[type="checkbox"]');            checkboxes.forEach(checkbox => {                checkbox.checked = false;                if (checkbox.dataset.type === 'file') {                    updateSelectedFiles(checkbox.dataset.pathfalse);                }            });
            updateStats();        }
        // 展开所有        function expandAll() {            const folders = document.querySelectorAll('.folder');            const toggles = document.querySelectorAll('.folder-toggle');
            folders.forEach(folder => {                folder.classList.add('expanded');            });
            toggles.forEach(toggle => {                toggle.innerHTML = '▼';            });        }
        // 折叠所有        function collapseAll() {            const folders = document.querySelectorAll('.folder');            const toggles = document.querySelectorAll('.folder-toggle');
            folders.forEach(folder => {                folder.classList.remove('expanded');            });
            toggles.forEach(toggle => {                toggle.innerHTML = '▶';            });        }
        // 渲染格式选项        function renderFormatOptions() {            const formatContainer = document.getElementById('format-options');            formatContainer.innerHTML = '';
            supportedFormats.forEach(format => {                const optionDiv = document.createElement('div');                optionDiv.className = 'format-option';
                const radio = document.createElement('input');                radio.type = 'radio';                radio.name = 'format';                radio.id = 'format-' + format;                radio.value = format;                if (format === 'txt') radio.checked = true;
                const label = document.createElement('label');                label.htmlFor = 'format-' + format;                label.textContent = format.toUpperCase() + (format === 'txt' ? ' (文本预览)' : ' (直接下载)');
                optionDiv.appendChild(radio);                optionDiv.appendChild(label);                formatContainer.appendChild(optionDiv);            });
            // 更新支持的格式数量            document.getElementById('supported-formats').textContent = supportedFormats.length;        }
        // 生成文件        function generateFile() {            if (selectedFiles.length === 0) {                showStatus('请至少选择一个文件''error');                return;            }
            const format = document.querySelector('input[name="format"]:checked').value;            const filename = document.getElementById('output-filename').value || '打包文件';
            showStatus('正在生成文件...''info');            document.getElementById('progress-container').style.display = 'block';            document.getElementById('preview-area').style.display = 'none';
            // 模拟进度条            let progress = 0;            const progressBar = document.getElementById('progress-bar');            const progressInterval = setInterval(() => {                progress += 5;                progressBar.style.width = progress + '%';
                if (progress >= 100) {                    clearInterval(progressInterval);
                    // 实际生成文件                    fetch('', {                        method'POST',                        headers: {                            'Content-Type''application/json',                        },                        bodyJSON.stringify({                            action'generate',                            files: selectedFiles,                            format: format,                            filename: filename                        })                    })                    .then(response => {                        if (format === 'txt') {                            return response.json();                        } else {                            if (!response.ok) {                                return response.json().then(err => { throw new Error(err.message); });                            }                            return response.blob();                        }                    })                    .then(data => {                        if (format === 'txt') {                            if (data.success) {                                // 对于TXT文件,显示内容预览                                showFilePreview(data.content, data.filename);                                showStatus('文件生成成功,请查看预览''success');                            } else {                                throw new Error(data.message);                            }                        } else {                            // 对于压缩文件,直接下载                            downloadFile(data, `${filename}.${format === 'gz' ? 'tar.gz' : format}`);                            showStatus('文件已生成并开始下载''success');                        }
                        document.getElementById('progress-container').style.display = 'none';                    })                    .catch(error => {                        console.error('Error:', error);                        showStatus('生成文件时出错: ' + error.message'error');                        document.getElementById('progress-container').style.display = 'none';                    });                }            }, 100);        }
        // 显示文件内容预览(TXT格式)        function showFilePreview(content, filename) {            const previewArea = document.getElementById('preview-area');            const previewTitle = document.getElementById('preview-title');            const previewContent = document.getElementById('preview-content');
            previewTitle.textContent = filename + ' 内容预览';            previewContent.value = content;            previewArea.style.display = 'block';
            // 滚动到预览区域            previewArea.scrollIntoView({ behavior'smooth' });        }
        // 下载预览文件        function downloadPreview() {            const content = document.getElementById('preview-content').value;            const filename = document.getElementById('output-filename').value || '打包文件';            downloadTxt(content, filename + '.txt');        }
        // 下载TXT文件        function downloadTxt(content, filename) {            const blob = new Blob([content], { type'text/plain; charset=utf-8' });            downloadFile(blob, filename);        }
        // 下载文件        function downloadFile(blob, filename) {            const url = window.URL.createObjectURL(blob);            const a = document.createElement('a');            a.href = url;            a.download = filename;            document.body.appendChild(a);            a.click();            document.body.removeChild(a);            window.URL.revokeObjectURL(url);        }
        // 显示状态消息        function showStatus(message, type) {            const statusDiv = document.getElementById('status-message');            statusDiv.textContent = message;            statusDiv.className = `status ${type}`;            statusDiv.style.display = 'block';
            // 自动隐藏成功消息            if (type === 'success') {                setTimeout(() => {                    statusDiv.style.display = 'none';                }, 5000);            }        }
        // 显示统计信息        function showStats() {            const totalFiles = countFiles(fileStructure);            const totalFolders = countFolders(fileStructure);
            document.getElementById('total-files').textContent = totalFiles;            document.getElementById('total-folders').textContent = totalFolders;            updateStats();        }
        // 更新统计信息        function updateStats() {            const selectedCount = document.getElementById('selected-count');            selectedCount.textContent = selectedFiles.length;        }
        // 计算文件数量        function countFiles(structure) {            let count = 0;
            structure.forEach(item => {                if (item.type === 'file') {                    count++;                } else if (item.type === 'folder' && item.children) {                    count += countFiles(item.children);                }            });
            return count;        }
        // 计算文件夹数量        function countFolders(structure) {            let count = 0;
            structure.forEach(item => {                if (item.type === 'folder') {                    count++;                    if (item.children) {                        count += countFolders(item.children);                    }                }            });
            return count;        }    </script></body></html>


打赏

本文链接:https://kinber.cn/post/5762.html 转载需授权!

分享到:


推荐本站淘宝优惠价购买喜欢的宝贝:

image.png

 您阅读本篇文章共花了: 

群贤毕至

访客