💾 文件存储
ThinkAdmin 通过 think\admin\Storage 提供统一文件存储入口,默认按系统配置 storage.type 选择存储驱动。当前内置驱动包括本地存储、Alist、七牛云、又拍云、腾讯云 COS 和阿里云 OSS。
该能力主要覆盖后台上传、文件访问地址生成、远程文件下载缓存、Base64 图片保存等场景。上传后的文件记录由后台上传接口写入 system_file,存储驱动本身只负责文件读写和地址生成。
存储驱动
Storage::types() 返回当前支持的驱动:
| 驱动值 | 存储方式 |
|---|---|
local | 本地服务器存储 |
alist | 自建 Alist 存储 |
qiniu | 七牛云对象存储 |
upyun | 又拍云 USS 存储 |
txcos | 腾讯云 COS 存储 |
alioss | 阿里云 OSS 存储 |
默认上传配置由后台插件初始化写入,常见关键项包括:
| 配置项 | 说明 |
|---|---|
storage.type | 默认存储驱动,初始值为 local |
storage.allow_exts | 服务端允许上传的后缀列表 |
storage.name_type | 前端上传命名方式,后台配置页初始化为 xmd5 |
storage.link_type | 链接附加策略,包含 compress 或 full 时影响部分 URL 后缀 |
后台上传接口始终会校验
storage.allow_exts,并额外禁止sh、asp、bat、cmd、exe、php等常见可执行后缀。
统一接口
所有驱动实现 think\admin\contract\StorageInterface:
use think\admin\Storage;
$storage = Storage::instance(); // 使用 storage.type 配置的默认驱动
$qiniu = Storage::instance('qiniu'); // 指定七牛云驱动
$info = $storage->set('image/ab/cdef.jpg', $content);
$content = $storage->get('image/ab/cdef.jpg');
$exists = $storage->has('image/ab/cdef.jpg');
$url = $storage->url('image/ab/cdef.jpg');
$path = $storage->path('image/ab/cdef.jpg');
$info = $storage->info('image/ab/cdef.jpg');
$deleted = $storage->del('image/ab/cdef.jpg');
$uploadUrl = $storage->upload();set() 与 info() 的成功返回值以驱动实现为准,通常会包含:
[
'url' => 'https://example.com/upload/image/ab/cdef.jpg',
'key' => 'image/ab/cdef.jpg',
'file' => '/absolute/path/or/remote/url',
]url 是业务最终使用的访问地址或安全文件键;key 与 file 的具体值与驱动有关。本地存储 info() 的 key 会返回带 upload/ 前缀的键,file 为服务器文件路径;云存储和 Alist 通常返回对象键本身,file 多数为可访问 URL。
存储驱动不会统一返回 hash 字段。后台上传接口会把前端计算出的文件 MD5 写入 system_file.hash,这是上传记录字段,不是存储接口返回字段。
文件命名
Storage::name() 按指定函数计算传入字符串,并生成相对文件键。该方法常用于服务端保存自定义文件,标准后台上传组件会在浏览器端按 storage.name_type 生成文件键:
use think\admin\Storage;
$name = Storage::name($source, 'jpg', 'image');
// image/ab/cdef1234567890abcdef1234567890.jpg实际规则:
- 执行
$fun($source),默认$fun为md5。 - 取哈希前 2 位作为一层目录。
- 取哈希第 3 位开始的 30 位作为文件名。
- 使用传入后缀拼接最终文件键。
// 以文件内容生成键,适合服务端自定义上传
$name = Storage::name($file->getPathname(), $ext, 'upload', 'md5_file');
// 使用 sha1 生成键
$name = Storage::name($value, 'jpg', 'image', 'sha1');注意:默认 md5 是对传入字符串本身取哈希。如果传入临时文件路径,需要用 md5_file 才是按文件内容生成键。
后台上传命名
后台上传脚本会读取 storage.name_type:
xmd5:读取文件内容并计算 SparkMD5,生成类似ab/cdef...jpg的键,支持秒传。date:生成yyyyMMddHHmmss-随机数风格的键,并整理成日期目录,不计算真实文件 MD5,因此不支持基于内容的秒传。
当组件设置了 data-path="avatar" 时,前端会把该前缀拼入最终键,例如:
avatar/ab/cdef1234567890abcdef1234567890.jpgdata-path 会被前端清理为只包含字母、数字、下划线、短横线和路径分隔层级。若 storage.name_type 为空,上传脚本会按 xmd5 处理。
秒传流程
ThinkAdmin 的标准上传组件内置秒传流程:
- 前端计算文件
xmd5和xkey。 - 请求
/admin/api.upload/state。 - 服务端通过
Storage::instance($uptype)->info($key, $safe, $name)判断文件是否已存在。 - 已存在时返回业务成功码
200,前端直接完成上传。 - 不存在时返回业务码
404和存储授权参数,前端继续上传文件。
秒传依赖“同一存储驱动 + 同一文件键”能查到文件。若使用日期随机命名,或者修改了存储前缀、压缩后文件内容、目标驱动,秒传结果会相应变化。
需要注意返回字段的差异:继续上传时接口返回的 key 是前端提交的对象键;秒传成功时接口会合并存储驱动 info() 的结果,本地存储的 key 会变成 upload/{对象键},云存储和 Alist 一般仍为对象键本身。业务表单通常不读取 key,而是使用最终的 url。
安全目录
safe 参数用于本地安全目录:
use think\admin\Storage;
$result = Storage::instance('local')->set('private/report.pdf', $content, true);
$content = Storage::instance('local')->get('private/report.pdf', true);本地驱动的安全目录行为:
- 普通文件写入
public/upload。 - 安全文件写入
safefile。 url($name, true)返回文件键本身,不返回公开访问 URL。
后台上传组件设置 data-safe="true" 时,前端会强制使用 local 上传。云存储驱动的方法签名保留了 $safe 参数,但没有提供等同本地 safefile 的私有目录语义。
安全文件通常需要业务控制器自行鉴权后读取并输出:
public function download()
{
$name = input('name');
$content = Storage::instance('local')->get($name, true);
if ($content === '') {
$this->error('文件不存在');
}
return response($content, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . basename($name) . '"',
]);
}图片链接后缀
StorageUsageTrait::getSuffix() 会根据 storage.link_type 附加链接后缀:
- 包含
compress:仅对png、jpg、jpeg图片追加服务商图片处理参数,本地存储不追加压缩参数。 - 包含
full:当传入$attname时追加下载文件名参数。
部分驱动的压缩后缀:
| 驱动 | 图片压缩后缀 |
|---|---|
qiniu | ?imageslim |
upyun | !/format/webp |
txcos | ?imageMogr2/format/webp |
alioss | ?x-oss-process=image/format,webp |
local | 无 |
远程下载与 Base64 图片
下载远程文件
Storage::down() 始终下载到本地存储,并使用 down/ 前缀缓存:
$info = Storage::down('https://example.com/logo.png', false, 86400);参数说明:
$force为true时强制重新下载。$expire为缓存有效秒数,0表示不过期。- 下载失败时会返回原始 URL 组成的兜底数组。
保存 Base64 图片
Storage::saveImage() 只接受 data:image/{gif|png|jpg|jpeg};base64,... 形式的图片:
$info = Storage::saveImage($base64, 'avatar');当第三个参数 $safemode 为 true 时,会使用本地安全目录保存:
$info = Storage::saveImage($base64, 'avatar', true);如果传入内容不匹配 data:image/...;base64,... 格式,方法不会抛错,也不会写入文件,而是直接返回 ['url' => $base64]。如果匹配到了图片数据但后缀不在 gif、png、jpg、jpeg 范围内,会抛出内容格式异常。
云存储区域
区域列表由各云存储驱动的静态 region() 返回,数组键是接口端点或服务商配置值,数组值是展示名称:
use think\admin\storage\QiniuStorage;
use think\admin\storage\AliossStorage;
use think\admin\storage\TxcosStorage;
$qiniu = QiniuStorage::region();
// ['up.qiniup.com' => '华东-浙江', ...]
$alioss = AliossStorage::region();
// ['oss-cn-hangzhou.aliyuncs.com' => '华东 1(杭州)', ...]
$txcos = TxcosStorage::region();
// ['cos.ap-shanghai.myqcloud.com' => '中国大陆 公有云地域 上海', ...]local、alist、upyun 当前返回空区域列表。
后缀与 MIME
Storage::mime() 和 Storage::mimes() 只提供后缀到 MIME 的映射,不能替代服务端安全校验:
$mime = Storage::mime('jpg,png');
// image/jpeg,image/png
$mimes = Storage::mimes();自定义上传接口仍应至少校验登录态、允许后缀、文件大小和业务权限:
$file = $this->request->file('file');
$ext = strtolower($file->extension());
$allowExts = str2arr(sysconf('storage.allow_exts|raw'));
if (!in_array($ext, $allowExts)) {
$this->error('不支持的文件类型!');
}
if ($file->getSize() > 2 * 1024 * 1024) {
$this->error('文件大小超过限制!');
}常见问题
如何切换存储驱动?
在后台存储配置中修改 storage.type,或按业务直接指定驱动:
$info = Storage::instance('qiniu')->set($name, $content);切换驱动不会迁移历史文件。历史文件需要按 system_file 或本地文件列表自行迁移。
为什么秒传没有生效?
优先检查:
- 后台上传命名是否为文件 MD5 模式。
- 文件是否上传到同一个存储驱动。
data-path或业务前缀是否一致。- 图片是否在前端被压缩、裁剪后导致内容变化。
如何批量迁移文件?
当前存储接口没有目录遍历方法。批量迁移通常需要先从 system_file 表或本地文件系统整理文件键,再逐个执行旧存储 get() 和新存储 set()。
data-size 能限制服务端上传大小吗?
不能。data-size 是后台上传组件的前端限制。自定义上传接口需要在服务端按业务自行校验文件大小。
安全目录文件如何访问?
安全目录文件不能直接通过公开 URL 访问。业务需要提供下载接口,在接口中完成权限判断后使用本地存储 get($name, true) 读取内容并输出。
