💾 文件存储

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链接附加策略,包含 compressfull 时影响部分 URL 后缀

后台上传接口始终会校验 storage.allow_exts,并额外禁止 shaspbatcmdexephp 等常见可执行后缀。

统一接口

所有驱动实现 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 是业务最终使用的访问地址或安全文件键;keyfile 的具体值与驱动有关。本地存储 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

实际规则:

  1. 执行 $fun($source),默认 $funmd5
  2. 取哈希前 2 位作为一层目录。
  3. 取哈希第 3 位开始的 30 位作为文件名。
  4. 使用传入后缀拼接最终文件键。
// 以文件内容生成键,适合服务端自定义上传
$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.jpg

data-path 会被前端清理为只包含字母、数字、下划线、短横线和路径分隔层级。若 storage.name_type 为空,上传脚本会按 xmd5 处理。

秒传流程

ThinkAdmin 的标准上传组件内置秒传流程:

  1. 前端计算文件 xmd5xkey
  2. 请求 /admin/api.upload/state
  3. 服务端通过 Storage::instance($uptype)->info($key, $safe, $name) 判断文件是否已存在。
  4. 已存在时返回业务成功码 200,前端直接完成上传。
  5. 不存在时返回业务码 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:仅对 pngjpgjpeg 图片追加服务商图片处理参数,本地存储不追加压缩参数。
  • 包含 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);

参数说明:

  • $forcetrue 时强制重新下载。
  • $expire 为缓存有效秒数,0 表示不过期。
  • 下载失败时会返回原始 URL 组成的兜底数组。

保存 Base64 图片

Storage::saveImage() 只接受 data:image/{gif|png|jpg|jpeg};base64,... 形式的图片:

$info = Storage::saveImage($base64, 'avatar');

当第三个参数 $safemodetrue 时,会使用本地安全目录保存:

$info = Storage::saveImage($base64, 'avatar', true);

如果传入内容不匹配 data:image/...;base64,... 格式,方法不会抛错,也不会写入文件,而是直接返回 ['url' => $base64]。如果匹配到了图片数据但后缀不在 gifpngjpgjpeg 范围内,会抛出内容格式异常。

云存储区域

区域列表由各云存储驱动的静态 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' => '中国大陆 公有云地域 上海', ...]

localalistupyun 当前返回空区域列表。

后缀与 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) 读取内容并输出。

最近更新:
Contributors: 邹景立, Anyon