💾 文件存储

ThinkAdmin 提供统一的文件存储引擎,支持多种存储方案,满足不同场景下的数据存储需求。

📚 基础概念

🤔 基本介绍

文件存储是指将用户上传的文件(图片、文档、视频等)保存到服务器或云存储服务的过程。

简单理解:就像把文件放到仓库里,需要的时候再取出来。

实际例子

用户上传图片 → 保存到云存储 → 返回访问链接

    https://example.com/image/abc123.jpg

ThinkAdmin 提供了统一的存储接口,支持多种存储方案,让开发者可以用相同的代码操作不同的存储服务。

🤔 使用优势

问题1:不同存储方案 API 不同

不使用统一接口时,需要为每种存储方案写不同的代码:

// ❌ 不统一:需要为每种存储写不同的代码
if ($storageType === 'qiniu') {
    // 七牛云 API
    $qiniu->upload($file);
} elseif ($storageType === 'alioss') {
    // 阿里云 OSS API
    $oss->putObject($file);
} elseif ($storageType === 'local') {
    // 本地存储
    move_uploaded_file($file, $path);
}

使用统一接口后,代码变得简单:

// ✅ 统一接口:所有存储方案使用相同的代码
Storage::instance()->set($filename, $content);
// 切换存储方案时,只需修改配置,无需修改代码

问题2:切换存储方案困难

使用统一接口后,切换存储方案只需修改配置:

// 配置文件中修改存储类型
// config/storage.php
return [
    'type' => 'qiniu',  // 改为 'alioss' 或 'local' 即可
];

📊 存储方案对比

ThinkAdmin 支持多种存储方案,各有特点:

存储方式不占服务器空间上传大文件支持CDN加速是否推荐
本地服务器存储
自建 Alist 存储✔️可配✔️
七牛云空间存储✔️✔️✔️✔️
又拍云USS存储✔️✔️✔️✔️
阿里云OSS存储✔️✔️✔️✔️
腾讯云COS存储✔️✔️✔️✔️

本地存储

  • 文件保存在服务器本地
  • 简单但占用服务器空间
  • 适合小项目或测试环境

云存储

  • 文件保存在云端
  • 不占服务器空间,支持 CDN 加速
  • 适合生产环境

混合存储

  • 可以根据文件类型选择不同的存储方案
  • 例如:图片用云存储,文档用本地存储

⚡ 文件秒传

文件秒传是指当文件已经上传过时,再次上传相同文件时立即成功,无需重新上传。

工作原理

ThinkAdmin 使用文件 hash(MD5)来判断文件是否已存在:

// 1. 计算文件 hash
$hash = md5_file($filePath);
// 例如:abc123def456...

// 2. 检查文件是否已存在
if (Storage::instance()->has($hash)) {
    // 文件已存在,直接返回 URL(秒传)
    return Storage::instance()->url($hash);
} else {
    // 文件不存在,正常上传
    return Storage::instance()->set($hash, $content);
}

实际效果

第一次上传:上传文件 → 保存到云存储 → 返回 URL(需要时间)
第二次上传:检查 hash → 文件已存在 → 立即返回 URL(秒传)

📖 相关概念

Hash(哈希)

  • 文件的唯一标识
  • 相同内容的文件 hash 相同
  • 例如:abc123def456...

CDN(内容分发网络)

  • 加速文件访问的网络服务
  • 将文件缓存到多个节点,提升访问速度

URL(统一资源定位符)

  • 文件的访问地址
  • 例如:https://example.com/image/abc123.jpg

安全目录

  • 不允许直接 URL 访问的目录
  • 用于存储私密文件
  • 只支持本地存储

🚀 主要功能

  • 多存储支持: 支持本地存储、云存储等多种方案
  • 统一接口: 提供统一的存储接口,简化开发
  • 灵活配置: 支持后台动态配置存储参数
  • 文件管理: 完整的文件管理功能
  • 性能优化: 支持文件秒传和 CDN 加速
  • 安全可靠: 提供安全的文件存储解决方案

📋 存储方案对比

存储方式不占服务器空间上传大文件支持CDN加速是否推荐
本地服务器存储
自建 Alist 存储✔️可配✔️
七牛云空间存储✔️✔️✔️✔️
又拍云USS存储✔️✔️✔️✔️
阿里云OSS存储✔️✔️✔️✔️
腾讯云COS存储✔️✔️✔️✔️

🌟 推荐方案

七牛云存储: 配置简单性价比高,首次注册认证赠送 10GB 的免费存储空间。立即注册

⚙️ 存储特性

文件命名策略

  • Hash 命名: 默认使用文件 hash 命名,同一文件只存储一份
  • 日期命名: 支持日期+随机的方式命名(不支持秒传)
  • 秒传支持: 相同文件再次上传时立即成功
  • URL 访问: 上传后返回可访问的 URL 链接

配置管理

  • 后台配置: 通过系统管理界面配置存储参数
  • 参数说明: 提供详细的参数说明和配置指导
  • 灵活切换: 支持不同存储方案的灵活切换

性能优化

  • 文件秒传: 基于文件 hash 实现秒传功能
  • CDN 加速: 支持 CDN 加速提升访问速度
  • 缓存机制: 内置缓存机制提升性能
  • 压缩优化: 支持文件压缩和优化

安全特性

  • 访问控制: 支持文件访问权限控制
  • 防盗链: 支持防盗链保护
  • 加密存储: 支持文件加密存储
  • 备份恢复: 支持文件备份和恢复
存储方式不占服务器空间上传大文件支持CDN加速是否推荐
本地服务器存储❌️
自建 Alist 存储✔️可配✔️
七牛云空间存储✔️✔️✔️✔️
又拍云USS存储✔️✔️✔️✔️
阿里云OSS存储✔️✔️✔️✔️
腾讯云COS存储✔️✔️✔️✔️

这里推荐使用 七牛云存储,主要是配置简单性价比高,首次注册认证赠送 10GB 的免费存储空间。 立即注册

  • ThinkAdmin 文件存储默认使用文件 hash 命名,同一个文件只会存储一份;
  • 支持文件秒传,当文件已经上传到服务之后,再次上传同一个文件时将立即成功;
  • 新版本的 ThinkAdmin 支持以日期+随机的方式命名,注意此方法不支持秒传功能;
  • 所有文件上传之后,将统一返回 url 可访问的链接地址,直接存储到内容即可访问;
  • 更多文件规则可以在系统后台文件管理处配置参数,不同的存储方式配置不同的参数;

config-language.png

后台配置演示

⚙️ 自动存储

ThinkAdmin 提供了统一的存储接口,默认自动选择配置的存储引擎,无需手动指定。

📝 基本用法

获取存储实例

use think\admin\Storage;

// 获取存储实例(自动选择配置的存储引擎)
$storage = Storage::instance();

文件操作

use think\admin\Storage;

$content = '文件内容(原文件内容,文档或二进制文件)';
$filename = '文件名称(支持路径,相对 upload 目录或云存储根目录)';

// 上传文件
$result = Storage::instance()->set($filename, $content);
// 返回:['url' => 'https://...', 'key' => '...', 'hash' => '...']

// 读取文件
$content = Storage::instance()->get($filename);
// 返回:文件内容(字符串)

// 删除文件
$result = Storage::instance()->del($filename);
// 返回:true 或 false

// 判断文件是否存在
$exists = Storage::instance()->has($filename);
// 返回:true 或 false

// 生成文件访问 URL
$url = Storage::instance()->url($filename);
// 返回:https://example.com/image/abc123.jpg

// 获取文件信息
$info = Storage::instance()->info($filename);
// 返回:['url' => '...', 'key' => '...', 'hash' => '...', 'file' => '...']

// 计算文件路径
$path = Storage::instance()->path($filename);
// 返回:文件路径(本地存储)或 URL(云存储)

// 获取上传入口(用于前端直传)
$uploadUrl = Storage::instance()->upload();
// 返回:上传接口地址

// 获取存储区域列表(云存储)
$regions = Storage::instance()->region();
// 返回:['华东' => 'z0', '华北' => 'z1', ...]

🔒 安全目录存储

参数 safe 说明

safe 参数用于存储文件到安全目录(只支持本地存储),不允许直接使用 URL 访问,主要用于上传一些私密文件。

use think\admin\Storage;

// 存储到安全目录(safe = true)
$result = Storage::instance()->set(
    'private/document.pdf',
    $fileContent,
    true  // safe 参数为 true
);
// 文件保存在安全目录,无法通过 URL 直接访问

// 读取安全目录文件
$content = Storage::instance()->get('private/document.pdf', true);

// 安全目录文件需要特殊处理才能访问
// 例如:通过控制器方法验证权限后返回文件内容

使用场景

  • 私密文档存储
  • 需要权限验证的文件
  • 不允许直接访问的文件

注意事项

  • 安全目录只支持本地存储
  • 云存储不支持安全目录功能

🎯 指定存储

// 本地服务器文件操作
use think\admin\storage\LocalStorage;

$content = '文件内容 ( 原文件内容,文档或二进制文件 )';
$filename = '文件名称 ( 支持路径,相对 upload 目录 或 云存储根目录 ) ';

$result = LocalStorage::instance()->upload(); // 上传入口
$result = LocalStorage::instance()->set($filename, $content, $safe); // 上传文件
$result = LocalStorage::instance()->get($filename, $safe); // 读取文件
$result = LocalStorage::instance()->del($filename, $safe); // 删除文件
$result = LocalStorage::instance()->has($filename, $safe); // 判断是否存在
$result = LocalStorage::instance()->url($filename, $safe); // 生成文件链接
$result = LocalStorage::instance()->info($filename, $safe); // 获取文件参数
// 自建 Alist 存储
// 该方式是自行搭建的存储,灵活私有,可使用内网存储
use think\admin\storage\AlistStorage;

$content = '文件内容 ( 原文件内容,文档或二进制文件 )';
$filename = '文件名称 ( 支持路径,相对 upload 目录 或 云存储根目录 ) ';

$result = AlistStorage::instance()->upload(); // 上传入口
$result = AlistStorage::instance()->region(); // 存储区域
$result = AlistStorage::instance()->set($filename, $content); // 上传文件
$result = AlistStorage::instance()->get($filename); // 读取文件
$result = AlistStorage::instance()->del($filename); // 删除文件
$result = AlistStorage::instance()->has($filename); // 判断是否存在
$result = AlistStorage::instance()->url($filename); // 生成文件链接
$result = AlistStorage::instance()->info($filename); // 获取文件参数
// 阿里云 OSS 存储
use think\admin\storage\AliossStorage;

$content = '文件内容 ( 原文件内容,文档或二进制文件 )';
$filename = '文件名称 ( 支持路径,相对 upload 目录 或 云存储根目录 ) ';

$location = '文件远程链接,如:https://v6.thinkadmin.top/favicon.ico';

$result = AliossStorage::instance()->upload(); // 上传入口
$result = AliossStorage::instance()->region(); // 存储区域
$result = AliossStorage::instance()->set($filename, $content); // 上传文件
$result = AliossStorage::instance()->get($filename); // 读取文件
$result = AliossStorage::instance()->del($filename); // 删除文件
$result = AliossStorage::instance()->has($filename); // 判断是否存在
$result = AliossStorage::instance()->url($filename); // 生成文件链接
$result = AliossStorage::instance()->info($filename); // 获取文件参数
// 腾讯云 COS 存储
use think\admin\storage\TxcosStorage;

$content = '文件内容 ( 原文件内容,文档或二进制文件 )';
$filename = '文件名称 ( 支持路径,相对 upload 目录 或 云存储根目录 ) ';

$result = TxcosStorage::instance()->upload(); // 上传入口
$result = TxcosStorage::instance()->region(); // 存储区域
$result = TxcosStorage::instance()->set($filename, $content); // 上传文件
$result = TxcosStorage::instance()->get($filename); // 读取文件
$result = TxcosStorage::instance()->del($filename); // 删除文件
$result = TxcosStorage::instance()->has($filename); // 判断是否存在
$result = TxcosStorage::instance()->url($filename); // 生成文件链接
$result = TxcosStorage::instance()->info($filename); // 获取文件参数
// 七牛云存储
use think\admin\storage\QiniuStorage;

$content = '文件内容 ( 原文件内容,文档或二进制文件 )';
$filename = '文件名称 ( 支持路径,相对 upload 目录 或 云存储根目录 ) ';

$result = QiniuStorage::instance()->upload(); // 上传入口
$result = QiniuStorage::instance()->region(); // 存储区域
$result = QiniuStorage::instance()->set($filename, $content); // 上传文件
$result = QiniuStorage::instance()->get($filename); // 读取文件
$result = QiniuStorage::instance()->del($filename); // 删除文件
$result = QiniuStorage::instance()->has($filename); // 判断是否存在
$result = QiniuStorage::instance()->url($filename); // 生成文件链接
$result = QiniuStorage::instance()->info($filename); // 获取文件参数

📦 静态方法

Storage 类还提供了一些静态方法,可以直接调用,无需实例化:

use think\admin\Storage;

// 获取文件相对名称(基于 hash 或指定函数)
$filename = Storage::name($url, $ext = '', $pre = '', $fun = 'md5');
// 示例:Storage::name('https://example.com/image.jpg', 'jpg', 'image') 
// 返回:image/ab/cd1234567890abcdef1234567890ab.jpg

// 下载文件到本地
$result = Storage::down($url, $force = false, $expire = 0);
// $force: 是否强制重新下载
// $expire: 文件保留时间(秒),0 表示永久保留

// 获取文件 MIME 类型
$mime = Storage::mime($exts, $mime = []);
// 示例:Storage::mime('jpg,png') 返回 'image/jpeg,image/png'

// 获取所有 MIME 类型映射
$mimes = Storage::mimes();

// 获取支持的存储类型列表
$types = Storage::types();
// 返回:['local' => '本地服务器存储', 'qiniu' => '七牛云对象存储', ...]

// 使用 CURL 读取网络资源
$content = Storage::curlGet($url);

// 保存 base64 图片数据
$result = Storage::saveImage($base64, $prefix = 'image', $safemode = false);
// 示例:Storage::saveImage('data:image/png;base64,iVBORw0KG...', 'avatar')

实际应用案例

<?php
declare(strict_types=1);

namespace app\admin\controller;

use think\admin\Controller;
use think\admin\Storage;

/**
 * 文件管理示例
 * @class File
 * @package app\admin\controller
 */
class File extends Controller
{
    /**
     * 下载远程文件并保存
     * @auth true
     */
    public function download()
    {
        $url = input('url');
        if (empty($url)) {
            $this->error('文件地址不能为空');
        }
        
        // 下载文件到本地(带缓存,24小时内不重复下载)
        $result = Storage::down($url, false, 86400);
        $this->success('文件下载成功', $result);
    }
    
    /**
     * 保存 base64 图片
     * @auth true
     */
    public function saveImage()
    {
        $base64 = input('image');
        if (empty($base64)) {
            $this->error('图片数据不能为空');
        }
        
        // 保存 base64 图片到云存储
        $result = Storage::saveImage($base64, 'avatar');
        $this->success('图片保存成功', $result);
    }
    
    /**
     * 处理富文本编辑器中的 base64 图片
     * @auth true
     */
    public function processContent()
    {
        $content = input('content');
        
        // 抓取内容中的 base64 图片并上传到云端
        $content = preg_replace_callback(
            '/"data:image\/([a-zA-Z]+);base64,(.*)"/', 
            function ($matches) {
                // 保存 base64 图片到云存储
                $result = Storage::saveImage(trim($matches[0], '"'));
                return '"' . $result['url'] . '"';
            }, 
            $content
        );
        
        // 保存处理后的内容
        // ...
        $this->success('内容处理成功');
    }
}

🚀 高级用法

📋 文件操作流程

完整的文件操作流程包括:检查、上传、读取、获取信息、生成 URL、删除。

use think\admin\Storage;

// 获取存储实例
$storage = Storage::instance();

// 1. 检查文件是否存在
if ($storage->has('image/ab/cd123456.jpg')) {
    // 文件存在,可以执行后续操作
    echo '文件已存在';
}

// 2. 上传文件
$result = $storage->set('image/ab/cd123456.jpg', file_get_contents($filePath));
// 返回:['url' => 'https://...', 'key' => 'image/ab/cd123456.jpg', 'hash' => '...']

// 3. 读取文件内容
$content = $storage->get('image/ab/cd123456.jpg');
// 返回:文件内容(字符串)

// 4. 获取文件信息
$info = $storage->info('image/ab/cd123456.jpg');
// 返回:['url' => '...', 'key' => '...', 'hash' => '...', 'file' => '...']

// 5. 生成文件访问 URL
$url = $storage->url('image/ab/cd123456.jpg');
// 返回:https://example.com/image/ab/cd123456.jpg

// 6. 删除文件
$storage->del('image/ab/cd123456.jpg');
// 返回:true 或 false

📝 文件命名策略

ThinkAdmin 支持多种文件命名策略,推荐使用 hash 命名(支持秒传)。

Hash 命名(推荐)

// 使用 hash 命名(推荐,支持秒传)
$filename = Storage::name($filePath, 'jpg', 'image');
// 返回:image/ab/cd1234567890abcdef1234567890ab.jpg

// 工作原理:
// 1. 计算文件 MD5:abc123def456...
// 2. 取前 2 位作为目录:ab
// 3. 取 3-4 位作为子目录:cd
// 4. 完整 hash 作为文件名:abc123def456...
// 5. 最终路径:image/ab/cd/abc123def456....jpg

日期命名

// 使用日期+随机命名(不支持秒传)
$filename = Storage::name($filePath, 'jpg', 'image', 'date');
// 返回:image/2024/01/15/abc123def456.jpg

// 格式:{prefix}/{年}/{月}/{日}/{随机字符串}.{扩展名}

自定义命名

// 自定义命名函数
$filename = Storage::name($filePath, 'jpg', 'image', function($path, $ext) {
    // $path: 文件路径
    // $ext: 文件扩展名
    return 'custom/' . md5($path) . '.' . $ext;
});
// 返回:custom/abc123def456.jpg

📦 批量文件操作

批量操作可以提高效率,适用于需要处理多个文件的场景。

批量上传

// 批量上传文件
$files = [
    ['path' => 'image1.jpg', 'content' => $content1],
    ['path' => 'image2.jpg', 'content' => $content2],
    ['path' => 'image3.jpg', 'content' => $content3],
];

$results = [];
foreach ($files as $file) {
    // 生成文件名
    $filename = Storage::name($file['path'], 'jpg', 'image');
    
    // 上传文件
    $result = Storage::instance()->set($filename, $file['content']);
    $results[] = $result;
}

// $results 包含所有上传结果

批量删除

// 批量删除文件
$keys = [
    'image/ab/cd1.jpg',
    'image/ab/cd2.jpg',
    'image/ab/cd3.jpg',
];

foreach ($keys as $key) {
    Storage::instance()->del($key);
}

🔍 文件类型验证

文件类型验证可以防止上传不安全的文件类型。

获取 MIME 类型

// 获取文件 MIME 类型
$mime = Storage::mime('jpg,png');
// 返回:image/jpeg,image/png

// 获取所有 MIME 类型映射
$mimes = Storage::mimes();
// 返回:['jpg' => 'image/jpeg', 'png' => 'image/png', ...]

验证文件扩展名

// 验证文件扩展名
$allowedExts = ['jpg', 'png', 'gif'];
$fileExt = pathinfo($filename, PATHINFO_EXTENSION);

if (!in_array(strtolower($fileExt), $allowedExts)) {
    $this->error('不支持的文件类型!');
}

完整验证示例

<?php
// 文件上传验证
$file = $this->request->file('file');

// 1. 验证文件是否存在
if (empty($file)) {
    $this->error('请选择要上传的文件');
}

// 2. 验证文件类型
$allowExts = ['jpg', 'png', 'gif'];
$ext = strtolower($file->extension());
if (!in_array($ext, $allowExts)) {
    $this->error('不支持的文件类型!');
}

// 3. 验证文件大小
$maxSize = 2097152;  // 2MB
if ($file->getSize() > $maxSize) {
    $this->error('文件大小超过限制!');
}

// 4. 验证 MIME 类型
$mime = Storage::mime($ext);
$fileMime = $file->getMime();
if (strpos($mime, $fileMime) === false) {
    $this->error('文件类型不匹配!');
}

🌍 云存储区域配置

不同云存储支持不同的区域,选择合适的区域可以提升访问速度。

获取区域列表

// 获取存储区域列表(不同云存储支持的区域)
use think\admin\storage\QiniuStorage;
use think\admin\storage\AliossStorage;
use think\admin\storage\TxcosStorage;

// 七牛云区域
$qiniuRegions = QiniuStorage::region();
// 返回:['华东' => 'z0', '华北' => 'z1', '华南' => 'z2', ...]

// 阿里云 OSS 区域
$aliossRegions = AliossStorage::region();
// 返回:['华东1(杭州)' => 'oss-cn-hangzhou', ...]

// 腾讯云 COS 区域
$txcosRegions = TxcosStorage::region();
// 返回:['华东地区(上海)' => 'ap-shanghai', ...]

选择区域

// 在后台配置中选择区域
// 系统管理 → 文件管理 → 存储配置 → 选择区域

// 区域选择建议:
// 1. 选择距离用户最近的区域
// 2. 选择距离服务器最近的区域
// 3. 考虑成本因素

❓ 常见问题

Q1:如何切换存储方案?

方法1:后台配置(推荐)

系统管理 → 文件管理 → 存储配置 → 选择存储类型

方法2:代码配置

// 修改配置文件
// config/storage.php
return [
    'type' => 'qiniu',  // 改为 'alioss'、'local' 等
];

注意事项

  • 切换存储方案后,已上传的文件不会自动迁移
  • 需要手动迁移文件或重新上传

Q2:文件秒传不生效?

可能原因

  1. 文件命名策略不是 hash
  2. 文件内容不同(即使文件名相同)

解决方法

// 确保使用 hash 命名
$filename = Storage::name($filePath, 'jpg', 'image', 'md5');
// 不要使用 'date' 命名策略

Q3:如何获取文件访问 URL?

方法1:使用 url() 方法

$url = Storage::instance()->url($filename);
// 返回:https://example.com/image/abc123.jpg

方法2:从上传结果获取

$result = Storage::instance()->set($filename, $content);
$url = $result['url'];
// 返回:https://example.com/image/abc123.jpg

Q4:云存储配置失败?

可能原因

  1. 配置参数错误
  2. 网络连接问题
  3. 权限不足

解决方法

// 1. 检查配置参数
$config = sysconf('storage');
// 确保 access_key、secret_key 等参数正确

// 2. 测试连接
try {
    $result = Storage::instance()->has('test.txt');
} catch (\Exception $e) {
    echo '连接失败:' . $e->getMessage();
}

Q5:如何实现文件下载?

方法1:直接返回文件内容

public function download()
{
    $filename = input('filename');
    $content = Storage::instance()->get($filename);
    
    // 设置响应头
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
    
    echo $content;
}

方法2:重定向到文件 URL

public function download()
{
    $filename = input('filename');
    $url = Storage::instance()->url($filename);
    
    // 重定向到文件 URL
    return redirect($url);
}

Q6:如何批量迁移文件?

迁移脚本示例

<?php
// 从本地存储迁移到云存储
$localStorage = LocalStorage::instance();
$cloudStorage = QiniuStorage::instance();

// 获取所有文件列表
$files = $localStorage->list('upload');

foreach ($files as $file) {
    // 读取本地文件
    $content = $localStorage->get($file);
    
    // 上传到云存储
    $cloudStorage->set($file, $content);
    
    // 删除本地文件(可选)
    // $localStorage->del($file);
}

Q7:如何限制文件大小?

方法1:后台配置

系统管理 → 文件管理 → 存储配置 → 最大文件大小

方法2:代码验证

$maxSize = sysconf('storage.max_size', 2097152);  // 默认 2MB
if ($file->getSize() > $maxSize) {
    $this->error('文件大小超过限制!');
}

Q8:如何限制文件类型?

方法1:后台配置

系统管理 → 文件管理 → 存储配置 → 允许的文件类型

方法2:代码验证

$allowExts = str2arr(sysconf('storage.allow_exts', 'jpg,png,gif'));
$ext = strtolower($file->extension());
if (!in_array($ext, $allowExts)) {
    $this->error('不支持的文件类型!');
}

后续更新:根据使用情况,会不定时增加其他存储支持,具体调用参数一致,可根据对应存储驱动调用即可。

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