Files
lgp-admin-plus-api/app/Service/common/upload/QcloudStorageService.php
LQ bcf54c2727 初始化
缺陷:主题配色需要优化整体的同风格
2026-08-14 23:21:21 +08:00

170 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Service\common\upload;
use App\BaseApp\BaseNotAuthService;
use App\Service\common\oss\OssStorageInterface;
use App\Service\common\UtilsService;
use Illuminate\Support\Facades\Http;
/**
* 腾讯云 COS简易 PUTAuthorization 签名 v5
*/
class QcloudStorageService extends BaseNotAuthService implements OssStorageInterface
{
use ObjectKeyNormalizeTrait;
use ObjectListXmlTrait;
protected array $config = [];
public function withConfig(array $config): static
{
$this->config = $config;
return $this;
}
public function uploadImage($filePath, string $key): bool|array
{
return $this->uploadFile($filePath, $key);
}
public function uploadVideo($filePath, string $key): bool|array
{
return $this->uploadFile($filePath, $key);
}
/**
* 列举 bucket 对象GET Bucket
*
* 与上传不同,列举带查询参数,而 COS 签名 v5 把查询串算进 HttpString
* 且 q-url-param-list 必须和实际发出去的参数一一对应;因此这里自己拼查询串,
* 不能交给 Http::get($url, $query) 去编码,否则签名和请求会对不上。
*/
public function listObjects(string $prefix = '', string $marker = '', int $limit = 100): array
{
$this->assertConfig();
$params = ['max-keys' => (string) $this->boundedLimit($limit)];
$listPrefix = $this->scopedListPrefix($prefix);
if ($listPrefix !== '') {
$params['prefix'] = $listPrefix;
}
if ($marker !== '') {
$params['marker'] = $marker;
}
ksort($params);
$host = $this->host();
$query = $this->buildQuery($params);
$response = Http::withHeaders([
'Host' => $host,
'Authorization' => $this->authorization('GET', '/', $params),
])->get('https://' . $host . '/?' . $query);
if (!$response->successful()) {
UtilsService::getInstance()->errorThrow('腾讯云列举对象失败:' . $response->body());
}
return $this->parseObjectListXml($response->body(), 'https://' . $host);
}
public function deleteObject(string $key): bool
{
$this->assertConfig();
$key = $this->normalizeObjectKey($key);
if ($key === '') {
return false;
}
$host = $this->host();
$urlPath = '/' . $key;
$response = Http::withHeaders([
'Host' => $host,
'Authorization' => $this->authorization('DELETE', $urlPath, []),
])->delete('https://' . $host . $urlPath);
return $response->successful();
}
/**
* COS 对象上传Sign Algorithm=sha1
*/
private function uploadFile($filePath, string $key): bool|array
{
$this->assertConfig();
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
$key = $prefix . '/' . ltrim($key, '/');
}
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
? $filePath->getRealPath()
: (string) $filePath;
$content = file_get_contents($path);
$host = $this->host();
$urlPath = '/' . ltrim($key, '/');
$url = 'https://' . $host . $urlPath;
$response = Http::withHeaders([
'Host' => $host,
'Authorization' => $this->authorization('PUT', $urlPath, []),
'Content-Type' => 'application/octet-stream',
])->withBody($content, 'application/octet-stream')->put($url);
if (!$response->successful()) {
UtilsService::getInstance()->errorThrow('腾讯云上传失败:' . $response->body());
}
$publicUrl = $domain !== '' ? ($domain . $urlPath) : $url;
return ['key' => $key, 'url' => $publicUrl];
}
/**
* 签名 v5上传、列举、删除共用差别只在 method / 路径 / 查询参数
*
* @param array<string, string> $params 参与签名的查询参数(键需已排序)
*/
private function authorization(string $method, string $urlPath, array $params): string
{
$secretId = (string) $this->config['access_key'];
$secretKey = (string) $this->config['secret_key'];
$host = $this->host();
$now = time();
$keyTime = $now . ';' . ($now + 600);
$signKey = hash_hmac('sha1', $keyTime, $secretKey);
$paramList = implode(';', array_map('strtolower', array_keys($params)));
$httpString = strtolower($method) . "\n" . $urlPath . "\n" . $this->buildQuery($params)
. "\nhost=" . strtolower($host) . "\n";
$stringToSign = "sha1\n{$keyTime}\n" . sha1($httpString) . "\n";
$signature = hash_hmac('sha1', $stringToSign, $signKey);
return 'q-sign-algorithm=sha1'
. '&q-ak=' . $secretId
. '&q-sign-time=' . $keyTime
. '&q-key-time=' . $keyTime
. '&q-header-list=host'
. '&q-url-param-list=' . $paramList
. '&q-signature=' . $signature;
}
/**
* 用 rawurlencode 拼查询串COS 要求 RFC3986 编码http_build_query 会把空格编成 +
*
* @param array<string, string> $params
*/
private function buildQuery(array $params): string
{
$pairs = [];
foreach ($params as $name => $value) {
$pairs[] = strtolower(rawurlencode((string) $name)) . '=' . rawurlencode((string) $value);
}
return implode('&', $pairs);
}
private function host(): string
{
return $this->config['bucket'] . '.cos.' . $this->config['region'] . '.myqcloud.com';
}
private function assertConfig(): void
{
if (trim((string) ($this->config['access_key'] ?? '')) === ''
|| trim((string) ($this->config['secret_key'] ?? '')) === ''
|| trim((string) ($this->config['bucket'] ?? '')) === ''
|| trim((string) ($this->config['region'] ?? '')) === ''
) {
UtilsService::getInstance()->errorThrow('腾讯云 COS 配置不完整(需要 SecretId/SecretKey/Bucket/Region');
}
}
}