erikwang2013 / aetherupload-webman
AetherUpload for webman and six more frameworks: chunked large-file upload with resumable transfer, instant upload (sec秒传) and integrity check. Framework-agnostic kernel with adapters for webman, Laravel, ThinkPHP, Symfony, Slim, Hyperf and Yii2.
Package info
github.com/erikwang2013/aetherupload-webman
pkg:composer/erikwang2013/aetherupload-webman
Requires
- php: ^8.0
- symfony/translation: ^5.4 || ^6.0 || ^7.0
- webman/event: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.6 || ^10.5
- symfony/console: ^6.0 || ^7.0
- webman/console: ^1.0 || ^2.1.7
Suggests
- predis/predis: 开启秒传(instant_completion)所需;未安装时秒传不可用,其余功能不受影响
- webman/console: webman 下运行 aetherupload:groups / :build / :clean 三个控制台命令所需(webman 应用的 CLI 本身即由它提供)
Provides
None
Conflicts
None
Replaces
None
README
中文 · English · 한국어 · Русский · Deutsch · Français · Español · Português · हिन्दी · العربية · বাংলা · Bahasa Indonesia · 日本語
本项目移植自广受好评的 Laravel 大文件上传扩展包 AetherUpload-Laravel,并为 webman 的常驻进程模型重写了配置读取、并发处理与存储层。
它解决什么
浏览器直接上传大文件,绕不开三个难题:超过 post_max_size 就传不上去;网络一断就得从头再来;同样的文件被反复传输。AetherUpload 的做法是——在浏览器里把文件切片,逐块追加到服务端的一个临时文件,落盘时用文件内容的 md5 命名。于是上传、断线续传、秒传、去重、完整性校验共用同一套机制,全程不把整个文件读进内存,也不需要为「文件在哪」维护一张数据库表。
它的形状
一个 composer 包,内核与宿主框架解耦:同一份代码在 webman、原生 PHP(无框架)、Laravel、ThinkPHP、Symfony、Slim、Hyperf、Yii2 下可用(见 支持的框架)。宿主的配置、路由、控制台命令、语言文件与前端脚本由安装 / 发布命令分发;不依赖数据库;Redis 只在开启秒传时才需要,属可选依赖。
项目结构
aetherupload-webman/
├── src/ 插件源码
│ ├── Runtime.php 宿主绑定点(静态门面):进程级只持有不可变绑定,未绑定时明确报错
│ ├── RequestContext.php 每请求 / 每协程的可变状态(分组配置快照、已加载语种),常驻进程下并发请求不串组
│ ├── Contract/ 11 个接口(配置 / 翻译 / 请求 / 上传文件 / 响应 / Redis / 事件 / 路径 / 文件系统 / 上下文 / 适配器)
│ ├── Kernel/ 内核侧默认实现:AbstractAdapter、PrefixedConfig、Filesystem、NullRedis、PhpFileTranslator、ClientRedis…
│ ├── Console/ 三个命令的业务逻辑(Runner)+ 无控制台约定宿主的现成入口,七种命令壳共用同一份
│ ├── Adapter/ 八个适配器:Webman / Native / Laravel / ThinkPhp / Symfony / Slim / Hyperf / Yii
│ ├── UploadController.php 上传入口:preprocess(预处理 / 秒传判定)与 saveChunk(分块写入)
│ ├── ResourceController.php 展示与下载入口:display / download,支持交给 nginx 直发
│ ├── PartialResource.php 分块文件本体:路径拼装、逐块追加、重命名、大小与类型校验
│ ├── Header.php 断点状态文件:只存一个 chunkIndex
│ ├── Resource.php 成品文件对象,作为参数注入「上传完成」事件
│ ├── RedisSavedPath.php 秒传索引:每条记录一个 key,独立 TTL
│ ├── SavedPathResolver.php 存储路径编解码(三段式无状态寻址)
│ ├── ConfigMapper.php 配置单例 + 每请求分组配置快照
│ ├── MimeType.php mime ↔ 扩展名映射,落盘前按白名单复核
│ ├── Util.php 临时名生成、路径安全校验、资源删除
│ ├── Install.php 安装 / 卸载:向宿主应用分发配置与资源
│ ├── Responser.php JSON 响应封装
│ ├── SimpleValidateTrait.php 极简必填校验
│ ├── ExamplePageTrait.php 示例页
│ └── helpers.php 模板助手:aetherupload_display_link / aetherupload_download_link
├── commands/ webman 的控制台壳(安装时复制到 app/command,逻辑在 src/Console)
│ ├── AetherUploadListGroups.php php webman aetherupload:groups 列出分组并创建对应目录
│ ├── AetherUploadBuildRedisHashes.php php webman aetherupload:build 按磁盘现状重建秒传索引
│ └── AetherUploadCleanUpDirectory.php php webman aetherupload:clean N 按 mtime 清理过期临时文件
├── config/
│ ├── app.php webman 插件配置:分组、路由、中间件与各类开关
│ ├── aetherupload.php 框架无关的同一份配置,供其余七个适配器作基线(两条路径由 ConfigParityTest 锁死一致)
│ └── route.php 四条路由 + 各自的中间件挂载点
├── docs/ 文档、图片与前端脚本
│ ├── aetherupload-architecture.svg 设计架构图
│ ├── aetherupload-design.svg 设计思路图
│ ├── aetherupload-lifecycle.svg 上传生命周期图
│ ├── HARNESS.md 单元测试契约(编写测试前必读)
│ ├── REPORT.md 某次全量测试与覆盖率的摘要报告
│ ├── i18n/ 多语言 README 与三张设计图的译文(12 种语言,译文导航见 TRANSLATING.md)
│ └── js/ 前端资源(发布到 <文档根>/vendor/aetherupload/js)
│ ├── aetherupload-all.js 打包版:核心 + zepto + spark-md5
│ ├── aetherupload-core.js 核心逻辑:算 hash、切片、上传、进度、断线重试
│ └── aetherupload-pet.svg 项目宠物「以太兽」,同时用作示例页图标与站点图标
├── translations/{zh,en}/ 多语言(发布到宿主的 translations 路径下 aetherupload/)
├── views/example.blade.php 示例页源码,接入时可直接参考
├── tests/
│ ├── *.php PHPUnit 单元用例(宿主用替身,PHP 8.0–8.4,PHPUnit 9.6 与 10.5 双版本)
│ └── Integration/<fw>/ 端到端套件,每个框架一套:真装框架、真跑上传链路(webman 走 phpunit.xml 起真服务,其余七个走各自的 ci.sh)
├── uploads/ 遗留目录,插件运行时不使用
└── composer.json
src/根目录下那 15 个文件(控制器、分块、断点、秒传索引、路径编解码、错误响应…)是与框架无关的上传内核:它们不引用任何宿主命名空间,配置 / 翻译 / 请求 / 响应 / Redis / 事件全部经Runtime取端口。 唯一的例外是Install.php——composer require触发的安装脚本跑在宿主应用起来之前,Runtime还没被绑上,它在那种情况下会兜底自绑 webman 适配器并回落到包内的config/app.php。
安装前这些都是包内文件;
composer require之后由Install.php按上表标注的映射,把配置、命令、前端脚本与语言文件分发到宿主应用的对应位置。
设计架构
- 客户端:引入一个
aetherupload-all.js即可(内含 zepto 与 spark-md5),它负责算 md5、切片、进度条与断线自动重试。 - 服务端:
UploadController只有两个入口(预处理、分块写入);PartialResource管路径、追加与重命名,Header只记 chunkIndex;配置经ConfigMapper按请求快照,避免常驻进程下并发请求互相串组。 - 存储与运维:磁盘上永远只有三类文件——
*.part分块、_header/断点、<md5>.<ext>成品;Redis 仅在开启秒传时使用;开启x_accel_redirect后由 nginx 直发文件,顺带补上 Range 与断点下载。
设计思路
图中三条决策之外,还有一条独立的选择:秒传索引做成可选依赖 + 每条记录独立 TTL。不装 Redis 的站点照样能上传(只是秒传失效);装了 Redis 的站点,每条记录用 SETEX 各自过期——既不共享过期时间,也不会因为持续上传而无限膨胀;升级后旧 hash 记录仍可回退读取。一致性交给 aetherupload:build(每日重建)与 aetherupload:clean(按 mtime 回收临时文件)。
上传生命周期
主路径只有四步:预处理 → 分块(循环)→ 最后一块校验 → 落盘。预处理建空的 .part 并把 chunkIndex=0 写进 header;每个分块的顺序固定为「校验 → 追加 → 回写 chunkIndex」;只有最后一块才会校验大小与 MIME、重算整份 md5、改名成 <md5>.<ext> 并写秒传索引。
两条旁路值得单独说明:
- 秒传命中:预处理阶段发现相同 md5 已有成品,直接返回其路径,一个分块都不用传。
- 中断与恢复:重发同一序号会被幂等跳过;序号跳变或分块被截断只返回错误,不会清理
.part与 header,所以弱网下客户端可以持续重试直到补上缺的那一块。真正会清理的只有两种情况——最后一块校验不通过(整份丢弃),以及页面关闭后由 cron 跑aetherupload:clean按 mtime 回收。
功能特性
- 百分比进度条
- 文件类型限制
- 文件大小限制
- 多语言支持
- 资源分组配置
- 上传完成事件
- 同步上传 ①
- 断线续传 ②
- 文件秒传 ③
- 自定义中间件 ④
- 自定义路由
- 宽松模式
①:同步上传相比异步上传,在上传带宽足够大的情况下速度稍慢,但同步可在上传同时进行文件的拼合,而异步因文件块上传完成的先后顺序不确定,需要在所有文件块都完成时才能拼合,将会导致异步上传在接近完成时需等待较长时间。同步上传每次只有一个文件块在上传,在单位时间内占用服务器的内存较少,相比异步方式可支持更多人同时上传。
②:断线续传和断点续传不同,断线续传是指遇到断网或无线网络不稳定时,在不关闭页面的情况下,上传组件会定时自动重试,一旦网络恢复,文件会从未上传成功的那个文件块开始继续上传。断线续传在刷新页面或关闭后重开是无法续传的,之前上传的部分已成为无效文件。
③:文件秒传需服务端Redis和客户端浏览器支持(FileReader、File.slice()),两者缺一则秒传功能无法生效。默认关闭,需在配置文件中开启。
④:结合自定义中间件,可对已上传资源的访问、下载行为进行权限控制。
支持的框架
内核(分块、续传、秒传、校验、寻址)与宿主框架解耦,同一份包在下列宿主下可用,每个都有真装框架、真跑完整上传链路的端到端测试兜底:
| 宿主 | 接入方式 | 端到端测试 |
|---|---|---|
| webman | 原生支持(composer require 即自动分发配置/路由/命令/前端脚本) |
tests/Integration/webman/ |
| 原生 PHP(无框架) | 一行 Bootstrap::handle(),路由由本包按配置分发 |
tests/Integration/native/ |
| Laravel | ServiceProvider + vendor:publish |
tests/Integration/laravel/ |
| ThinkPHP | app/service.php 里注册 Service |
tests/Integration/thinkphp/ |
| Symfony | Bundle + 路由资源导入 | tests/Integration/symfony/ |
| Slim | 一行 Bootstrap::create() |
tests/Integration/slim/ |
| Hyperf | ConfigProvider |
tests/Integration/hyperf/ |
| Yii2 | 应用 bootstrap 里挂 Bootstrap |
tests/Integration/yii/ |
包名里的
webman是历史原因(本插件最初只支持 webman),不影响在其余宿主下使用。
通用两步
无论哪个宿主,装完包之后有两条必做动作(webman 由安装脚本自动完成,其余宿主需要手动跑对应命令):
- 建存储目录:
aetherupload:groups—— 创建root_dir、_header与各分组目录。 不建就一定失败:内核的createGroupSubDir()是非递归mkdir,父目录缺失时直接返回 false,而错误会被统一翻译成笼统的upload_error,排查时很难看出是目录问题。 - 发布文件:
aetherupload:publish(Laravel 用vendor:publish --tag=aetherupload-*)—— 把语言文件与前端js放进宿主可访问的位置。
命令名的分隔符跟着各框架的控制台约定:webman / Laravel / ThinkPHP / Symfony / Hyperf / Slim / 原生 PHP 用
:,Yii 用/(php yii aetherupload/groups)。下面每家的示例都是可直接复制运行的写法。
各框架接入
原生 PHP(无框架)
任何跑在 PHP 上的应用都能直接用 —— 不装框架、不装中间件、不注册服务,入口脚本一行:
// public/index.php(FPM / Apache / nginx+php-fpm 的入口脚本) require __DIR__ . '/../vendor/autoload.php'; exit(\AetherUpload\Adapter\Native\Bootstrap::handle([ 'base_path' => dirname(__DIR__), 'config' => require __DIR__ . '/../config/aetherupload.php', // 省略则用包内默认值 'redis' => static fn () => new \Redis(), // 秒传需要,可选 ]));
php bin/aetherupload aetherupload:groups # 建目录 php bin/aetherupload aetherupload:publish # 发布语言文件与前端 js
bin/aetherupload 也是三行,自己放一份即可:
#!/usr/bin/env php <?php require __DIR__ . '/../vendor/autoload.php'; \AetherUpload\Adapter\Native\Bootstrap::bind(require __DIR__ . '/../config/aetherupload.php', dirname(__DIR__)); exit((new \AetherUpload\Console\Application())->run());
路由不用自己写:
Bootstrap::handle()按配置里的route_preprocess/route_uploading/route_display/route_download分发当前请求,未命中 404、方法不对 405 并给出Allow。 中间件:配置里的middleware_*在这一宿主下是无参可调用对象,返回响应对象即短路 —— 权限不够直接return Runtime::response()->text('forbidden', 403);,返回别的值一律无视并继续(四号脚注说的权限控制就是这么接)。 内置服务器:php -S 127.0.0.1:8080 -t public public/index.php即可。要让内置服务器自己发public/里的静态文件(发布出来的前端 js 走这条),在handle()之前加一句if (is_file(__DIR__ . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))) { return false; }。 不用内置函数兜底:请求读$_GET/$_POST/$_FILES(PHP 不额外过滤,内核的类型守卫照常生效),响应由NativeResponse::send()一次性header()+echo发出,没有框架的中间层。
Laravel
// bootstrap/providers.php(Laravel 11+)或 config/app.php 的 providers 数组 AetherUpload\Adapter\Laravel\AetherUploadServiceProvider::class,
php artisan vendor:publish --tag=aetherupload-config # 发布 config/aetherupload.php php artisan vendor:publish --tag=aetherupload-translations # 发布语言文件 php artisan vendor:publish --tag=aetherupload-assets # 发布前端 js php artisan aetherupload:groups
本包 composer.json 不含
extra.laravel.providers(各框架依赖互斥,无法写死自动发现),因此必须手动注册 provider。 配置合并是浅合并:应用一旦发布了config/aetherupload.php,其中的groups会整体替换插件默认值——新增分组时请把默认分组一并抄进去。
ThinkPHP
// app/service.php return [ \AetherUpload\Adapter\ThinkPhp\AetherUploadService::class ];
php think aetherupload:groups php think aetherupload:publish
同样注意
groups是整体替换;另外config/route.php与config/lang.php是必需文件,缺失会让框架在array_merge/array_change_key_case处收 null 并抛 TypeError。
Symfony
// config/bundles.php AetherUpload\Adapter\Symfony\AetherUploadBundle::class => ['all' => true],
# config/routes.yaml(Symfony 的 bundle 路由必须由应用侧 import,没有自动加载机制) aetherupload: resource: '@AetherUploadBundle/Resources/config/routes.php' type: php
Configuration树声明了全部配置键,未声明的键会让容器启动失败(不是静默忽略)。
Slim
// public/index.php $app = \AetherUpload\Adapter\Slim\Bootstrap::create([ 'config' => require __DIR__ . '/../config/aetherupload.php', // 省略则用包内默认值 'base_path' => dirname(__DIR__), 'redis' => static fn () => new Predis\Client(['database' => 0]), // 秒传需要 ]); $app->run();
Slim 的 PSR-7 响应不可变,内核产物由
Bootstrap::handler()那层落成真 PSR-7(唯一的转换点,放中间件里是穿不过去的)。 Slim 没有控制台约定,本包给出四条命令的现成入口类,入口脚本要自己放一份(四行):// bin/aetherupload require __DIR__ . '/../vendor/autoload.php'; \AetherUpload\Adapter\Slim\Bootstrap::bind(require __DIR__ . '/../config/aetherupload.php', dirname(__DIR__)); exit((new \AetherUpload\Adapter\Slim\Console\Application())->run());php bin/aetherupload aetherupload:groups # 另有 publish / build / clean需要
symfony/console(本包 require-dev / suggest)。
Hyperf
// config/config.php —— 本包未写 extra.hyperf.config(会与其它框架的自动发现机制相互干扰, // 待独立验过后再补),因此在这里显式展开 ConfigProvider(路由、命令、publish 键都由它给出) return [ // … ] + (new \AetherUpload\Adapter\Hyperf\ConfigProvider())();
php bin/hyperf.php aetherupload:groups php bin/hyperf.php aetherupload:publish
需要
ext-swoole。另外两个开发期注意点:xdebug.mode=profile会让 Hyperf 冷启动静默退出(ClassLoader::init()里 exit 255,无任何 PHP 报错),CI 与本地请设XDEBUG_MODE=off;swoole 的Coroutine\run()不能与 PHPUnit 同进程,协程相关测试要把探针放到独立进程里跑。
Yii2
// 应用配置 return [ 'bootstrap' => ['aetherupload' => \AetherUpload\Adapter\Yii\Bootstrap::class], 'params' => ['aetherupload' => require __DIR__ . '/aetherupload.php'], // 可基于 publish 生成的样例 'components' => [ // 前置条件:必须写在 components 下。顶层同名键会被 Yii 忽略, // 那样 enableStrictParsing 不生效,路由的 verb 约束会被绕过(GET 打到只声明 POST 的动作) 'urlManager' => ['enableStrictParsing' => true], // 秒传需要;控制台应用也要配,否则 aetherupload/build 会报 Unknown component ID: redis 'redis' => [ /* … */ ], ], ];
php yii aetherupload/groups # 注意:Yii 的控制台分隔符是 "/",写成 aetherupload:groups 会报 Unknown command
php yii aetherupload/publish
发布落点是文档根下的
vendor/aetherupload/js(webman 的文档根是public/,Yii 的是web/):示例页引用的是绝对路径/vendor/aetherupload/js/…,语义是"文档根",所以YiiPaths::assetPath()优先取宿主的@webroot别名,取不到时退回<app>/web。
webman
# 在 webman 项目根目录执行
composer require erikwang2013/aetherupload-webman
webman 是唯一零配置的宿主:
composer require时由Install.php自动分发配置、路由、命令、语言文件与前端脚本,并建好存储目录。装完直接访问http://域名/aetherupload即为示例页。提示:更改相关配置选项请编辑
config/plugin/erikwang2013/aetherupload-webman/app.php。
其余七个宿主必须先注册适配器,再跑「通用两步」。webman 也提供同样的命令(
php webman aetherupload:groups/aetherupload:publish),只是安装时已自动跑过一遍。
使用
文件上传
参考示例文件及注释,在需要上传大文件的页面引入相应文件及代码。
分组配置
在本插件配置文件的groups下新增分组,运行php webman aetherupload:groups自动创建对应目录。
'video' => [ 'group_dir' => 'video', 'resource_maxsize' => 0, 'resource_extensions' => [], 'event_before_upload_complete' => false, 'event_upload_complete' => false, ],
在前端通过调用setGroup('分组名')方法指定上传分组,注意分组名必须已经存在,且不能包含下划线(它参与存储路径编码,含下划线会导致该分组下的资源无法被定位,配置与上传阶段即会失败)。
添加秒传功能(需Redis及浏览器支持)
参考Webman文档Redis部分,安装所需依赖。
安装Redis并启动服务。
安装predis包composer require predis/predis。
在config/redis.php中设置client为predis。
在本插件配置文件中将instant_completion设置为true。
提示:在Redis中维护了一份与实际资源文件对应的秒传清单,实际资源文件的增删造成的变化均需要同步到秒传清单中,否则会产生脏数据。
扩展包已包含新增部分,当需要删除资源文件时,使用者需手动调用对应方法删除文件及秒传清单中的记录。
\AetherUpload\Util::deleteResource($savedPath); //删除对应的资源文件 \AetherUpload\Util::deleteRedisSavedPath($savedPath); //删除对应的Redis秒传记录
两者均为幂等操作:文件或秒传记录已不存在时同样返回true。
自定义中间件
参考Webman文档路由中间件部分,创建你的中间件并将你编写的中间件名称填入配置文件对应部分。
'middleware_download' => [app\middleware\MiddlewareA::class,app\middleware\MiddlewareB::class],
可使用此功能对文件进行上传、访问、下载行为的权限控制。
自定义路由
在本插件配置文件下编辑'route_uploading' => '/aetherupload/uploading'等选项,在前端调用setUploadingRoute('/aetherupload/uploading')等方法。
文件访问、下载路由编辑后,可直接访问,无需调用前端方法。
上传完成事件
分为上传完成前、上传完成后事件,参考Webman文档常用组件Event事件部分。
在config/event.php中为aetherupload.before_upload_complete及aetherupload.upload_complete配置对应的事件处理类。
在本插件配置文件中将groups下相应选项设置为true。
'event_before_upload_complete' => true, 'event_upload_complete' => true,
可使用此功能在上传完成前、上传完成后进行额外处理。
宽松模式
在本插件配置文件下编辑'lax_mode' => true,,在前端调用setLaxMode(true)方法。
通过上传前跳过计算hash,可缩短总耗时。此选项开启后,无法进行秒传和完整性校验。
多语言
前端检测浏览器语言后自动设置,目前支持中、英。
使用方便的控制台命令
php webman aetherupload:groups 列出所有分组并自动创建对应目录
php webman aetherupload:build 在Redis中重建资源文件的秒传清单
php webman aetherupload:clean 2 清除2天前的无效临时文件
优化建议
- (推荐)设置每天自动清除无效的临时文件
由于上传流程存在意外终止的情况,如在传输过程中强行关闭页面或浏览器,将会导致已产生的文件部分成为无效文件,占据大量的存储空间,我们可以使用crontab的定时任务功能来定期清除它们。
在Linux中运行crontab -e命令,确保文件中包含这行代码:
0 0 * * * php /项目根目录的绝对路径/webman aetherupload:clean 1> /dev/null 2>&1
- 设置每天自动重建Redis中的秒传清单
不恰当的处理和某些极端情况可能使秒传清单中出现脏数据,从而影响到秒传功能的准确性,重建秒传清单可消除脏数据,恢复与实际资源文件的同步。
在Linux中运行crontab -e命令,确保文件中包含这行代码:
0 0 * * * php /项目根目录的绝对路径/webman aetherupload:build 1> /dev/null 2>&1
- (可选)启用nginx内部重定向,支持大文件断点续传与视频拖动
webman的文件响应不实现HTTP Range,大文件下载会整包下发,视频无法拖动进度条,且整个传输期间占用一个worker进程。若部署在nginx下,可开启本插件的x_accel_redirect选项,文件改由nginx直接发送,worker立即释放,并支持Range(断点续传、视频拖动)。
在本插件配置文件下编辑'x_accel_redirect' => true,,并在nginx配置中为内部前缀添加location,alias指向项目上传根目录的绝对路径(注意结尾的斜杠):
location /internal-aetherupload/ { internal; alias /项目根目录的绝对路径/storage/app/aetherupload/; }
该location必须保留internal,防止外部绕过程序直接访问资源文件;修改root_dir后alias需同步调整。
-
提高分块临时文件读写速度(仅对PHP生效)
利用Linux的tmpfs文件系统,来达到将上传的分块临时文件放到内存中快速读写的目的,通过以空间换时间,提升读写效率,将会额外占用部分内存(约1个分块大小)。
将php.ini中上传临时目录upload_tmp_dir项的值设置为"/dev/shm",重启服务。 -
提高分块临时文件读写速度(对系统临时目录生效)
利用Linux的tmpfs文件系统,来达到将上传的分块临时文件放到内存中快速读写的目的,通过以空间换时间,提升读写效率,将会额外占用部分内存(约1个分块大小)。
执行以下命令:
mkdir /dev/shm/tmp
chmod 1777 /dev/shm/tmp
mount --bind /dev/shm/tmp /tmp
兼容性
| IE | Edge | Firefox | Chrome | Safari | |
|---|---|---|---|---|---|
| 上传 | 10+ | 12+ | 3.6+ | 6+ | 5.1+ |
| 秒传 | 10+ | 12+ | 3.6+ | 6+ | 6+ |
安全性
AetherUpload在上传前使用白名单+黑名单的形式进行文件后缀名过滤,上传后再检查文件的Mime-Type类型。白名单直接限制了保存文件扩展名,黑名单默认屏蔽了常见的可执行文件扩展名,来阻止上传恶意文件,安全起见白名单一栏不应留空。
虽然做了诸多安全工作,但恶意文件上传是防不胜防的,建议正确设置上传目录权限,确保相关程序对资源文件没有执行权限。