oss 直传

分片上传
This commit is contained in:
2026-05-08 10:49:16 +08:00
parent 4dc9ec1878
commit 7d7a4aa4d2
26 changed files with 1644 additions and 72 deletions
@@ -4,6 +4,7 @@ namespace app\common\service\storage\engine;
use Exception;
use Qcloud\Cos\Client;
use QCloud\COSSTS\Sts;
/**
* 腾讯云存储引擎 (COS)
@@ -113,4 +114,125 @@ class Qcloud extends Server
return $this->fileName;
}
/**
* @notes 获取 STS 临时凭证(用于浏览器直传)
* @param string $keyPrefix 资源前缀,如 uploads/video/20260508/
* @param int $maxSizeBytes 单文件大小上限(字节)
* @param int $durationSeconds 凭证有效期(秒)
* @return array {credentials, expiredTime, requestId}
* @throws Exception
*/
public function getStsCredentials(string $keyPrefix, int $maxSizeBytes, int $durationSeconds = 1800): array
{
$bucket = $this->config['bucket'];
// bucket 形如 likeadmin-1300000000appId 即末段
$appId = '';
if (strpos($bucket, '-') !== false) {
$parts = explode('-', $bucket);
$appId = end($parts);
}
if (!$appId || !ctype_digit($appId)) {
throw new Exception('腾讯云 COS bucket 名称不合法,无法解析 appId(期望形如 name-1300000000');
}
$shortBucket = substr($bucket, 0, strrpos($bucket, '-'));
$region = $this->config['region'];
$prefix = ltrim($keyPrefix, '/');
if ($prefix === '' || substr($prefix, -1) !== '/') {
$prefix = $prefix . '/';
}
$duration = max(900, min($durationSeconds, 7200));
// 自行构造 policy:对象级写动作收紧 + bucket 级 ListMultipartUploadscos-js-sdk-v5 续传探测必需)
$objectArn = sprintf('qcs::cos:%s:uid/%s:%s/%s*', $region, $appId, $bucket, $prefix);
$bucketArn = sprintf('qcs::cos:%s:uid/%s:%s/*', $region, $appId, $bucket);
$policy = [
'version' => '2.0',
'statement' => [
[
'effect' => 'allow',
'action' => [
'name/cos:PostObject',
'name/cos:PutObject',
'name/cos:InitiateMultipartUpload',
'name/cos:UploadPart',
'name/cos:CompleteMultipartUpload',
'name/cos:AbortMultipartUpload',
'name/cos:ListParts',
],
'resource' => [$objectArn],
'condition' => [
'numeric_less_than_equal' => [
'cos:content-length' => $maxSizeBytes,
],
],
],
[
// ListMultipartUploads 是 bucket 级操作,无法用对象级 ARN 命中
'effect' => 'allow',
'action' => ['name/cos:ListMultipartUploads'],
'resource' => [$bucketArn],
],
],
];
$config = [
'url' => 'https://sts.tencentcloudapi.com/',
'domain' => 'sts.tencentcloudapi.com',
'proxy' => '',
'secretId' => $this->config['access_key'],
'secretKey' => $this->config['secret_key'],
'bucket' => $bucket,
'region' => $region,
'durationSeconds' => $duration,
'policy' => $policy,
];
$sts = new Sts();
$result = $sts->getTempKeys($config);
if (!is_array($result) || empty($result['credentials'])) {
throw new Exception('获取 STS 临时凭证失败');
}
return [
'credentials' => [
'tmpSecretId' => $result['credentials']['tmpSecretId'] ?? '',
'tmpSecretKey' => $result['credentials']['tmpSecretKey'] ?? '',
'sessionToken' => $result['credentials']['sessionToken'] ?? '',
],
'expiredTime' => (int)($result['expiredTime'] ?? (time() + $duration)),
'startTime' => (int)($result['startTime'] ?? time()),
'bucket' => $bucket,
'shortBucket' => $shortBucket,
'region' => $region,
'appId' => $appId,
'host' => sprintf('https://%s.cos.%s.myqcloud.com', $bucket, $region),
];
}
/**
* @notes HEAD 校验对象是否真实存在并返回 size / contentType
* @param string $key
* @return array|false
*/
public function headObject(string $key)
{
try {
$result = $this->cosClient->headObject([
'Bucket' => $this->config['bucket'],
'Key' => $key,
]);
return [
'size' => (int)($result['ContentLength'] ?? 0),
'content_type' => (string)($result['ContentType'] ?? ''),
'etag' => trim((string)($result['ETag'] ?? ''), '"'),
];
} catch (Exception $e) {
$this->error = $e->getMessage();
return false;
}
}
}