91 lines
4.2 KiB
PHP
91 lines
4.2 KiB
PHP
|
|
<?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;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* S3 兼容上传(AWS / MinIO / 华为 OBS / 百度 BOS)
|
|||
|
|
* 使用 AWS Signature Version 4 的 PUT Object
|
|||
|
|
*/
|
|||
|
|
class S3CompatibleStorageService extends BaseNotAuthService implements OssStorageInterface
|
|||
|
|
{
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 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];
|
|||
|
|
}
|
|||
|
|
}
|