PHPackages                             yunadmin/yunstorage - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. yunadmin/yunstorage

ActiveLibrary[File &amp; Storage](/categories/file-storage)

yunadmin/yunstorage
===================

无 SDK 依赖的多云对象存储类库，支持阿里云 OSS (V4)、腾讯云 COS (V5)、七牛云 Kodo、百度云 BOS (V2)

1.0.0(yesterday)02↑2900%MITPHPPHP ^8.0

Since Aug 25Pushed yesterdayCompare

[ Source](https://github.com/haoziliao/yunstorage)[ Packagist](https://packagist.org/packages/yunadmin/yunstorage)[ Docs](https://github.com/haoziliao/yunstorage)[ RSS](/packages/yunadmin-yunstorage/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (1)Versions (2)Used By (0)

YunStorage 对象存储扩展
=================

[](#yunstorage-对象存储扩展)

一个轻量级、无第三方 SDK 依赖的多云对象存储扩展，原生 PHP 实现，支持阿里云 OSS、腾讯云 COS、七牛云 Kodo、百度云 BOS 四大厂商。

---

目录结构
----

[](#目录结构)

```
yunstorage/ (yunadmin/yunstorage)
├── composer.json            Composer 包定义
├── README.md                本文档
└── src/
    ├── Storage.php          工厂入口, 通过 Storage::make() 创建 Driver 实例
    ├── DriverInterface.php  驱动契约接口 (upload / fetch / delete / getObjectUrl / getError)
    ├── AbstractDriver.php   抽象基类, 封装配置访问 / HTTP 客户端 (yunadmin/http-client) / MIME 猜测 / 错误解析
    ├── Signer.php           签名工具集 (base64url / hmacSha1 / hmacSha256 / urlEncode / encodePath)
    └── drivers/
        ├── AliyunDriver.php 阿里云 OSS (V4 签名 OSS4-HMAC-SHA256)
        ├── QcloudDriver.php 腾讯云 COS (V5 签名)
        ├── QiniuDriver.php  七牛云 Kodo (上传凭证 + QBox 管理凭证)
        └── BaiduDriver.php  百度云 BOS (BCE V2 签名)

```

**命名空间映射**（PSR-4，根 `Yunadmin\YunStorage\` → `src/`，与 `yunadmin/http-client` 风格一致）：

文件命名空间`src/Storage.php``Yunadmin\YunStorage\Storage``src/AbstractDriver.php``Yunadmin\YunStorage\AbstractDriver``src/DriverInterface.php``Yunadmin\YunStorage\DriverInterface``src/Signer.php``Yunadmin\YunStorage\Signer``src/drivers/AliyunDriver.php``Yunadmin\YunStorage\drivers\AliyunDriver`---

环境要求
----

[](#环境要求)

- PHP &gt;= 8.0
- 必装扩展：`ext-curl`、`ext-json`
- 依赖：`yunadmin/http-client`

---

安装
--

[](#安装)

```
composer require yunadmin/yunstorage
```

---

快速开始
----

[](#快速开始)

```
use Yunadmin\YunStorage\Storage;

// 1. 准备配置 (实际项目从数据库或 config 读取)
$config = [
    'bucket'     => 'my-bucket',
    'access_key' => 'your-access-key',
    'secret_key' => 'your-secret-key',
    'domain'     => 'oss-cn-hangzhou.aliyuncs.com',
];

// 2. 创建 Driver 实例
$storage = Storage::make('aliyun', $config);

// 3. 上传本地文件
if ($storage->upload('/tmp/avatar.jpg', 'images/avatar.jpg')) {
    $url = $storage->getObjectUrl('images/avatar.jpg');
    echo "上传成功, 访问地址: {$url}";
} else {
    echo "上传失败: " . $storage->getError();
}

// 4. 抓取远程 URL 资源到对象存储
$storage->fetch('https://example.com/photo.png', 'images/photo.png');

// 5. 删除对象
$storage->delete('images/avatar.jpg');
```

---

API 文档
------

[](#api-文档)

### `Yunadmin\YunStorage\Storage` 工厂类

[](#yunadminyunstoragestorage-工厂类)

#### `Storage::make(string $name, array $config): DriverInterface`

[](#storagemakestring-name-array-config-driverinterface)

创建驱动实例。

参数类型说明`$name`string驱动名：`aliyun` / `qcloud` / `qiniu` / `baidu``$config`array驱动配置（详见各厂商配置说明）**异常**：传入不支持的驱动名时抛 `InvalidArgumentException`。

#### `Storage::extend(string $name, string $class): void`

[](#storageextendstring-name-string-class-void)

注册自定义驱动，用于扩展第三方厂商。

```
Storage::extend('huawei', \app\extend\ObsDriver::class);
$storage = Storage::make('huawei', $config);
```

#### `Storage::supportedDrivers(): array`

[](#storagesupporteddrivers-array)

返回所有已注册的驱动名列表。

---

### `DriverInterface` 接口方法

[](#driverinterface-接口方法)

所有 Driver 实例都实现以下 5 个方法：

#### `upload(string $localPath, string $key): bool`

[](#uploadstring-localpath-string-key-bool)

上传本地文件到对象存储。

- `$localPath`：本地文件绝对路径
- `$key`：对象存储中的 key（含目录，如 `images/2024/avatar.jpg`）
- 内部使用 cURL 流式上传（`CURLOPT_INFILE` 或 `CURLFile`），**大文件不占内存**
- 成功返回 `true`，失败返回 `false` 并可通过 `getError()` 获取错误信息

#### `fetch(string $url, ?string $key): bool`

[](#fetchstring-url-string-key-bool)

抓取远程 URL 资源或上传本地文件到对象存储。

- `$url`：`http(s)://` 远程地址 或 本地绝对路径
- `$key`：对象存储中的 key（为 `null` 时由云端自动命名，仅部分厂商支持）
- 行为：
    - 当 `$url` 是 `http://` 或 `https://` 开头时，抓取远程资源
    - 当 `$url` 是本地路径时，等价于 `upload()`

#### `delete(string $key): bool`

[](#deletestring-key-bool)

删除对象存储中的指定对象。

#### `getObjectUrl(string $key): string`

[](#getobjecturlstring-key-string)

获取对象的访问 URL。

- 优先使用配置的 `custom_domain` 拼接
- 否则按厂商默认域名拼接
- 返回完整的 URL 字符串

#### `getError(): string`

[](#geterror-string)

获取最近一次操作的错误信息（在 `upload` / `fetch` / `delete` 返回 `false` 后调用）。

---

各厂商配置说明
-------

[](#各厂商配置说明)

### 阿里云 OSS

[](#阿里云-oss)

```
$config = [
    'bucket'        => 'your-bucket',                     // Bucket 名
    'access_key'    => 'your-access-key-id',              // AccessKey ID
    'secret_key'    => 'your-access-key-secret',          // AccessKey Secret
    'domain'        => 'oss-cn-hangzhou.aliyuncs.com',    // Endpoint (不含 bucket 前缀, V4 会自动从中解析 region)
    'region'        => '',                                 // 可选, 显式指定 (如 cn-hangzhou), 默认从 endpoint 解析
    'custom_domain' => '',                                 // 可选, 如 https://cdn.example.com
];
```

**区域 Endpoint 参考**：

区域EndpointRegion (V4 签名用)华东1（杭州）`oss-cn-hangzhou.aliyuncs.com``cn-hangzhou`华东2（上海）`oss-cn-shanghai.aliyuncs.com``cn-shanghai`华北1（青岛）`oss-cn-qingdao.aliyuncs.com``cn-qingdao`华北2（北京）`oss-cn-beijing.aliyuncs.com``cn-beijing`华南1（深圳）`oss-cn-shenzhen.aliyuncs.com``cn-shenzhen`香港`oss-cn-hongkong.aliyuncs.com``cn-hongkong`**签名方式**：**V4 签名**（`OSS4-HMAC-SHA256`），2025 年 9 月起对新 Bucket 强制要求，本扩展已默认采用 V4。

**V4 关键点**：

- 派生密钥链：`SigningKey = HMAC-SHA256("aliyun_v4_request", HMAC-SHA256("oss", HMAC-SHA256(region, HMAC-SHA256("aliyun_v4"+date, SK))))`
- 必签 header：`host` / `x-oss-date` / `x-oss-content-sha256`
- 流式上传用 `UNSIGNED-PAYLOAD`（避免双重读取文件），字符串 body 用实际 `sha256` hash
- `region` 从 `domain` 自动解析（如 `oss-cn-hangzhou.aliyuncs.com` → `cn-hangzhou`），也可显式配置

---

### 腾讯云 COS

[](#腾讯云-cos)

```
$config = [
    'bucket'        => 'your-bucket-1250000000',          // Bucket 名 (含 APPID 后缀)
    'region'        => 'ap-guangzhou',                     // 区域代号
    'access_key'    => 'your-secret-id',                   // SecretId
    'secret_key'    => 'your-secret-key',                  // SecretKey
    'domain'        => '',                                   // 可选, 留空则自动拼接
    'custom_domain' => '',                                   // 可选, 如 https://cdn.example.com
];
```

**区域 Region 参考**：

区域Region广州`ap-guangzhou`上海`ap-shanghai`北京`ap-beijing`成都`ap-chengdu`香港`ap-hongkong`**域名拼接规则**（`domain` 留空时）：`{bucket}.cos.{region}.myqcloud.com`

**签名方式**：V5 签名，`q-sign-algorithm=sha1`，SignKey 和 Signature 均用 hex 字符串

---

### 七牛云 Kodo

[](#七牛云-kodo)

```
$config = [
    'bucket'        => 'your-bucket',                       // Bucket 名
    'access_key'    => 'your-access-key',                  // AK
    'secret_key'    => 'your-secret-key',                  // SK
    'region'        => 'z0',                                // 区域: z0/z1/z2/na0/as0
    'custom_domain' => 'https://cdn.example.com',          // 访问域名 (必须配置)
];
```

**区域 Region 参考**：

区域Region华东`z0`华北`z1`华南`z2`北美`na0`东南亚`as0`**接口域名**（自动按 region 选择）：

接口域名（z0 示例）上传`upload.qiniup.com`删除`rs.qiniu.com`抓取`iovip.qbox.me`**签名方式**：

- 上传凭证：`token = AK:base64url(hmac_sha1(SK, encodedPutPolicy)):encodedPutPolicy`
- 管理凭证：`QBox AK:base64url(hmac_sha1(SK, "path?query\nbody"))`

---

### 百度云 BOS

[](#百度云-bos)

```
$config = [
    'bucket'        => 'your-bucket',                       // Bucket 名
    'region'        => 'gz',                                // 区域: gz/bj/su
    'access_key'    => 'your-access-key',                  // AK
    'secret_key'    => 'your-secret-key',                  // SK
    'domain'        => '',                                   // 可选, 留空则自动拼接
    'custom_domain' => '',                                   // 可选, 如 https://cdn.example.com
];
```

**区域 Region 参考**：

区域Region广州`gz`北京`bj`苏州`su`**域名拼接规则**（`domain` 留空时）：`{bucket}.{region}.bcebos.com`

**签名方式**：**BCE V2 签名**（2019-04 起百度推荐的最新协议，性能更高、密钥更安全）。

V2 与 V1 主要差异：

- 去掉 `timestamp`（完整时间）和 `expiration`（有效期）
- 增加 `date`（短日期 `yyyymmdd`）、`region`、`service`，融入派生密钥链

V2 算法：

```
authStringPrefix = "bce-auth-v2/{AK}/{date}/{region}/{service}"
SigningKey       = HMAC-SHA256-HEX(authStringPrefix, SK)         // 返回 hex 字符串
CanonicalRequest = METHOD\n + CanonicalURI\n + CanonicalQuery\n + CanonicalHeaders
Signature        = HMAC-SHA256-HEX(CanonicalRequest, SigningKey) // 返回 hex 字符串
Authorization    = "{authStringPrefix}/{signedHeaders}/{signature}"

```

**关键点**：

- signingKey 用 **hex 字符串**（非二进制）作为下一步 HMAC 的 key（BCE 规范）
- 必签 header：`host` + `x-bce-date`（避免 `Content-Length`/`Content-Type` 不匹配）
- `region` 从配置读取，默认 `bj`

---

接入到 ThinkPHP 项目
---------------

[](#接入到-thinkphp-项目)

### 方式一：直接使用（推荐）

[](#方式一直接使用推荐)

在业务逻辑中直接调用：

```
use Yunadmin\YunStorage\Storage;
use think\facade\Db;

// 从数据库读取存储配置
$channel = Db::name('storage')->where('status', 1)->find();
$config = json_decode($channel['params'], true) + [
    'bucket'     => $channel['bucket'],
    'access_key' => $channel['access_key'],
    'secret_key' => $channel['secret_key'],
    'domain'     => $channel['domain'],
    'region'     => $channel['region'] ?? '',
];

$storage = Storage::make($channel['channel'], $config);
$storage->upload($file->getPathname(), 'uploads/' . $file->hashName());
```

### 方式二：封装到引擎类

[](#方式二封装到引擎类)

适配原有 ThinkPHP `app/common/service/storage/engine/` 目录的引擎类风格：

```
namespace app\common\service\storage\engine;

use Yunadmin\YunStorage\Storage;

class Aliyun extends Server
{
    private $storage;

    public function __construct($config)
    {
        parent::__construct($config);
        $this->storage = Storage::make('aliyun', $config);
    }

    public function upload($save_dir = '')
    {
        $key = $save_dir . '/' . $this->getFileName();
        if ($this->storage->upload($this->getRealPath(), $key)) {
            $this->setFileUrl($this->storage->getObjectUrl($key));
            return true;
        }
        $this->error = $this->storage->getError();
        return false;
    }

    public function delete($fileName)
    {
        return $this->storage->delete($fileName);
    }

    public function fetch($url, $key = null)
    {
        return $this->storage->fetch($url, $key);
    }
}
```

---

自定义 Driver 扩展
-------------

[](#自定义-driver-扩展)

实现一个新的对象存储厂商只需 3 步：

### 步骤 1：实现 `DriverInterface`

[](#步骤-1实现-driverinterface)

```
namespace app\extend;

use Yunadmin\YunStorage\AbstractDriver;
use Yunadmin\YunStorage\Signer;

class HuaweiObsDriver extends AbstractDriver
{
    public function upload(string $localPath, string $key): bool
    {
        // 1. 校验参数
        if ($key === '' || !is_file($localPath)) {
            $this->setError($key === '' ? 'key 不能为空' : "文件不存在: {$localPath}");
            return false;
        }

        // 2. 构造请求 URL
        $host   = $this->getConfig('bucket') . '.' . $this->getConfig('domain');
        $url    = 'https://' . $host . '/' . Signer::encodePath($key);

        // 3. 签名
        $headers = [
            'Content-Type'   => $this->guessMime($localPath),
            'Content-Length' => filesize($localPath),
        ];
        $this->signRequest('PUT', $key, $host, $headers);

        // 4. 流式上传
        list($resp, $code) = $this->httpUpload($url, $localPath, [], '', $headers);
        return $code >= 200 && $code < 300
            ?: ($this->setError($this->parseError($resp) ?: "HTTP {$code}") ? false : false);
    }

    public function fetch(string $url, ?string $key): bool { /* ... */ }
    public function delete(string $key): bool { /* ... */ }

    private function signRequest(string $method, string $key, string $host, array &$headers): void
    {
        // 实现厂商的签名算法
        // ...
        $headers['Authorization'] = 'OBS ' . $this->getConfig('access_key') . ':' . $signature;
    }
}
```

### 步骤 2：注册驱动

[](#步骤-2注册驱动)

```
use Yunadmin\YunStorage\Storage;

Storage::extend('huawei', \app\extend\HuaweiObsDriver::class);
```

### 步骤 3：使用

[](#步骤-3使用)

```
$storage = Storage::make('huawei', $config);
$storage->upload('/tmp/file.jpg', 'images/file.jpg');
```

---

设计说明
----

[](#设计说明)

### 为什么不使用官方 SDK？

[](#为什么不使用官方-sdk)

对比项官方 SDKYunStorage包体积阿里 SDK ~3MB + 腾讯 SDK ~2MB + 七牛 SDK ~1MB + 百度 SDK ~2MB = **~8MB****~30KB**依赖各厂商依赖 Guzzle/PSR 等库，版本冲突风险**零依赖**，仅 PHP 原生功能大而全（分片上传/生命周期/桶管理等）**精简核心**（上传/抓取/删除/URL）学习成本各家 API 风格不一致**统一接口**，切换厂商零成本维护升级 SDK 可能破坏兼容签名算法稳定，几乎无需升级### 大文件流式上传

[](#大文件流式上传)

所有 Driver 上传时通过 `AbstractDriver::httpUpload()` 委托 `yunadmin/http-client`，底层走 cURL 的 `CURLOPT_INFILE`（raw PUT）或 `CURLFile`（multipart），按 chunk 读取文件，**内存占用恒为 O(1)**，上传 1GB 文件与 1KB 文件内存占用相同。

### 错误处理规范

[](#错误处理规范)

所有方法失败时返回 `false`，并可通过 `getError()` 获取人类可读的错误信息：

```
if (!$storage->upload($file, $key)) {
    throw new \RuntimeException('上传失败: ' . $storage->getError());
}
```

错误信息来源优先级：

1. 云厂商返回的错误描述（自动解析 XML 或 JSON）
2. 本地校验错误（如 key 为空、文件不存在）
3. HTTP 状态码兜底（如 `OSS PUT 失败, HTTP 403`）

### 签名算法正确性

[](#签名算法正确性)

各厂商签名均按官方文档实现，关键点：

厂商算法关键细节阿里 OSS**V4: HMAC-SHA256 + Hex**派生密钥链 4 步；必签 `host`/`x-oss-date`/`x-oss-content-sha256`；流式上传用 `UNSIGNED-PAYLOAD`；region 从 endpoint 自动解析腾讯 COSHMAC-SHA1 + HexSignKey 和 Signature 均用 hex 字符串（非 base64）百度 BOS**V2: HMAC-SHA256 + Hex**派生密钥链含 `date`/`region`/`service`；signingKey 用 hex 字符串（非二进制）作为下一步 HMAC 的 key；只签 `host` + `x-bce-date`七牛 KodoHMAC-SHA1 + Base64URL上传凭证 `token` 三段式，管理凭证 `QBox` 前缀---

常见问题
----

[](#常见问题)

### Q1：上传时报 403 SignatureDoesNotMatch

[](#q1上传时报-403-signaturedoesnotmatch)

**原因**：系统时间与云服务器时间偏差过大（&gt;15 分钟）。

**解决**：检查服务器时间是否已通过 NTP 同步：

```
# Linux
ntpdate ntp.aliyun.com

# Windows (PowerShell)
w32tm /resync
```

### Q2：腾讯 COS 报 "q-signature 不匹配"

[](#q2腾讯-cos-报-q-signature-不匹配)

**检查**：`region` 配置是否与 bucket 实际区域一致，bucket 名是否包含 APPID 后缀（如 `mybucket-1250000000`）。

### Q3：七牛上传成功但 getObjectUrl 返回的 URL 无法访问

[](#q3七牛上传成功但-getobjecturl-返回的-url-无法访问)

**原因**：七牛要求必须配置 `custom_domain` 或 `domain`（绑定到 bucket 的 CDN 或测试域名）。

**解决**：在七牛控制台绑定加速域名后填入 `custom_domain`。

### Q4：百度 BOS 删除报 403

[](#q4百度-bos-删除报-403)

**原因**：BCE 签名只对 `host` 和 `x-bce-date` 两个 header 签名，若 HTTP 客户端自动添加了 `Content-Length` 不匹配的 header 会被拒绝。

**解决**：本扩展已规避此问题，如仍报错请检查 cURL 是否被全局拦截器修改了 header。

### Q5：如何切换存储厂商而不改业务代码？

[](#q5如何切换存储厂商而不改业务代码)

业务层只依赖 `DriverInterface`，切换厂商只需改 `Storage::make()` 第一个参数：

```
// 切换前
$storage = Storage::make('aliyun', $aliyunConfig);

// 切换后 (业务代码完全不变)
$storage = Storage::make('qcloud', $qcloudConfig);

$storage->upload($file, $key);  // 同样的调用
```

### Q6：如何获取上传后的文件大小或 hash？

[](#q6如何获取上传后的文件大小或-hash)

本扩展聚焦核心功能，未返回上传响应详情。如需获取，可在 Driver 中扩展 `upload()` 方法的返回值，或通过 `getObjectUrl()` 拼接的 URL 发 HEAD 请求查询。

---

版本兼容
----

[](#版本兼容)

- PHP 7.2 / 7.3 / 7.4 / 8.0 / 8.1 / 8.2 / 8.3 / 8.4 全部支持
- ThinkPHP 5.1 / 6.0 / 6.1 / 8.0 均可直接放入 `extend/` 目录使用
- 不依赖任何 Composer 包，可与现有项目共存

---

License
-------

[](#license)

MIT

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/12525194?v=4)[尚云科技](/maintainers/yunadmin)[@yunadmin](https://github.com/yunadmin)

---

Top Contributors

[![haoziliao](https://avatars.githubusercontent.com/u/135537411?v=4)](https://github.com/haoziliao "haoziliao (1 commits)")

---

Tags

storagealiyunqiniuosstencentcosobject-storagebosbaidu

### Embed Badge

![Health badge](/badges/yunadmin-yunstorage/health.svg)

```
[![Health](https://phpackages.com/badges/yunadmin-yunstorage/health.svg)](https://phpackages.com/packages/yunadmin-yunstorage)
```

###  Alternatives

[jacobcyl/ali-oss-storage

aliyun oss filesystem storage for laravel 5+

522575.7k7](/packages/jacobcyl-ali-oss-storage)[alphasnow/aliyun-oss-laravel

alibaba cloud object storage service for laravel

183284.2k4](/packages/alphasnow-aliyun-oss-laravel)[summergeorge/ali-oss-storage

aliyun oss filesystem storage for laravel 5+

3915.6k](/packages/summergeorge-ali-oss-storage)[luoyy/ali-oss-storage

aliyun oss filesystem storage for laravel 10+

1431.3k1](/packages/luoyy-ali-oss-storage)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
