efun / cloud-uploader
PHP 通用云存储文件,大文件分片,base64上传类库,支持阿里云 OSS、腾讯云 COS、七牛云 Kodo
Requires
- php: >=7.4
Requires (Dev)
- phpunit/phpunit: ^9.0
Suggests
- aliyuncs/oss-sdk-php: 用于阿里云 OSS 存储适配器
- qcloud/cos-sdk-v5: 用于腾讯云 COS 存储适配器
- qiniu/php-sdk: 用于七牛云 Kodo 存储适配器
- workerman/webman-framework: 若在 webman 框架中使用,formUpload() 可通过鸭子类型直接接收 UploadFile 对象
This package is not auto-updated.
Last update: 2026-07-20 03:48:13 UTC
README
PHP 通用云存储文件上传类库,提供统一接口对接阿里云 OSS、腾讯云 COS、七牛云 Kodo,以及本地文件系统。
特性
- 统一接口 — 通过适配器模式封装不同云服务商的 SDK,调用方无需关心底层差异
- 多种上传方式 — 表单上传、本地文件上传、Base64 上传、分片上传、批量上传
- 灵活验证规则 — 可配置单文件大小限制、总大小限制、文件数量、扩展名白名单/黑名单
- 统一返回结构 — 每次上传均返回
UploadResult,包含完整的上传信息 - 易于扩展 — 接入新的云存储只需新增一个适配器类,无需修改核心代码
- 本地适配器 — 内置
LocalStorageAdapter,可将文件直接保存到服务器本地目录,方便开发调试 - 配置驱动 — 提供
Storage::disk()门面,一行代码切换存储引擎,验证规则和适配器配置统一管理
环境要求
- PHP >= 7.4
- Composer
安装
composer require efun/cloud-uploader
如需使用具体的云存储服务,请同时安装对应的 SDK:
# 阿里云 OSS
composer require aliyuncs/oss-sdk-php
# 腾讯云 COS
composer require qcloud/cos-sdk-v5
# 七牛云 Kodo
composer require qiniu/php-sdk
快速开始
<?php
use CloudUploader\FileUploader;
use CloudUploader\ValidationRule;
use CloudUploader\Adapters\AliyunOssAdapter;
// 1. 创建验证规则(使用默认值:单文件 200MB,最多 10 个文件)
$rule = new ValidationRule();
// 2. 创建云存储适配器
$adapter = new AliyunOssAdapter([
'accessKeyId' => 'your-access-key-id',
'accessKeySecret' => 'your-access-key-secret',
'endpoint' => 'oss-cn-hangzhou.aliyuncs.com',
'bucket' => 'your-bucket-name',
'dirname' => 'crmAdmin', // 必填:存储目录前缀
'domain' => 'https://cdn.example.com', // 必填:自定义访问域名
]);
// 3. 创建上传器(规则通过链式调用或按次传参设置,详见下方「规则的三种传递方式」)
$uploader = new FileUploader($adapter);
// 链式设置验证规则
$uploader->setDefaultRule($rule);
// 4. 上传本地文件
$result = $uploader->uploadLocalFile('/path/to/report.pdf');
if ($result->isSuccess()) {
echo "上传成功!\n";
echo "访问 URL: " . $result->getUrl() . "\n";
} else {
echo "上传失败: " . $result->getError() . "\n";
}
Storage 门面(配置驱动,推荐)
除了手动创建适配器和验证规则外,本库还提供 Storage 门面类,通过配置数组统一管理所有存储引擎和验证规则,一行代码即可切换磁盘。
磁盘名称常量
Storage 类内置了磁盘名称常量,避免字符串硬编码,提高代码可维护性:
| 常量 | 值 | 说明 |
|---|---|---|
Storage::LOCAL | 'local' | 本地文件系统 |
Storage::OSS | 'oss' | 阿里云 OSS |
Storage::COS | 'cos' | 腾讯云 COS |
Storage::QINIU | 'qiniu' | 七牛云 Kodo |
配置文件
默认配置文件位于 config/app.php,通过 require 加载后传入 Storage::loadConfig() 即可:
// config/app.php 核心结构
return [
'enable' => true, // 全局开关
'storage' => [
'default' => 'local', // 默认磁盘:local / oss / cos / qiniu
'single_limit' => 1024 * 1024 * 200, // 单文件 200MB
'total_limit' => 1024 * 1024 * 200, // 总大小 200MB
'nums' => 10, // 最大文件数
'include' => [], // 白名单扩展名
'exclude' => [], // 黑名单扩展名
'local' => [
'adapter' => \CloudUploader\Adapters\LocalStorageAdapter::class,
'root' => '/path/to/storage',
'dirname' => function () { return date('Ymd'); },
'domain' => 'http://127.0.0.1:8787',
'uri' => '/runtime',
],
'oss' => [
'adapter' => \CloudUploader\Adapters\AliyunOssAdapter::class,
'accessKeyId' => getenv('OSS_KEY_ID'),
'accessKeySecret' => getenv('OSS_KEY_SECRET'),
'bucket' => getenv('OSS_BUCKET'),
'dirname' => function () { return 'crmSaas'; },
'domain' => getenv('OSS_DOMAIN'),
'endpoint' => 'oss-cn-beijing.aliyuncs.com',
'isCName' => false,
],
'cos' => [
'adapter' => \CloudUploader\Adapters\TencentCosAdapter::class,
'secretId' => getenv('COS_SECRET_ID'),
'secretKey' => getenv('COS_SECRET_KEY'),
'bucket' => getenv('COS_BUCKET'),
'dirname' => 'storage',
'domain' => getenv('COS_DOMAIN'),
'region' => 'ap-shanghai',
],
'qiniu' => [
'adapter' => \CloudUploader\Adapters\QiniuKodoAdapter::class,
'accessKey' => getenv('QINIU_ACCESS_KEY'),
'secretKey' => getenv('QINIU_SECRET_KEY'),
'bucket' => getenv('QINIU_BUCKET'),
'dirname' => 'storage',
'domain' => getenv('QINIU_DOMAIN'),
],
],
];
配置文件可放置在项目中的任意位置,通过 require 加载为数组后传入 Storage::loadConfig()。配合 getenv() 读取环境变量,敏感信息(AccessKey 等)无需硬编码。
基本用法
use CloudUploader\Storage;
// 加载配置数组(先 require 得到数组,再传入 loadConfig)
$config = require __DIR__ . '/config/app.php';
Storage::loadConfig($config);
// 使用默认磁盘(由配置文件 storage.default 决定)
$uploader = Storage::disk();
$result = $uploader->uploadLocalFile('/path/to/file.pdf');
// 切换到阿里云 OSS(使用常量)
$oss = Storage::disk(Storage::OSS);
$result = $oss->multipartUpload('/path/to/large.mp4', 10 * 1024 * 1024);
// 切换配置后再使用七牛云 Kodo
Storage::loadConfig($qiniuConfig);
$qiniu = Storage::disk(Storage::QINIU);
$result = $qiniu->uploadBase64('data:image/png;base64,...');
完整示例
use CloudUploader\Storage;
// 从自定义位置加载配置
$config = require __DIR__ . '/config/upload.php';
Storage::loadConfig($config);
// 使用默认磁盘上传
$uploader = Storage::disk();
$result = $uploader->uploadLocalFile('/path/to/report.pdf');
if ($result->isSuccess()) {
echo "上传成功:" . $result->getUrl();
}
// 运行时动态切换磁盘(使用常量)
$result = Storage::disk(Storage::OSS)->uploadLocalFile('/path/to/backup.zip');
$result = Storage::disk(Storage::COS)->uploadLocalFile('/path/to/archive.tar.gz');
// 按次覆盖验证规则(规则优先级不变)
$result = Storage::disk()->uploadLocalFile(
'/path/to/bigfile.mp4',
new ValidationRule(['single_limit' => 2 * 1024 * 1024 * 1024])
);
// 手动三步分片上传(通过适配器)
$adapter = Storage::disk(Storage::OSS)->getAdapter();
$init = $adapter->initiateMultipartUpload('large.mp4');
// ... uploadPart 循环 ...
// ... completeMultipartUpload ...
自定义磁盘扩展
use CloudUploader\Storage;
// 注册自定义适配器(如 MinIO、S3 等)
Storage::registerDisk('minio', MyMinioAdapter::class);
// 在配置数组中添加对应磁盘配置后即可使用
$minio = Storage::disk('minio');
Storage 与手动创建对比
| 方式 | 适用场景 |
|---|---|
Storage::disk() | 生产项目,多磁盘切换,配置集中管理 |
手动 new FileUploader($adapter, $rule) | 快速原型,临时脚本,需要动态构造适配器参数 |
两种方式底层完全兼容,Storage::disk() 返回的也是 FileUploader 实例,所有上传方法和验证规则传递方式完全一致。
验证规则配置
$rule = new ValidationRule([
'single_limit' => 10 * 1024 * 1024, // 单个文件最大 10MB(默认 200MB)
'total_limit' => 50 * 1024 * 1024, // 总大小最大 50MB(默认 200MB)
'nums' => 5, // 最多允许 5 个文件(默认 10)
'include' => ['xlsx', 'pdf', 'docx'], // 白名单:仅允许这些扩展名
'exclude' => ['exe', 'bat'], // 黑名单:禁止这些扩展名
]);
优先级规则:当 include 不为空时,只检查白名单,exclude 被忽略。include 为空时 exclude 才生效。
也可通过链式调用动态修改:
$rule = (new ValidationRule())
->setSingleLimit(5 * 1024 * 1024)
->setNums(3)
->setInclude(['pdf', 'doc']);
规则的三种传递方式
验证规则可通过以下三种方式传递给上传方法,按优先级从高到低:
| 方式 | 优先级 | 说明 |
|---|---|---|
| 按次传参 | 最高 | 调用上传方法时直接传入 $rule,覆盖所有其他规则 |
| 链式调用 | 中等 | setDefaultRule() 返回 $this,调用后立即生效 |
| 系统默认 | 最低(兜底) | 未设置任何规则时,自动使用内置默认值(200MB/10个文件) |
// 方式 A:按次传参(最高优先级,仅本次上传生效)
$result = $uploader->uploadLocalFile('/path/to/file.pdf',
new ValidationRule(['single_limit' => 2 * 1024 * 1024])
);
// 方式 B:链式调用 setDefaultRule 后立即上传
$result = $uploader->setDefaultRule(new ValidationRule([
'single_limit' => 5 * 1024 * 1024,
'include' => ['pdf', 'docx'],
]))->uploadLocalFile('/path/to/doc.pdf');
// 方式 C:不设置任何规则,使用系统内置默认值(200MB/10个文件)
$uploader = new FileUploader($adapter);
$result = $uploader->uploadLocalFile('/path/to/file.pdf'); // 使用系统默认 200MB 限制
$result = $uploader->uploadLocalFile('/path/to/large.pdf', // 按次覆盖为 100MB
new ValidationRule(['single_limit' => 100 * 1024 * 1024])
);
上传方法
1. 表单文件上传
接收框架的 UploadFile 对象(webman / Laravel),通过鸭子类型自动识别,无需硬依赖:
// 单文件:$request->file('file') 返回 UploadFile 对象,直接传入
$result = $uploader->formUpload($request->file('file'));
// 多文件:<input type="file" name="files[]" multiple />
// webman / Laravel 返回 UploadFile[] 数组,直接传入
$results = $uploader->formUpload($request->file('files'));
// 单次上传时覆盖默认验证规则
$result = $uploader->formUpload($request->file('file'), $oneTimeRule);
2. 本地文件上传
$result = $uploader->uploadLocalFile('/var/www/data.xlsx');
// 按次覆盖验证规则
$result = $uploader->uploadLocalFile('/var/www/data.xlsx',
new ValidationRule(['single_limit' => 5 * 1024 * 1024])
);
3. Base64 文件上传
// 纯 Base64(需同时指定文件名)
$result = $uploader->uploadBase64($base64Str, 'avatar.jpg');
// Data URI 格式(自动提取 MIME 类型和扩展名)
$result = $uploader->uploadBase64('data:image/png;base64,iVBORw0KG...');
// 按次覆盖验证规则
$result = $uploader->uploadBase64($base64Str, 'file.pdf',
new ValidationRule(['single_limit' => 10 * 1024 * 1024])
);
4. 大文件分片上传
便捷方法(一步完成):对于大多数场景,可直接使用 multipartUpload() 便捷方法,内部自动完成初始化→逐片上传→合并的完整流程:
// 便捷方法(服务端自动分片,无需手动控制每个分片)
$result = $uploader->multipartUpload(
'/path/to/large-video.mp4',
10 * 1024 * 1024 // 分片大小 10MB(默认 5MB)
);
// 按次覆盖验证规则
$result = $uploader->multipartUpload(
'/path/to/large-video.mp4',
10 * 1024 * 1024,
new ValidationRule(['single_limit' => 2 * 1024 * 1024 * 1024])
);
手动分片(三步流程):如需精确控制每个分片的上传进度(例如前端进度条),可通过适配器直接调用分步接口:
$adapter = $uploader->getAdapter();
$filePath = '/path/to/large-video.mp4';
$fileName = basename($filePath);
$partSize = 5 * 1024 * 1024; // 每个分片 5MB
$fileSize = filesize($filePath);
// 第一步:初始化分片上传,获取 uploadId 和云端对象路径
$init = $adapter->initiateMultipartUpload($fileName);
$uploadId = $init['uploadId'];
$object = $init['object'];
$parts = [];
$partNumber = 1;
$offset = 0;
try {
// 第二步:循环上传每个分片
while ($offset < $fileSize) {
$length = min($partSize, $fileSize - $offset);
$part = $adapter->uploadPart(
$uploadId, $object, $partNumber,
$filePath, $offset, $length
);
// uploadPart 返回包含 partNumber + etag(OSS/COS)或 ctx(七牛)的数组
$parts[] = $part;
$partNumber++;
$offset += $length;
}
// 第三步:提交所有分片标识,完成云端合并
$result = $adapter->completeMultipartUpload($uploadId, $object, $parts);
echo "上传成功,URL: " . $result->getUrl();
} catch (\Throwable $e) {
// 发生异常时中止上传,清理已上传的分片碎片
$adapter->abortMultipartUpload($uploadId, $object);
throw $e;
}
5. 批量上传(混合来源)
$results = $uploader->batchUpload([
// 字符串:本地文件路径
'/var/www/uploads/doc1.pdf',
// 带自定义名称的路径
['path' => '/var/www/uploads/doc2.docx', 'name' => '报告文档.docx'],
// Base64 内容
['base64' => base64_encode('Hello'), 'name' => 'hello.txt'],
// Data URI 格式的 Base64
['base64' => 'data:image/png;base64,...', 'name' => 'screenshot.png'],
// webman / Laravel UploadFile 对象(鸭子类型自动识别)
$request->file('file'),
]);
// 按次覆盖验证规则
$results = $uploader->batchUpload($files, new ValidationRule(['nums' => 3]));
6. 前端 + 后端协作分片上传
适用于前端进度条、大文件断点续传等场景。前端使用 File.slice() 切片,逐片发送给后端;后端接收后通过适配器逐片上传至云端,最后合并。
接口约定(4 个端点):
| 端点 | 方法 | 说明 |
|---|---|---|
/api/upload/initiate | POST | 初始化,返回 {uploadId, object} |
/api/upload/part | POST | 上传一个分片,返回 {partNumber, etag/ctx} |
/api/upload/complete | POST | 合并所有分片,返回 UploadResult |
/api/upload/abort | POST | 取消上传,清理碎片 |
前端(JavaScript):
async function upload() {
const file = document.getElementById('fileInput').files[0];
const CHUNK_SIZE = 10 * 1024 * 1024; // 每片 10MB
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
// ① 初始化
const initRes = await fetch('/api/upload/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name }),
});
const { uploadId, object } = await initRes.json();
// ② 逐片上传
const parts = [];
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('uploadId', uploadId);
formData.append('object', object);
formData.append('partNumber', i + 1);
formData.append('file', chunk, file.name);
const partRes = await fetch('/api/upload/part', {
method: 'POST',
body: formData,
});
const part = await partRes.json();
parts.push(part);
const percent = Math.round((end / file.size) * 100);
console.log(`上传进度: ${percent}%`);
}
// ③ 合并
const completeRes = await fetch('/api/upload/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId, object, parts }),
});
const result = await completeRes.json();
console.log('上传完成', result);
}
后端(webman 控制器):
use CloudUploader\Storage;
class MultipartController
{
/**
* ① 初始化分片上传
* POST /api/upload/initiate Body: { "filename": "video.mp4" }
*/
public function initiate(Request $request)
{
$filename = $request->input('filename');
$adapter = Storage::disk(Storage::OSS)->getAdapter();
$init = $adapter->initiateMultipartUpload($filename);
return json([
'uploadId' => $init['uploadId'],
'object' => $init['object'],
]);
}
/**
* ② 上传单个分片
* POST /api/upload/part FormData: uploadId, object, partNumber, file
*/
public function uploadPart(Request $request)
{
$uploadId = $request->post('uploadId');
$object = $request->post('object');
$partNumber = (int)$request->post('partNumber');
$file = $request->file('file');
// 前端切片写入临时文件
$tempFile = tempnam(sys_get_temp_dir(), 'chunk_');
$file->move($tempFile);
$chunkSize = filesize($tempFile);
try {
$adapter = Storage::disk(Storage::OSS)->getAdapter();
$part = $adapter->uploadPart(
$uploadId, $object, $partNumber, $tempFile,
0, // offset = 0(临时文件只有这一片)
$chunkSize
);
return json($part);
} finally {
@unlink($tempFile);
}
}
/**
* ③ 合并分片
* POST /api/upload/complete Body: { uploadId, object, parts: [...] }
*/
public function complete(Request $request)
{
$uploadId = $request->input('uploadId');
$object = $request->input('object');
$parts = $request->input('parts');
$adapter = Storage::disk(Storage::OSS)->getAdapter();
$result = $adapter->completeMultipartUpload($uploadId, $object, $parts);
return json($result);
}
/**
* ④ 取消上传(前端主动取消或上传失败时调用)
* POST /api/upload/abort Body: { uploadId, object }
*/
public function abort(Request $request)
{
$uploadId = $request->input('uploadId');
$object = $request->input('object');
$adapter = Storage::disk(Storage::OSS)->getAdapter();
$adapter->abortMultipartUpload($uploadId, $object);
return json(['success' => true]);
}
}
返回结果结构
每次上传返回 UploadResult 对象,实现了 ArrayAccess、JsonSerializable 和 __toString(),支持多种访问方式:
返回字段
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
success | bool | 是否成功 | true |
origin_name | string | 原始文件名 | '常用编程软件和工具.xlsx' |
save_name | string | 保存文件名 | '03414c9b...xlsx' |
save_path | string | 保存路径 | 'crmAdmin/20260717/03414c9b...xlsx' |
url | string | 访问 URL | 'https://...' |
unique_id | string | 唯一标识 | '03414c9bdaf7a...' |
size | int | 文件大小(字节) | 15050 |
mime_type | string | MIME 类型 | 'application/...' |
extension | string | 扩展名 | 'xlsx' |
error | string | 错误信息(成功为空) | '' |
多种访问方式
$result = $uploader->uploadLocalFile('/path/to/file.pdf');
// 方式 1:Getter 方法
$url = $result->getUrl();
$isOk = $result->isSuccess();
// 方式 2:数组方式访问(ArrayAccess)
$url = $result['url'];
$originName = $result['origin_name'];
// 方式 3:动态字段写入/覆盖
$result['db_id'] = 12345; // 写入自定义字段
$result['url'] = 'https://custom...'; // 覆盖内置字段
// 方式 4:转为数组
$data = $result->toArray();
// 方式 5:JSON 序列化(json_encode 自动调用 JsonSerializable)
$json = json_encode($result, JSON_UNESCAPED_UNICODE);
// 方式 6:直接作为 webman / Laravel 控制器返回值(自动调用 __toString())
return $result; // 自动输出 JSON 字符串
失败结果
// 失败上传返回的 UploadResult,success 为 false,error 包含错误信息
$result = UploadResult::failure('文件大小超过限制', ['origin_name' => 'bigfile.mp4']);
echo $result['success']; // false
echo $result['error']; // '文件大小超过限制'
多文件上传返回
// formUpload 多文件、batchUpload 均返回 UploadResult[] 数组
$results = $uploader->formUpload($request->file('files'));
foreach ($results as $result) {
if ($result['success']) {
echo "上传成功: " . $result['url'] . "\n";
} else {
echo "上传失败: " . $result['error'] . "\n";
}
}
// 失败的文件不会跳过,而是在对应位置返回 failure 结果
// 保证返回值数组与输入文件数组一一对应,顺序不变
// 直接返回给 webman 前端:
return json($results); // [{...}, {...}, {...}]
适配器配置
阿里云 OSS
// 阿里云 OSS
$adapter = new AliyunOssAdapter([
'accessKeyId' => 'LTAI...',
'accessKeySecret' => '...',
'endpoint' => 'oss-cn-hangzhou.aliyuncs.com',
'bucket' => 'my-bucket',
'dirname' => 'crmAdmin', // 必填:存储目录前缀,支持字符串或闭包
'domain' => 'https://cdn.example.com', // 必填:自定义访问域名
]);
isCName 参数:使用标准 OSS endpoint(如
oss-cn-hangzhou.aliyuncs.com)时设为false(默认);使用自定义域名/CNAME 时设为true。
腾讯云 COS
$adapter = new TencentCosAdapter([
'secretId' => 'AKID...',
'secretKey' => '...',
'region' => 'ap-guangzhou',
'bucket' => 'my-bucket-1234567890', // 格式:BucketName-APPID
'dirname' => 'storage', // 必填:存储目录前缀,支持字符串或闭包
'domain' => 'https://cdn.example.com', // 必填:自定义访问域名
]);
七牛云 Kodo
$adapter = new QiniuKodoAdapter([
'accessKey' => '...',
'secretKey' => '...',
'bucket' => 'my-bucket',
'dirname' => 'crmAdmin', // 必填:存储目录前缀,支持字符串或闭包
'domain' => 'https://cdn.example.com', // 必填:自定义访问域名
]);
dirname 用法说明
dirname 表示文件在 Bucket 下的存储路径前缀。支持两种方式:
// 方式 1:固定字符串前缀
$adapter = new AliyunOssAdapter([
// ...其他配置...
'dirname' => 'crmAdmin',
]);
// 文件保存路径:crmAdmin/20260717/xxxxx.xlsx
// 方式 2:闭包动态生成目录(如按日期分目录)
$adapter = new AliyunOssAdapter([
// ...其他配置...
'dirname' => function () {
return 'uploads/' . date('Y/m/d');
},
]);
// 文件保存路径:uploads/2024/01/15/20240115/xxxxx.xlsx
// dirname 为必填参数,传入空字符串将导致异常
本地文件系统(LocalStorageAdapter)
将文件直接保存到服务器本地目录,适用于开发调试或内部文件落地场景。
$adapter = new LocalStorageAdapter([
'root_path' => '/var/www/uploads', // 必填:本地存储根目录
'dirname' => 'local_files', // 必填:子目录前缀
'domain' => 'https://cdn.example.com', // 必填:访问域名
]);
限制说明:
- 不支持分片上传步骤方法(
initiateMultipartUpload/uploadPart/completeMultipartUpload),调用会抛出UnsupportedOperationException。 multipartUpload()便捷方法已降级为一次性写入。
域名优先级
文件访问 URL 按以下优先级构建:
- 如果配置了
domain,直接使用{domain}/{save_path} - 否则使用各云服务的默认域名
异常处理
try {
$result = $uploader->uploadLocalFile('/path/to/file.pdf');
} catch (ValidationException $e) {
// 验证失败:文件大小、扩展名、数量不满足规则
echo "验证失败: " . $e->getMessage();
} catch (UploadException $e) {
// 上传失败:SDK 调用失败、网络错误等
echo "上传失败: " . $e->getMessage();
// 可获取部分已收集的数据
$partialData = $e->getPartialData();
} catch (\Throwable $e) {
echo "系统错误: " . $e->getMessage();
}
扩展自定义存储适配器
实现 StorageAdapterInterface 并继承 BaseStorageAdapter 即可接入新的云存储:
use CloudUploader\BaseStorageAdapter;
use CloudUploader\UploadResult;
class MyCustomAdapter extends BaseStorageAdapter
{
public function upload(string $localPath, string $filename): UploadResult
{
// 调用你的云存储 SDK...
return UploadResult::success([...]);
}
public function multipartUpload(string $localPath, string $filename, int $partSize = 5242880): UploadResult
{
// 分片上传...
return UploadResult::success([...]);
}
public function initiateMultipartUpload(string $filename): array
{
// 初始化分片上传...
return [...];
}
public function uploadPart(string $uploadId, string $object, int $partNumber, string $localPath, int $offset, int $length): array
{
// 上传单个分片...
return [...];
}
public function completeMultipartUpload(string $uploadId, string $object, array $parts): UploadResult
{
// 完成分片合并...
return UploadResult::success([...]);
}
public function abortMultipartUpload(string $uploadId, string $object): void
{
// 取消分片上传...
}
public function uploadContent(string $content, string $filename): UploadResult
{
// 内容上传...
return UploadResult::success([...]);
}
}
运行测试
# 安装开发依赖
composer install
# 运行测试
vendor/bin/phpunit
# 带覆盖率报告
vendor/bin/phpunit --coverage-text
目录结构
├── src/
│ ├── UploadResult.php # 统一返回结果 DTO
│ ├── ValidationRule.php # 验证规则配置
│ ├── StorageAdapterInterface.php # 适配器接口
│ ├── BaseStorageAdapter.php # 适配器基类
│ ├── FileUploader.php # 核心上传器
│ ├── Storage.php # Storage 门面(配置驱动入口)
│ ├── Adapters/
│ │ ├── AliyunOssAdapter.php # 阿里云 OSS
│ │ ├── TencentCosAdapter.php # 腾讯云 COS
│ │ ├── QiniuKodoAdapter.php # 七牛云 Kodo
│ │ └── LocalStorageAdapter.php # 本地文件系统
│ └── Exception/
│ ├── UploadException.php # 上传异常
│ ├── ValidationException.php # 验证异常
│ └── UnsupportedOperationException.php # 不支持的操作异常
├── config/
│ └── app.php # 默认配置文件
├── tests/ # 单元测试
├── examples/
│ └── example.php # 完整使用示例
├── composer.json
├── phpunit.xml
└── README.md
License
MIT