Parcourir la source

图片上传到S3

bianjunhui il y a 14 heures
Parent
commit
0553f88f0e

+ 69 - 6
packages/Webkul/Admin/src/Http/Controllers/TinyMCEController.php

@@ -2,19 +2,27 @@
 
 namespace Webkul\Admin\Http\Controllers;
 
-use Illuminate\Support\Facades\Storage;
+use Aws\S3\S3Client;
+use Illuminate\Support\Str;
 use Webkul\Core\Traits\Sanitizer;
 
 class TinyMCEController extends Controller
 {
     use Sanitizer;
 
+    /**
+     * S3 client instance.
+     *
+     * @var S3Client|null
+     */
+    protected $s3Client = null;
+
     /**
      * Storage folder path.
      *
      * @var string
      */
-    private $storagePath = 'tinymce';
+    private $storagePath = 'uploads';
 
     /**
      * Allowed image MIME types.
@@ -30,6 +38,30 @@ class TinyMCEController extends Controller
         'image/webp',
     ];
 
+    /**
+     * Get S3 client instance (lazy-loaded).
+     *
+     * @return S3Client
+     */
+    protected function getS3Client()
+    {
+        if (is_null($this->s3Client)) {
+            $this->s3Client = new S3Client([
+                'version' => 'latest',
+                'region'  => env('AWS_DEFAULT_REGION'),
+                'credentials' => [
+                    'key'    => env('AWS_ACCESS_KEY_ID'),
+                    'secret' => env('AWS_SECRET_ACCESS_KEY'),
+                ],
+                'http'    => [
+                    'verify' => false,
+                ],
+            ]);
+        }
+
+        return $this->s3Client;
+    }
+
     /**
      * Upload file from tinymce.
      *
@@ -57,7 +89,7 @@ class TinyMCEController extends Controller
     }
 
     /**
-     * Store media.
+     * Store media to S3.
      *
      * @return array
      */
@@ -90,14 +122,45 @@ class TinyMCEController extends Controller
             return ['error' => trans('admin::app.components.tinymce.errors.file-extension-mismatch')];
         }
 
-        $path = $file->store($this->storagePath);
+        // 生成 S3 存储路径
+        $fileName = Str::random(40) . '.' . $extension;
+        $path = $this->storagePath . '/' . date('Ym/d') . '/' . $fileName;
+
+        $body = file_get_contents($file->getRealPath());
+
+        // SVG 需要先消毒再上传
+        if ($mimeType === 'image/svg+xml') {
+            $body = $this->sanitizeSvgContent($body);
+        }
+
+        // 上传到 S3
+        $this->getS3Client()->putObject([
+            'Bucket'      => env('AWS_BUCKET'),
+            'Key'         => $path,
+            'Body'        => $body,
+            'ContentType' => $mimeType,
+        ]);
 
-        $this->sanitizeSVG($path, $mimeType);
+        $fileUrl = rtrim(env('AWS_URL', ''), '/') . '/' . ltrim($path, '/');
 
         return [
             'file'      => $path,
             'file_name' => $file->getClientOriginalName(),
-            'file_url'  => Storage::url($path),
+            'file_url'  => $fileUrl,
         ];
     }
+
+    /**
+     * Sanitize SVG content (direct string version for S3 workflow).
+     *
+     * @param  string  $content
+     * @return string
+     */
+    protected function sanitizeSvgContent(string $content): string
+    {
+        $sanitizer = new \enshrined\svgSanitize\Sanitizer();
+        $sanitizer->removeRemoteReferences(true);
+
+        return $sanitizer->sanitize($content);
+    }
 }

+ 2 - 2
packages/Webkul/Admin/src/Http/Requests/ProductForm.php

@@ -75,7 +75,7 @@ class ProductForm extends FormRequest
         $this->rules = array_merge($this->product->getTypeInstance()->getTypeValidationRules(), [
             'sku'                  => ['required', 'unique:products,sku,'.$this->id, new Slug],
             'url_key'              => ['required', new ProductCategoryUniqueSlug('products', $this->id)],
-            'images.files.*'       => ['nullable', 'mimes:bmp,jpeg,jpg,png,webp'],
+            'images.files.*'       => ['nullable', 'mimes:bmp,jpeg,jpg,png,webp,gif'],
             'images.positions.*'   => ['nullable', 'integer'],
             'videos.files.*'       => ['nullable', 'mimetypes:application/octet-stream,video/mp4,video/webm,video/quicktime', 'max:'.$this->maxVideoFileSize],
             'videos.positions.*'   => ['nullable', 'integer'],
@@ -93,7 +93,7 @@ class ProductForm extends FormRequest
             foreach (request()->images['files'] as $key => $file) {
                 if (Str::contains($key, 'image_')) {
                     $this->rules = array_merge($this->rules, [
-                        'images.files.'.$key => ['required', 'mimes:bmp,jpeg,jpg,png,webp'],
+                        'images.files.'.$key => ['required', 'mimes:bmp,jpeg,jpg,png,webp,gif'],
                     ]);
                 }
             }

+ 25 - 13
packages/Webkul/Product/src/Repositories/ProductMediaRepository.php

@@ -56,13 +56,9 @@ class ProductMediaRepository extends Repository
             foreach ($data[$uploadFileType]['files'] as $indexOrModelId => $file) {
                 if ($file instanceof UploadedFile) {
                     if (Str::contains($file->getMimeType(), 'image')) {
-                        $manager = new ImageManager;
+                        $extension = strtolower($file->getClientOriginalExtension());
 
-                        $image = $manager->make($file)->encode('webp');
-
-                        $path = $this->getProductDirectory($product).'/'.Str::random(40).'.webp';
-                        //Storage::put($path, $image);
-                        // 方式一:使用原生 AWS SDK 绕过 Laravel Storage
+                        // 使用原生 AWS SDK 绕过 Laravel Storage
                         $s3Client = new S3Client([
                             'version' => 'latest',
                             'region'  => env('AWS_DEFAULT_REGION'),
@@ -75,13 +71,29 @@ class ProductMediaRepository extends Repository
                             ],
                         ]);
 
-                        $result = $s3Client->putObject([
-                            'Bucket' => env('AWS_BUCKET'),
-                            'Key'    => $path,
-                            'Body'   => (string) $image,
-                            'ContentType' => 'image/webp',
-                        ]);
-
+                        if ($extension === 'gif') {
+                            // GIF 动画:直接上传原始文件,不经 ImageManager 处理以保留动画帧
+                            $path = $this->getProductDirectory($product) . '/' . Str::random(40) . '.gif';
+
+                            $result = $s3Client->putObject([
+                                'Bucket'      => env('AWS_BUCKET'),
+                                'Key'         => $path,
+                                'Body'        => fopen($file->getRealPath(), 'r'),
+                                'ContentType' => 'image/gif',
+                            ]);
+                        } else {
+                            // 其他图片:转为 webp 后上传
+                            $manager = new ImageManager;
+                            $image = $manager->make($file)->encode('webp');
+                            $path = $this->getProductDirectory($product) . '/' . Str::random(40) . '.webp';
+
+                            $result = $s3Client->putObject([
+                                'Bucket'      => env('AWS_BUCKET'),
+                                'Key'         => $path,
+                                'Body'        => (string) $image,
+                                'ContentType' => 'image/webp',
+                            ]);
+                        }
                     } else {
                         $path = $file->store($this->getProductDirectory($product));
                     }