更新
This commit is contained in:
@@ -20,6 +20,7 @@ class DirectUploadService
|
||||
/** 视频允许的扩展名(沿用 config/project.file_video) */
|
||||
public const TYPE_VIDEO = 'video';
|
||||
public const TYPE_VOICE = 'voice';
|
||||
public const TYPE_DESKTOP_PACKAGE = 'desktop_package';
|
||||
|
||||
/** 默认凭证有效期 30 分钟 */
|
||||
public const DEFAULT_DURATION = 1800;
|
||||
@@ -28,6 +29,7 @@ class DirectUploadService
|
||||
private const MAX_SIZE = [
|
||||
self::TYPE_VIDEO => 2 * 1024 * 1024 * 1024, // 2GB
|
||||
self::TYPE_VOICE => 500 * 1024 * 1024, // 500MB
|
||||
self::TYPE_DESKTOP_PACKAGE => 2 * 1024 * 1024 * 1024, // 2GB
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -36,7 +38,7 @@ class DirectUploadService
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function issueCredentials(string $type): array
|
||||
public static function issueCredentials(string $type, int $adminId = 0, string $name = ''): array
|
||||
{
|
||||
if (!isset(self::MAX_SIZE[$type])) {
|
||||
throw new Exception('不支持的上传类型: ' . $type);
|
||||
@@ -54,9 +56,27 @@ class DirectUploadService
|
||||
throw new Exception('腾讯云 COS 配置不完整');
|
||||
}
|
||||
|
||||
$keyPrefix = self::buildKeyPrefix($type);
|
||||
$keyPrefix = self::buildKeyPrefix($type, $adminId);
|
||||
$objectKey = '';
|
||||
// 兼容前后端错峰发布:旧 uploader 只传 type,不传 name。
|
||||
// 新 uploader 仍使用更严格的单对象授权;旧版则限制在当前管理员当天目录,
|
||||
// 并在 confirm 阶段校验文件名、扩展名与实际对象。
|
||||
if ($type === self::TYPE_DESKTOP_PACKAGE && trim($name) !== '') {
|
||||
$extension = strtolower((string)pathinfo($name, PATHINFO_EXTENSION));
|
||||
$objectKey = $keyPrefix
|
||||
. (int)round(microtime(true) * 1000)
|
||||
. '-'
|
||||
. bin2hex(random_bytes(8))
|
||||
. ($extension !== '' ? '.' . $extension : '');
|
||||
self::validateFileExtension($type, $objectKey, $name);
|
||||
}
|
||||
$engine = new QcloudEngine($storageConfig);
|
||||
$sts = $engine->getStsCredentials($keyPrefix, self::MAX_SIZE[$type], self::DEFAULT_DURATION);
|
||||
$sts = $engine->getStsCredentials(
|
||||
$objectKey !== '' ? $objectKey : $keyPrefix,
|
||||
self::MAX_SIZE[$type],
|
||||
self::DEFAULT_DURATION,
|
||||
$objectKey !== ''
|
||||
);
|
||||
|
||||
return [
|
||||
'provider' => 'qcloud',
|
||||
@@ -66,6 +86,7 @@ class DirectUploadService
|
||||
'host' => $sts['host'],
|
||||
'cdn_domain' => rtrim((string)($storageConfig['domain'] ?? ''), '/'),
|
||||
'key_prefix' => $keyPrefix,
|
||||
'object_key' => $objectKey,
|
||||
'max_size' => self::MAX_SIZE[$type],
|
||||
'duration' => self::DEFAULT_DURATION,
|
||||
'expired_time' => $sts['expiredTime'],
|
||||
@@ -93,8 +114,7 @@ class DirectUploadService
|
||||
}
|
||||
|
||||
$key = ltrim((string)($params['key'] ?? ''), '/');
|
||||
$allowedPrefix = self::buildKeyPrefix($type);
|
||||
if ($key === '' || strpos($key, $allowedPrefix) !== 0) {
|
||||
if (!self::isAllowedObjectKey($type, $key, (int)($params['admin_id'] ?? 0))) {
|
||||
throw new Exception('对象 Key 非法');
|
||||
}
|
||||
|
||||
@@ -112,6 +132,7 @@ class DirectUploadService
|
||||
if ($name === '') {
|
||||
$name = basename($key);
|
||||
}
|
||||
self::validateFileExtension($type, $key, $name);
|
||||
if (strlen($name) > 128) {
|
||||
$name = substr($name, 0, 123) . substr($name, -5);
|
||||
}
|
||||
@@ -137,9 +158,16 @@ class DirectUploadService
|
||||
];
|
||||
}
|
||||
|
||||
private static function buildKeyPrefix(string $type): string
|
||||
private static function buildKeyPrefix(string $type, int $adminId = 0): string
|
||||
{
|
||||
return 'uploads/' . $type . '/' . date('Ymd') . '/';
|
||||
$prefix = 'uploads/' . $type . '/';
|
||||
if ($type === self::TYPE_DESKTOP_PACKAGE) {
|
||||
if ($adminId <= 0) {
|
||||
throw new Exception('安装包上传账号无效');
|
||||
}
|
||||
$prefix .= $adminId . '/';
|
||||
}
|
||||
return $prefix . date('Ymd') . '/';
|
||||
}
|
||||
|
||||
private static function resolveFileType(string $type): int
|
||||
@@ -150,4 +178,46 @@ class DirectUploadService
|
||||
default => FileEnum::FILE_TYPE,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 桌面安装包是可执行文件,只允许发布流程所需的 EXE / ZIP。
|
||||
*/
|
||||
private static function validateFileExtension(string $type, string $key, string $name): void
|
||||
{
|
||||
if ($type !== self::TYPE_DESKTOP_PACKAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nameExtension = strtolower((string)pathinfo($name, PATHINFO_EXTENSION));
|
||||
$keyExtension = strtolower((string)pathinfo($key, PATHINFO_EXTENSION));
|
||||
$allowedExtensions = ['exe', 'zip'];
|
||||
if (!in_array($nameExtension, $allowedExtensions, true)
|
||||
|| $nameExtension !== $keyExtension) {
|
||||
throw new Exception('桌面安装包仅支持 EXE 或 ZIP 文件');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装包 Key 绑定上传管理员,并兼容跨午夜完成的上传。
|
||||
*/
|
||||
private static function isAllowedObjectKey(string $type, string $key, int $adminId): bool
|
||||
{
|
||||
if ($key === '') {
|
||||
return false;
|
||||
}
|
||||
if ($type !== self::TYPE_DESKTOP_PACKAGE) {
|
||||
return strpos($key, self::buildKeyPrefix($type)) === 0;
|
||||
}
|
||||
if ($adminId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ownerPrefix = 'uploads/' . self::TYPE_DESKTOP_PACKAGE . '/' . $adminId . '/';
|
||||
if (strpos($key, $ownerPrefix) !== 0) {
|
||||
return false;
|
||||
}
|
||||
$date = substr($key, strlen($ownerPrefix), 8);
|
||||
return in_array($date, [date('Ymd'), date('Ymd', time() - 86400)], true)
|
||||
&& substr($key, strlen($ownerPrefix) + 8, 1) === '/';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,13 +116,19 @@ class Qcloud extends Server
|
||||
|
||||
/**
|
||||
* @notes 获取 STS 临时凭证(用于浏览器直传)
|
||||
* @param string $keyPrefix 资源前缀,如 uploads/video/20260508/
|
||||
* @param string $keyScope 资源前缀或完整对象 Key
|
||||
* @param int $maxSizeBytes 单文件大小上限(字节)
|
||||
* @param int $durationSeconds 凭证有效期(秒)
|
||||
* @param bool $exactObject 是否只授权单个对象 Key
|
||||
* @return array {credentials, expiredTime, requestId}
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getStsCredentials(string $keyPrefix, int $maxSizeBytes, int $durationSeconds = 1800): array
|
||||
public function getStsCredentials(
|
||||
string $keyScope,
|
||||
int $maxSizeBytes,
|
||||
int $durationSeconds = 1800,
|
||||
bool $exactObject = false
|
||||
): array
|
||||
{
|
||||
$bucket = $this->config['bucket'];
|
||||
// bucket 形如 likeadmin-1300000000,appId 即末段
|
||||
@@ -137,15 +143,19 @@ class Qcloud extends Server
|
||||
|
||||
$shortBucket = substr($bucket, 0, strrpos($bucket, '-'));
|
||||
$region = $this->config['region'];
|
||||
$prefix = ltrim($keyPrefix, '/');
|
||||
if ($prefix === '' || substr($prefix, -1) !== '/') {
|
||||
$prefix = $prefix . '/';
|
||||
$scope = ltrim($keyScope, '/');
|
||||
if ($scope === '') {
|
||||
throw new Exception('COS 授权对象不能为空');
|
||||
}
|
||||
if (!$exactObject && substr($scope, -1) !== '/') {
|
||||
$scope .= '/';
|
||||
}
|
||||
|
||||
$duration = max(900, min($durationSeconds, 7200));
|
||||
|
||||
// 自行构造 policy:对象级写动作收紧 + bucket 级 ListMultipartUploads(cos-js-sdk-v5 续传探测必需)
|
||||
$objectArn = sprintf('qcs::cos:%s:uid/%s:%s/%s*', $region, $appId, $bucket, $prefix);
|
||||
$objectResource = $exactObject ? $scope : $scope . '*';
|
||||
$objectArn = sprintf('qcs::cos:%s:uid/%s:%s/%s', $region, $appId, $bucket, $objectResource);
|
||||
$bucketArn = sprintf('qcs::cos:%s:uid/%s:%s/*', $region, $appId, $bucket);
|
||||
|
||||
$policy = [
|
||||
|
||||
Reference in New Issue
Block a user