Kaynağa Gözat

文件上传功能重构

jqh 5 yıl önce
ebeveyn
işleme
5cf672d126

+ 9 - 0
src/Admin.php

@@ -21,6 +21,7 @@ use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Fluent;
 
 /**
  * Class Admin.
@@ -432,6 +433,14 @@ class Admin
         Event::dispatch('admin.booted');
     }
 
+    /**
+     * @return Fluent
+     */
+    public static function context()
+    {
+        return app('admin.context');
+    }
+
     /**
      * 获取js配置.
      *

+ 2 - 0
src/AdminServiceProvider.php

@@ -52,6 +52,7 @@ class AdminServiceProvider extends ServiceProvider
         'admin.permission' => Middleware\Permission::class,
         'admin.bootstrap'  => Middleware\Bootstrap::class,
         'admin.session'    => Middleware\Session::class,
+        'admin.upload'     => Middleware\WebUploader::class,
     ];
 
     /**
@@ -65,6 +66,7 @@ class AdminServiceProvider extends ServiceProvider
             'admin.bootstrap',
             'admin.permission',
             'admin.session',
+            'admin.upload',
         ],
     ];
 

+ 5 - 9
src/Form/Concerns/HasFiles.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Form\Concerns;
 
+use Dcat\Admin\Admin;
 use Dcat\Admin\Contracts\UploadField as UploadFieldInterface;
 use Dcat\Admin\Form\Builder;
 use Dcat\Admin\Form\Field;
@@ -22,9 +23,9 @@ trait HasFiles
     protected function handleUploadFile($data)
     {
         $column = $data['upload_column'] ?? null;
-        $file = $data['file'] ?? null;
+        $file = Admin::context()->webUploader->getCompleteUploadedFile() ?: ($data['file'] ?? null);
 
-        if (! $column && ! $file instanceof UploadedFile) {
+        if (! $column || ! $file instanceof UploadedFile) {
             return;
         }
 
@@ -37,13 +38,8 @@ trait HasFiles
 
             $response = $field->upload($file);
 
-            // 判断是否是分块上传
-            $isChunking = $response instanceof JsonResponse && ($response->getData(true)['merge'] ?? false);
-
-            if (! $isChunking) {
-                if (($results = $this->callUploaded($field, $file, $response)) && $results instanceof Response) {
-                    return $results;
-                }
+            if (($results = $this->callUploaded($field, $file, $response)) && $results instanceof Response) {
+                return $results;
             }
 
             return $response;

+ 15 - 153
src/Form/Field/UploadField.php

@@ -2,11 +2,11 @@
 
 namespace Dcat\Admin\Form\Field;
 
+use Dcat\Admin\Admin;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\URL;
 use Illuminate\Support\Facades\Validator;
-use Symfony\Component\Finder\Finder;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -166,19 +166,21 @@ trait UploadField
     public function upload(UploadedFile $file)
     {
         try {
-            $id = request('_id');
-            if (! $id) {
-                return $this->responseError(403, 'Missing id');
-            }
+            $request = request();
+
+            $id = $request->get('_id');
 
-            if (! ($file = $this->mergeChunks($id, $file))) {
-                return $this->response(['merge' => 1]);
+            /* @var \Dcat\Admin\Support\WebUploader $webUploader */
+            $webUploader = Admin::context()->webUploader;
+
+            if (! $id) {
+                return $webUploader->responseErrorMessage(403, 'Missing id');
             }
 
             if ($errors = $this->getErrorMessages($file)) {
-                $this->deleteTempFile();
+                $webUploader->deleteTempFile();
 
-                return $this->responseError(101, $errors);
+                return $webUploader->responseValidationMessage($errors);
             }
 
             $this->name = $this->getStoreName($file);
@@ -193,23 +195,17 @@ trait UploadField
                 $result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name);
             }
 
-            $this->deleteTempFile();
+            $webUploader->deleteTempFile();
 
             if ($result) {
                 $path = $this->getUploadPath();
 
-                return $this->response([
-                    'status' => true,
-                    'id'     => $path,
-                    'name'   => $this->name,
-                    'path'   => basename($path),
-                    'url'    => $this->objectUrl($path),
-                ]);
+                return $webUploader->responseUploaded($path, $this->objectUrl($path));
             }
 
-            return $this->responseError(107, trans('admin.upload.upload_failed'));
+            return $webUploader->responseFailedMessage();
         } catch (\Throwable $e) {
-            $this->deleteTempFile();
+            $webUploader->deleteTempFile();
 
             throw $e;
         }
@@ -222,140 +218,6 @@ trait UploadField
     {
     }
 
-    /**
-     * @param string       $id
-     * @param UploadedFile $file
-     *
-     * @return UploadedFile|null
-     */
-    protected function mergeChunks($id, UploadedFile $file)
-    {
-        $chunk = request('chunk', 0);
-        $chunks = request('chunks', 1);
-
-        if ($chunks <= 1) {
-            return $file;
-        }
-
-        $tmpDir = $this->getTempDir($id);
-        $newFilename = md5($file->getClientOriginalName());
-
-        $file->move($tmpDir, "{$newFilename}.{$chunk}.part");
-
-        $done = true;
-        for ($index = 0; $index < $chunks; $index++) {
-            if (! is_file("{$tmpDir}/{$newFilename}.{$index}.part")) {
-                $done = false;
-                break;
-            }
-        }
-
-        if (! $done) {
-            return;
-        }
-
-        $this->tempFilePath = $tmpDir.'/'.$newFilename.'.tmp';
-        $this->putTempFileContent($chunks, $tmpDir, $newFilename);
-
-        return new UploadedFile(
-            $this->tempFilePath,
-            $file->getClientOriginalName(),
-            'application/octet-stream',
-            UPLOAD_ERR_OK,
-            true
-        );
-    }
-
-    /**
-     * Deletes the temporary file.
-     */
-    public function deleteTempFile()
-    {
-        if (! $this->tempFilePath) {
-            return;
-        }
-        @unlink($this->tempFilePath);
-
-        if (
-            ! Finder::create()
-            ->in($dir = dirname($this->tempFilePath))
-            ->files()
-            ->count()
-        ) {
-            @rmdir($dir);
-        }
-    }
-
-    /**
-     * @param int    $chunks
-     * @param string $tmpDir
-     * @param string $newFilename
-     */
-    protected function putTempFileContent($chunks, $tmpDir, $newFileame)
-    {
-        $out = fopen($this->tempFilePath, 'wb');
-
-        if (flock($out, LOCK_EX)) {
-            for ($index = 0; $index < $chunks; $index++) {
-                $partPath = "{$tmpDir}/{$newFileame}.{$index}.part";
-                if (! $in = @fopen($partPath, 'rb')) {
-                    break;
-                }
-
-                while ($buff = fread($in, 4096)) {
-                    fwrite($out, $buff);
-                }
-
-                @fclose($in);
-                @unlink($partPath);
-            }
-
-            flock($out, LOCK_UN);
-        }
-
-        fclose($out);
-    }
-
-    /**
-     * @param mixed $id
-     *
-     * @return string
-     */
-    protected function getTempDir($id)
-    {
-        $tmpDir = storage_path('tmp/'.$id);
-
-        if (! is_dir($tmpDir)) {
-            app('files')->makeDirectory($tmpDir, 0755, true);
-        }
-
-        return $tmpDir;
-    }
-
-    /**
-     * Response the error messages.
-     *
-     * @param $code
-     * @param $error
-     *
-     * @return \Illuminate\Http\JsonResponse
-     */
-    protected function responseError($code, $error)
-    {
-        return $this->response([
-            'error' => ['code' => $code, 'message' => $error], 'status' => false,
-        ]);
-    }
-
-    /**
-     * @param array $message
-     *
-     * @return \Illuminate\Http\JsonResponse
-     */
-    protected function response(array $message)
-    {
-        return response()->json($message);
-    }
 
     /**
      * Specify the directory and name for upload file.

+ 39 - 0
src/Middleware/WebUploader.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Dcat\Admin\Middleware;
+
+use Dcat\Admin\Admin;
+use Dcat\Admin\Support\WebUploader as Uploader;
+use Illuminate\Http\Request;
+use Symfony\Component\HttpFoundation\File\Exception\FileException;
+
+/**
+ * 文件分块上传合并处理中间件.
+ *
+ * Class WebUploader
+ * @package Dcat\Admin\Middleware
+ */
+class WebUploader
+{
+    public function handle(Request $request, \Closure $next)
+    {
+        Admin::context()->webUploader = $webUploader = new Uploader();
+
+        if (! $webUploader->isUploading()) {
+            return $next($request);
+        }
+
+        try {
+            if (! $file = $webUploader->getCompleteUploadedFile()) {
+                // 分块未上传完毕,返回已合并成功信息
+                return $webUploader->responseMerged();
+            }
+        } catch (FileException $e) {
+            $webUploader->deleteTempFile();
+
+            throw $e;
+        }
+
+        return $next($request);
+    }
+}

+ 332 - 0
src/Support/WebUploader.php

@@ -0,0 +1,332 @@
+<?php
+
+namespace Dcat\Admin\Support;
+
+use Illuminate\Http\Request;
+use Symfony\Component\Finder\Finder;
+use Symfony\Component\HttpFoundation\File\Exception\FileException;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+/**
+ * WebUploader文件上传处理.
+ *
+ * @property string       $_id
+ * @property int          $chunk
+ * @property int          $chunks
+ * @property string       $upload_column
+ * @property UploadedFile $file
+ */
+class WebUploader
+{
+    public $tempDirectory = 'tmp';
+
+    protected $tempFilePath;
+
+    protected $completeFile;
+
+    public function __construct(Request $request = null)
+    {
+        $request = $request ?: request();
+
+        $this->_id = $request->get('_id');
+        $this->chunk = $request->get('chunk');
+        $this->chunks = $request->get('chunks');
+        $this->upload_column = $request->get('upload_column');
+        $this->file = $request->file('file');
+    }
+
+    /**
+     * 判断是否是分块上传.
+     *
+     * @return bool
+     */
+    public function hasChunkFile()
+    {
+        return $this->chunks > 1;
+    }
+
+    /**
+     * 判断是否是文件上传请求.
+     *
+     * @return bool
+     */
+    public function isUploading()
+    {
+        $file = $this->file;
+
+        if (
+            ! $file
+            || ! $this->upload_column
+            || ! $file instanceof UploadedFile
+        ) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 响应上传成功信息.
+     *
+     * @param string $path 文件完整路径
+     * @param string $url
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function responseUploaded(string $path, string $url)
+    {
+        return $this->response([
+            'status' => true,
+            'id'     => $path,
+            'name'   => basename($path),
+            'path'   => basename($path),
+            'url'    => $url,
+        ]);
+    }
+
+    /**
+     * 返回分块文件已合并信息.
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function responseMerged()
+    {
+        return $this->response(['merge' => 1]);
+    }
+
+    /**
+     * 响应失败信息.
+     *
+     * @param string $message
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function responseFailedMessage(string $message = null)
+    {
+        return $this->responseErrorMessage(107, $message ?: trans('admin.upload.upload_failed'));
+    }
+
+    /**
+     * @param FileException $e
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function responseFileException(FileException $e)
+    {
+        return $this->responseValidationMessage($e->getMessage());
+    }
+
+    /**
+     * 响应验证失败信息.
+     *
+     * @param mixed $message
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function responseValidationMessage($message)
+    {
+        return $this->responseErrorMessage(103, $message);
+    }
+
+    /**
+     * 响应失败信息.
+     *
+     * @param $code
+     * @param $error
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function responseErrorMessage($code, $error)
+    {
+        return $this->response([
+            'error' => ['code' => $code, 'message' => $error], 'status' => false,
+        ]);
+    }
+
+    /**
+     * 获取完整的上传文件.
+     *
+     * @return UploadedFile|void
+     */
+    public function getCompleteUploadedFile()
+    {
+        $file = $this->file;
+
+        if (! $file || ! $file instanceof UploadedFile) {
+            return;
+        }
+
+        if (! $this->hasChunkFile()) {
+            return $file;
+        }
+
+        if ($this->completeFile !== null) {
+            return $this->completeFile;
+        }
+
+        return $this->completeFile = $this->mergeChunks($file);
+    }
+
+    /**
+     * 移除临时文件以及文件夹.
+     */
+    public function deleteTempFile()
+    {
+        if (! $this->tempFilePath) {
+            return;
+        }
+        @unlink($this->tempFilePath);
+
+        if (
+            ! Finder::create()
+                ->in($dir = dirname($this->tempFilePath))
+                ->files()
+                ->count()
+        ) {
+            @rmdir($dir);
+        }
+    }
+
+    /**
+     * 合并分块文件.
+     *
+     * @param UploadedFile $file
+     *
+     * @return UploadedFile|false
+     */
+    protected function mergeChunks(UploadedFile $file)
+    {
+        $tmpDir = $this->getTempPath($this->_id);
+        $newFilename = $this->generateChunkFileName($file);
+
+        // 移动当前分块到临时目录.
+        $this->moveChunk($file, $tmpDir, $newFilename);
+
+        // 判断所有分块是否上传完毕.
+        if (! $this->isComplete($tmpDir, $newFilename)) {
+            return false;
+        }
+
+        $this->tempFilePath = $tmpDir.'/'.$newFilename.'.tmp';
+
+        $this->putTempFileContent($this->tempFilePath, $tmpDir, $newFilename);
+
+        return new UploadedFile(
+            $this->tempFilePath,
+            $file->getClientOriginalName(),
+            null,
+            null,
+            true
+        );
+    }
+
+    /**
+     * 判断所有分块是否上传完毕.
+     *
+     * @param string $tmpDir
+     * @param string $newFilename
+     *
+     * @return bool
+     */
+    protected function isComplete($tmpDir, $newFilename)
+    {
+        for ($index = 0; $index < $this->chunks; $index++) {
+            if (! is_file("{$tmpDir}/{$newFilename}.{$index}.part")) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * 移动分块文件到临时目录.
+     *
+     * @param UploadedFile $file
+     * @param string $tmpDir
+     * @param string $newFilename
+     */
+    protected function moveChunk(UploadedFile $file, $tmpDir, $newFilename)
+    {
+        $file->move($tmpDir, "{$newFilename}.{$this->chunk}.part");
+    }
+
+    /**
+     * @param string $path
+     * @param string $tmpDir
+     * @param string $newFilename
+     */
+    protected function putTempFileContent($path, $tmpDir, $newFileame)
+    {
+        $out = fopen($path, 'wb');
+
+        if (flock($out, LOCK_EX)) {
+            for ($index = 0; $index < $this->chunks; $index++) {
+                $partPath = "{$tmpDir}/{$newFileame}.{$index}.part";
+                if (! $in = @fopen($partPath, 'rb')) {
+                    break;
+                }
+
+                while ($buff = fread($in, 4096)) {
+                    fwrite($out, $buff);
+                }
+
+                @fclose($in);
+                @unlink($partPath);
+            }
+
+            flock($out, LOCK_UN);
+        }
+
+        fclose($out);
+    }
+
+    /**
+     * 生成分块文件名称.
+     *
+     * @param UploadedFile $file
+     *
+     * @return string
+     */
+    protected function generateChunkFileName(UploadedFile $file)
+    {
+        return md5($file->getClientOriginalName());
+    }
+
+    /**
+     * 获取临时文件路径.
+     *
+     * @param mixed $path
+     *
+     * @return string
+     */
+    public function getTempPath($path)
+    {
+        return $this->getTempDirectory().'/'.$path;
+    }
+
+    /**
+     * 获取临时文件目录.
+     *
+     * @return string
+     */
+    public function getTempDirectory()
+    {
+        $dir = storage_path($this->tempDirectory);
+
+        if (! is_dir($dir)) {
+            app('files')->makeDirectory($dir, 0755, true);
+        }
+
+        return trim($dir, '/');
+    }
+
+    /**
+     * @param array $data
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function response($data)
+    {
+        return response()->json($data);
+    }
+}