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); } /** * SigV4 PUT;endpoint 必填(如 https://s3.amazonaws.com 或 MinIO 地址) */ private function uploadFile($filePath, string $key): bool|array { $accessKey = (string) ($this->config['access_key'] ?? ''); $secretKey = (string) ($this->config['secret_key'] ?? ''); $bucket = (string) ($this->config['bucket'] ?? ''); $region = (string) ($this->config['region'] ?? 'us-east-1'); $endpoint = rtrim((string) ($this->config['endpoint'] ?? ''), '/'); $domain = rtrim((string) ($this->config['domain'] ?? ''), '/'); $driver = (string) ($this->config['driver'] ?? 'aws'); if ($accessKey === '' || $secretKey === '' || $bucket === '' || $endpoint === '') { UtilsService::getInstance()->errorThrow(strtoupper($driver) . ' 配置不完整(需要 AccessKey/Secret/Bucket/Endpoint)'); } $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; $payload = file_get_contents($path); $host = parse_url($endpoint, PHP_URL_HOST) ?: preg_replace('#^https?://#', '', $endpoint); // path-style: endpoint/bucket/key $canonicalUri = '/' . rawurlencode($bucket) . '/' . str_replace('%2F', '/', rawurlencode($key)); // 简化:多数 MinIO/OBS 接受 path-style $url = $endpoint . '/' . $bucket . '/' . $key; $amzDate = gmdate('Ymd\THis\Z'); $dateStamp = gmdate('Ymd'); $payloadHash = hash('sha256', $payload); $canonicalHeaders = "host:{$host}\nx-amz-content-sha256:{$payloadHash}\nx-amz-date:{$amzDate}\n"; $signedHeaders = 'host;x-amz-content-sha256;x-amz-date'; $canonicalRequest = "PUT\n{$canonicalUri}\n\n{$canonicalHeaders}\n{$signedHeaders}\n{$payloadHash}"; $service = $driver === 'huawei' ? 's3' : 's3'; $credentialScope = "{$dateStamp}/{$region}/{$service}/aws4_request"; $stringToSign = "AWS4-HMAC-SHA256\n{$amzDate}\n{$credentialScope}\n" . hash('sha256', $canonicalRequest); $kDate = hash_hmac('sha256', $dateStamp, 'AWS4' . $secretKey, true); $kRegion = hash_hmac('sha256', $region, $kDate, true); $kService = hash_hmac('sha256', $service, $kRegion, true); $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true); $signature = hash_hmac('sha256', $stringToSign, $kSigning); $authorization = "AWS4-HMAC-SHA256 Credential={$accessKey}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}"; $response = Http::withHeaders([ 'Authorization' => $authorization, 'x-amz-content-sha256' => $payloadHash, 'x-amz-date' => $amzDate, 'Content-Type' => 'application/octet-stream', 'Host' => $host, ])->withBody($payload, 'application/octet-stream')->put($url); if (!$response->successful()) { UtilsService::getInstance()->errorThrow($driver . ' 上传失败:' . $response->body()); } $publicUrl = $domain !== '' ? ($domain . '/' . $key) : $url; return ['key' => $key, 'url' => $publicUrl]; } }