efun/cloud-uploader

PHP 通用云存储文件,大文件分片,base64上传类库,支持阿里云 OSS、腾讯云 COS、七牛云 Kodo

Maintainers

Package info

gitee.com/DPF1994/efun-cloud-uploader.git

pkg:composer/efun/cloud-uploader

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

v1.0.0 2026-07-18 10:56 UTC

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/initiatePOST初始化,返回 {uploadId, object}
/api/upload/partPOST上传一个分片,返回 {partNumber, etag/ctx}
/api/upload/completePOST合并所有分片,返回 UploadResult
/api/upload/abortPOST取消上传,清理碎片

前端(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 对象,实现了 ArrayAccessJsonSerializable__toString(),支持多种访问方式:

返回字段

字段类型说明示例
successbool是否成功true
origin_namestring原始文件名'常用编程软件和工具.xlsx'
save_namestring保存文件名'03414c9b...xlsx'
save_pathstring保存路径'crmAdmin/20260717/03414c9b...xlsx'
urlstring访问 URL'https://...'
unique_idstring唯一标识'03414c9bdaf7a...'
sizeint文件大小(字节)15050
mime_typestringMIME 类型'application/...'
extensionstring扩展名'xlsx'
errorstring错误信息(成功为空)''

多种访问方式

$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 按以下优先级构建:

  1. 如果配置了 domain,直接使用 {domain}/{save_path}
  2. 否则使用各云服务的默认域名

异常处理

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