|
|
@@ -0,0 +1,342 @@
|
|
|
+<?php
|
|
|
+namespace app\common\library;
|
|
|
+
|
|
|
+use think\Config;
|
|
|
+use think\Exception;
|
|
|
+use think\File;
|
|
|
+use GuzzleHttp\Client;
|
|
|
+use GuzzleHttp\Psr7\Request;
|
|
|
+
|
|
|
+class AwsS3
|
|
|
+{
|
|
|
+ protected $config;
|
|
|
+ protected $client;
|
|
|
+ protected $bucket;
|
|
|
+ protected $region;
|
|
|
+
|
|
|
+ public function __construct()
|
|
|
+ {
|
|
|
+ $this->config = Config::get('aws.s3');
|
|
|
+ $this->bucket = $this->config['bucket'];
|
|
|
+ $this->region = $this->config['region'];
|
|
|
+ $this->client = new Client([
|
|
|
+ 'timeout' => 30,
|
|
|
+ 'verify' => false, // 根据环境调整
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成 AWS S4 签名
|
|
|
+ */
|
|
|
+ protected function generateSignature($method, $uri, $headers, $payload = '')
|
|
|
+ {
|
|
|
+ $accessKey = $this->config['credentials']['key'];
|
|
|
+ $secretKey = $this->config['credentials']['secret'];
|
|
|
+
|
|
|
+ $service = 's3';
|
|
|
+ $algorithm = 'AWS4-HMAC-SHA256';
|
|
|
+
|
|
|
+ // 当前时间
|
|
|
+ $timestamp = time();
|
|
|
+ $longDate = gmdate('Ymd\THis\Z', $timestamp);
|
|
|
+ $shortDate = substr($longDate, 0, 8);
|
|
|
+
|
|
|
+ // 计算签名所需组件
|
|
|
+ $scope = $shortDate . '/' . $this->region . '/' . $service . '/aws4_request';
|
|
|
+
|
|
|
+ // 规范化请求
|
|
|
+ $canonicalHeaders = '';
|
|
|
+ $signedHeaders = '';
|
|
|
+
|
|
|
+ ksort($headers);
|
|
|
+ foreach ($headers as $key => $value) {
|
|
|
+ $canonicalHeaders .= strtolower($key) . ':' . trim($value) . "\n";
|
|
|
+ $signedHeaders .= strtolower($key) . ';';
|
|
|
+ }
|
|
|
+ $signedHeaders = rtrim($signedHeaders, ';');
|
|
|
+
|
|
|
+ $canonicalRequest = implode("\n", [
|
|
|
+ $method,
|
|
|
+ $uri,
|
|
|
+ '', // 查询字符串
|
|
|
+ $canonicalHeaders,
|
|
|
+ $signedHeaders,
|
|
|
+ hash('sha256', $payload)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 计算签名
|
|
|
+ $stringToSign = implode("\n", [
|
|
|
+ $algorithm,
|
|
|
+ $longDate,
|
|
|
+ $scope,
|
|
|
+ hash('sha256', $canonicalRequest)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 派生签名密钥
|
|
|
+ $kDate = hash_hmac('sha256', $shortDate, 'AWS4' . $secretKey, true);
|
|
|
+ $kRegion = hash_hmac('sha256', $this->region, $kDate, true);
|
|
|
+ $kService = hash_hmac('sha256', $service, $kRegion, true);
|
|
|
+ $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
|
|
|
+
|
|
|
+ $signature = hash_hmac('sha256', $stringToSign, $kSigning);
|
|
|
+
|
|
|
+ return $algorithm . ' ' . implode(', ', [
|
|
|
+ 'Credential=' . $accessKey . '/' . $scope,
|
|
|
+ 'SignedHeaders=' . $signedHeaders,
|
|
|
+ 'Signature=' . $signature
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传文件到 S3
|
|
|
+ */
|
|
|
+ public function upload($file, $key = null, $options = [],$website)
|
|
|
+ {
|
|
|
+ $link ='https://cdn.alipearlhair.com';
|
|
|
+ if($website=='alipearlhair'){
|
|
|
+ $this->bucket ='alipearl-images';
|
|
|
+ $link ='https://cdn.alipearlhair.com';
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // 处理文件
|
|
|
+ if ($file instanceof File) {
|
|
|
+ $filePath = $file->getRealPath();
|
|
|
+ $fileContent = file_get_contents($filePath);
|
|
|
+ $mimeType = $file->getMime();
|
|
|
+ } elseif (is_string($file) && file_exists($file)) {
|
|
|
+ $fileContent = file_get_contents($file);
|
|
|
+ $mimeType = mime_content_type($file);
|
|
|
+ } else {
|
|
|
+ $fileContent = $file;
|
|
|
+ $mimeType = $options['mime_type'] ?? 'application/octet-stream';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成存储 key
|
|
|
+ if (empty($key)) {
|
|
|
+ $key = $this->generateKey($file);
|
|
|
+ }
|
|
|
+ // 准备请求
|
|
|
+ $method = 'PUT';
|
|
|
+ $uri = '/' . $key;
|
|
|
+ $url = 'https://' . $this->bucket . '.s3.' . $this->region . '.amazonaws.com' . $uri;
|
|
|
+ $urls = $link . $uri;
|
|
|
+ $headers = [
|
|
|
+ 'Host' => $this->bucket . '.s3.' . $this->region . '.amazonaws.com',
|
|
|
+ 'Content-Type' => $mimeType,
|
|
|
+ 'Content-Length' => strlen($fileContent),
|
|
|
+ 'x-amz-content-sha256' => hash('sha256', $fileContent),
|
|
|
+ 'x-amz-date' => gmdate('Ymd\THis\Z'),
|
|
|
+
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 添加额外头部
|
|
|
+ if (!empty($options['headers'])) {
|
|
|
+ $headers = array_merge($headers, $options['headers']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成签名
|
|
|
+ $headers['Authorization'] = $this->generateSignature($method, $uri, $headers, $fileContent);
|
|
|
+
|
|
|
+ // 发送请求
|
|
|
+ $response = $this->client->request($method, $url, [
|
|
|
+ 'headers' => $headers,
|
|
|
+ 'body' => $fileContent,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($response->getStatusCode() == 200) {
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'key' => $key,
|
|
|
+ 'url' => $urls,
|
|
|
+ 'size' => strlen($fileContent),
|
|
|
+ 'type' => $mimeType
|
|
|
+ ];
|
|
|
+ } else {
|
|
|
+ throw new Exception('上传失败,状态码: ' . $response->getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'msg' => '上传失败: ' . $e->getMessage(),
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成存储路径 key
|
|
|
+ */
|
|
|
+ protected function generateKey($file)
|
|
|
+ {
|
|
|
+ $config = $this->config['upload'];
|
|
|
+ $savePath = $config['save_path'];
|
|
|
+
|
|
|
+ // 替换路径变量
|
|
|
+ $replace = [
|
|
|
+ '{year}' => date('Y'),
|
|
|
+ '{mon}' => date('m'),
|
|
|
+ '{day}' => date('d'),
|
|
|
+ '{hour}' => date('H'),
|
|
|
+ '{min}' => date('i'),
|
|
|
+ '{sec}' => date('s'),
|
|
|
+ '{random}' => mt_rand(1000, 9999),
|
|
|
+ '{uniqid}' => uniqid(),
|
|
|
+ '{timestamp}'=> time(),
|
|
|
+ ];
|
|
|
+
|
|
|
+ $savePath = str_replace(array_keys($replace), array_values($replace), $savePath);
|
|
|
+
|
|
|
+ // 生成文件名
|
|
|
+ // 获取原始文件名和扩展名
|
|
|
+ $originalName = $file->getInfo('name'); // 原始文件名,如 "image.jpg"
|
|
|
+ $originalExtension = pathinfo($originalName, PATHINFO_EXTENSION);
|
|
|
+
|
|
|
+// 如果原始扩展名有效,使用它
|
|
|
+ if (!empty($originalExtension) && strlen($originalExtension) <= 6) {
|
|
|
+ $extension = strtolower($originalExtension);
|
|
|
+ } else {
|
|
|
+ // 否则通过内容检测
|
|
|
+ $extension = $this->getRealExtensionByContent($file->getRealPath());
|
|
|
+ }
|
|
|
+ $sha1 = sha1($file);
|
|
|
+ $filename = date('YmdHis') . '_' . substr($sha1, 0, 16) . '.' . $extension;
|
|
|
+ return $savePath . $filename;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除 S3 文件
|
|
|
+ */
|
|
|
+ public function delete($key)
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $method = 'DELETE';
|
|
|
+ $uri = '/' . $key;
|
|
|
+ $url = 'https://' . $this->bucket . '.s3.' . $this->region . '.amazonaws.com' . $uri;
|
|
|
+
|
|
|
+ $headers = [
|
|
|
+ 'Host' => $this->bucket . '.s3.' . $this->region . '.amazonaws.com',
|
|
|
+ 'x-amz-date' => gmdate('Ymd\THis\Z'),
|
|
|
+ 'x-amz-content-sha256' => hash('sha256', ''),
|
|
|
+ ];
|
|
|
+
|
|
|
+ $headers['Authorization'] = $this->generateSignature($method, $uri, $headers);
|
|
|
+
|
|
|
+ $response = $this->client->request($method, $url, [
|
|
|
+ 'headers' => $headers,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $response->getStatusCode() == 204;
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查文件是否存在
|
|
|
+ */
|
|
|
+ public function exists($key)
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $method = 'HEAD';
|
|
|
+ $uri = '/' . $key;
|
|
|
+ $url = 'https://' . $this->bucket . '.s3.' . $this->region . '.amazonaws.com' . $uri;
|
|
|
+
|
|
|
+ $headers = [
|
|
|
+ 'Host' => $this->bucket . '.s3.' . $this->region . '.amazonaws.com',
|
|
|
+ 'x-amz-date' => gmdate('Ymd\THis\Z'),
|
|
|
+ 'x-amz-content-sha256' => hash('sha256', ''),
|
|
|
+ ];
|
|
|
+
|
|
|
+ $headers['Authorization'] = $this->generateSignature($method, $uri, $headers);
|
|
|
+
|
|
|
+ $response = $this->client->request($method, $url, [
|
|
|
+ 'headers' => $headers,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $response->getStatusCode() == 200;
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取文件 URL
|
|
|
+ */
|
|
|
+ public function getUrl($key, $expires = 3600)
|
|
|
+ {
|
|
|
+ if ($expires === null) {
|
|
|
+ // 公开读取的直接 URL
|
|
|
+ $cdnUrl = rtrim($this->config['upload']['cdn_url'], '/');
|
|
|
+ return $cdnUrl . '/' . $key;
|
|
|
+ } else {
|
|
|
+ // 生成预签名 URL
|
|
|
+ return $this->generatePresignedUrl($key, $expires);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成预签名 URL
|
|
|
+ */
|
|
|
+ protected function generatePresignedUrl($key, $expires = 3600)
|
|
|
+ {
|
|
|
+ $accessKey = $this->config['credentials']['key'];
|
|
|
+ $secretKey = $this->config['credentials']['secret'];
|
|
|
+
|
|
|
+ $timestamp = time();
|
|
|
+ $expiresAt = $timestamp + $expires;
|
|
|
+ $longDate = gmdate('Ymd\THis\Z', $timestamp);
|
|
|
+ $shortDate = substr($longDate, 0, 8);
|
|
|
+
|
|
|
+ $service = 's3';
|
|
|
+ $algorithm = 'AWS4-HMAC-SHA256';
|
|
|
+ $scope = $shortDate . '/' . $this->region . '/' . $service . '/aws4_request';
|
|
|
+
|
|
|
+ // 生成签名
|
|
|
+ $canonicalQueryString = implode('&', [
|
|
|
+ 'X-Amz-Algorithm=AWS4-HMAC-SHA256',
|
|
|
+ 'X-Amz-Credential=' . urlencode($accessKey . '/' . $scope),
|
|
|
+ 'X-Amz-Date=' . $longDate,
|
|
|
+ 'X-Amz-Expires=' . $expires,
|
|
|
+ 'X-Amz-SignedHeaders=host'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $canonicalRequest = implode("\n", [
|
|
|
+ 'GET',
|
|
|
+ '/' . $key,
|
|
|
+ $canonicalQueryString,
|
|
|
+ 'host:' . $this->bucket . '.s3.' . $this->region . '.amazonaws.com',
|
|
|
+ '',
|
|
|
+ 'host',
|
|
|
+ 'UNSIGNED-PAYLOAD'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $stringToSign = implode("\n", [
|
|
|
+ $algorithm,
|
|
|
+ $longDate,
|
|
|
+ $scope,
|
|
|
+ hash('sha256', $canonicalRequest)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 派生签名密钥
|
|
|
+ $kDate = hash_hmac('sha256', $shortDate, 'AWS4' . $secretKey, true);
|
|
|
+ $kRegion = hash_hmac('sha256', $this->region, $kDate, true);
|
|
|
+ $kService = hash_hmac('sha256', $service, $kRegion, true);
|
|
|
+ $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
|
|
|
+
|
|
|
+ $signature = hash_hmac('sha256', $stringToSign, $kSigning);
|
|
|
+
|
|
|
+ $url = sprintf(
|
|
|
+ 'https://%s.s3.%s.amazonaws.com/%s?%s&X-Amz-Signature=%s',
|
|
|
+ $this->bucket,
|
|
|
+ $this->region,
|
|
|
+ $key,
|
|
|
+ $canonicalQueryString,
|
|
|
+ $signature
|
|
|
+ );
|
|
|
+
|
|
|
+ return $url;
|
|
|
+ }
|
|
|
+}
|