UploadField.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. <?php
  2. namespace Dcat\Admin\Form\Field;
  3. use Illuminate\Support\Arr;
  4. use Illuminate\Support\Facades\Storage;
  5. use Illuminate\Support\Facades\Validator;
  6. use Symfony\Component\Finder\Finder;
  7. use Symfony\Component\HttpFoundation\File\UploadedFile;
  8. use Symfony\Component\HttpFoundation\Response;
  9. use Illuminate\Support\Facades\URL;
  10. trait UploadField
  11. {
  12. /**
  13. * Upload directory.
  14. *
  15. * @var string
  16. */
  17. protected $directory = '';
  18. /**
  19. * File name.
  20. *
  21. * @var null
  22. */
  23. protected $name = null;
  24. /**
  25. * Storage instance.
  26. *
  27. * @var \Illuminate\Filesystem\Filesystem
  28. */
  29. protected $storage;
  30. /**
  31. * If use unique name to store upload file.
  32. *
  33. * @var bool
  34. */
  35. protected $useUniqueName = false;
  36. /**
  37. * If use sequence name to store upload file.
  38. *
  39. * @var bool
  40. */
  41. protected $useSequenceName = false;
  42. /**
  43. * Controls the storage permission. Could be 'private' or 'public'.
  44. *
  45. * @var string
  46. */
  47. protected $storagePermission;
  48. /**
  49. * @var string
  50. */
  51. protected $tempFilePath;
  52. /**
  53. * Retain file when delete record from DB.
  54. *
  55. * @var bool
  56. */
  57. protected $retainable = false;
  58. /**
  59. * Initialize the storage instance.
  60. *
  61. * @return void.
  62. */
  63. protected function initStorage()
  64. {
  65. $this->disk(config('admin.upload.disk'));
  66. if (! $this->storage) {
  67. $this->storage = false;
  68. }
  69. }
  70. /**
  71. * If name already exists, rename it.
  72. *
  73. * @param $file
  74. *
  75. * @return void
  76. */
  77. public function renameIfExists(UploadedFile $file)
  78. {
  79. if ($this->getStorage()->exists("{$this->getDirectory()}/$this->name")) {
  80. $this->name = $this->generateUniqueName($file);
  81. }
  82. }
  83. /**
  84. * @return string
  85. */
  86. protected function getUploadPath()
  87. {
  88. return "{$this->getDirectory()}/$this->name";
  89. }
  90. /**
  91. * Get store name of upload file.
  92. *
  93. * @param UploadedFile $file
  94. *
  95. * @return string
  96. */
  97. protected function getStoreName(UploadedFile $file)
  98. {
  99. if ($this->useUniqueName) {
  100. return $this->generateUniqueName($file);
  101. }
  102. if ($this->useSequenceName) {
  103. return $this->generateSequenceName($file);
  104. }
  105. if ($this->name instanceof \Closure) {
  106. return $this->name->call($this, $file);
  107. }
  108. if (is_string($this->name)) {
  109. return $this->name;
  110. }
  111. return $file->getClientOriginalName();
  112. }
  113. /**
  114. * Get directory for store file.
  115. *
  116. * @return mixed|string
  117. */
  118. public function getDirectory()
  119. {
  120. if ($this->directory instanceof \Closure) {
  121. return call_user_func($this->directory, $this->form);
  122. }
  123. return $this->directory ?: $this->defaultDirectory();
  124. }
  125. /**
  126. * Indicates if the underlying field is retainable.
  127. *
  128. * @return $this
  129. */
  130. public function retainable($retainable = true)
  131. {
  132. $this->retainable = $retainable;
  133. return $this;
  134. }
  135. /**
  136. * Upload File.
  137. *
  138. * @param UploadedFile $file
  139. * @return Response
  140. */
  141. public function upload(UploadedFile $file)
  142. {
  143. try {
  144. $id = request('_id');
  145. if (!$id) {
  146. return $this->responseError(403, 'Missing id');
  147. }
  148. if (! ($file = $this->mergeChunks($id, $file))) {
  149. return $this->response(['merge' => 1]);
  150. }
  151. if ($errors = $this->getErrorMessages($file)) {
  152. $this->deleteTempFile();
  153. return $this->responseError(101, $errors);
  154. }
  155. $this->name = $this->getStoreName($file);
  156. $this->renameIfExists($file);
  157. $this->prepareFile($file);
  158. if (!is_null($this->storagePermission)) {
  159. $result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name, $this->storagePermission);
  160. } else {
  161. $result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name);
  162. }
  163. $this->deleteTempFile();
  164. if ($result) {
  165. $path = $this->getUploadPath();
  166. return $this->response([
  167. 'status' => true,
  168. 'id' => $path,
  169. 'name' => $this->name,
  170. 'path' => basename($path),
  171. 'url' => $this->objectUrl($path)
  172. ]);
  173. }
  174. return $this->responseError(107, trans('admin.upload.upload_failed'));
  175. } catch (\Throwable $e) {
  176. $this->deleteTempFile();
  177. throw $e;
  178. }
  179. }
  180. /**
  181. * @param UploadedFile $file
  182. */
  183. protected function prepareFile(UploadedFile $file)
  184. {
  185. }
  186. /**
  187. * @param string $id
  188. * @param UploadedFile $file
  189. * @return UploadedFile|null
  190. */
  191. protected function mergeChunks($id, UploadedFile $file)
  192. {
  193. $chunk = request('chunk', 0);
  194. $chunks = request('chunks', 1);
  195. if ($chunks <= 1) {
  196. return $file;
  197. }
  198. $tmpDir = $this->getTempDir($id);
  199. $newFilename = md5($file->getClientOriginalName());
  200. $file->move($tmpDir, "{$newFilename}.{$chunk}.part");
  201. $done = true;
  202. for ($index = 0; $index < $chunks; $index++) {
  203. if (!is_file("{$tmpDir}/{$newFilename}.{$index}.part")) {
  204. $done = false;
  205. break;
  206. }
  207. }
  208. if (!$done) return;
  209. $this->tempFilePath = $tmpDir.'/'.$newFilename.'.tmp';
  210. $this->putTempFileContent($chunks, $tmpDir, $newFilename);
  211. return new UploadedFile(
  212. $this->tempFilePath,
  213. $file->getClientOriginalName(),
  214. 'application/octet-stream',
  215. UPLOAD_ERR_OK,
  216. true
  217. );
  218. }
  219. /**
  220. * Deletes the temporary file.
  221. */
  222. public function deleteTempFile()
  223. {
  224. if (!$this->tempFilePath) {
  225. return;
  226. }
  227. @unlink($this->tempFilePath);
  228. if (
  229. !Finder::create()
  230. ->in($dir = dirname($this->tempFilePath))
  231. ->files()
  232. ->count()
  233. ) {
  234. @rmdir($dir);
  235. }
  236. }
  237. /**
  238. * @param int $chunks
  239. * @param string $tmpDir
  240. * @param string $newFilename
  241. */
  242. protected function putTempFileContent($chunks, $tmpDir, $newFileame)
  243. {
  244. $out = fopen($this->tempFilePath, 'wb');
  245. if (flock($out, LOCK_EX)) {
  246. for ($index = 0; $index < $chunks; $index++) {
  247. $partPath = "{$tmpDir}/{$newFileame}.{$index}.part";
  248. if (!$in = @fopen($partPath, 'rb')) {
  249. break;
  250. }
  251. while ($buff = fread($in, 4096)) {
  252. fwrite($out, $buff);
  253. }
  254. @fclose($in);
  255. @unlink($partPath);
  256. }
  257. flock($out, LOCK_UN);
  258. }
  259. fclose($out);
  260. }
  261. /**
  262. * @param $id
  263. * @return string
  264. */
  265. protected function getTempDir($id)
  266. {
  267. $tmpDir = storage_path('tmp/' . $id);
  268. if (!is_dir($tmpDir)) {
  269. app('files')->makeDirectory($tmpDir, 0755, true);
  270. }
  271. return $tmpDir;
  272. }
  273. /**
  274. * Response the error messages.
  275. *
  276. * @param $code
  277. * @param $error
  278. * @return \Illuminate\Http\JsonResponse
  279. */
  280. protected function responseError($code, $error)
  281. {
  282. return $this->response([
  283. 'error' => ['code' => $code, 'message' => $error], 'status' => false
  284. ]);
  285. }
  286. /**
  287. *
  288. * @param array $message
  289. * @return \Illuminate\Http\JsonResponse
  290. */
  291. protected function response(array $message)
  292. {
  293. return response()->json($message);
  294. }
  295. /**
  296. * Specify the directory and name for upload file.
  297. *
  298. * @param string $directory
  299. * @param null|string $name
  300. *
  301. * @return $this
  302. */
  303. public function move($directory, $name = null)
  304. {
  305. $this->dir($directory);
  306. $this->name($name);
  307. return $this;
  308. }
  309. /**
  310. * Specify the directory upload file.
  311. *
  312. * @param string $dir
  313. *
  314. * @return $this
  315. */
  316. public function dir($dir)
  317. {
  318. if ($dir) {
  319. $this->directory = $dir;
  320. }
  321. return $this;
  322. }
  323. /**
  324. * Set name of store name.
  325. *
  326. * @param string|callable $name
  327. *
  328. * @return $this
  329. */
  330. public function name($name)
  331. {
  332. if ($name) {
  333. $this->name = $name;
  334. }
  335. return $this;
  336. }
  337. /**
  338. * Use unique name for store upload file.
  339. *
  340. * @return $this
  341. */
  342. public function uniqueName()
  343. {
  344. $this->useUniqueName = true;
  345. return $this;
  346. }
  347. /**
  348. * Use sequence name for store upload file.
  349. *
  350. * @return $this
  351. */
  352. public function sequenceName()
  353. {
  354. $this->useSequenceName = true;
  355. return $this;
  356. }
  357. /**
  358. * Generate a unique name for uploaded file.
  359. *
  360. * @param UploadedFile $file
  361. *
  362. * @return string
  363. */
  364. protected function generateUniqueName(UploadedFile $file)
  365. {
  366. return md5(uniqid()) . '.' . $file->getClientOriginalExtension();
  367. }
  368. /**
  369. * Generate a sequence name for uploaded file.
  370. *
  371. * @param UploadedFile $file
  372. *
  373. * @return string
  374. */
  375. protected function generateSequenceName(UploadedFile $file)
  376. {
  377. $index = 1;
  378. $extension = $file->getClientOriginalExtension();
  379. $originalName = $file->getClientOriginalName();
  380. $newName = $originalName . '_' . $index . '.' . $extension;
  381. while ($this->getStorage()->exists("{$this->getDirectory()}/$newName")) {
  382. $index++;
  383. $newName = $originalName . '_' . $index . '.' . $extension;
  384. }
  385. return $newName;
  386. }
  387. /**
  388. * @param UploadedFile $file
  389. *
  390. * @return bool|\Illuminate\Support\MessageBag
  391. */
  392. protected function getErrorMessages(UploadedFile $file)
  393. {
  394. $rules = $attributes = [];
  395. if (!$fieldRules = $this->getRules()) {
  396. return false;
  397. }
  398. $rules[$this->column] = $fieldRules;
  399. $attributes[$this->column] = $this->label;
  400. /* @var \Illuminate\Validation\Validator $validator */
  401. $validator = Validator::make([$this->column => $file], $rules, $this->validationMessages, $attributes);
  402. if (!$validator->passes()) {
  403. $errors = $validator->errors()->getMessages()[$this->column];
  404. return implode('; ', $errors);
  405. }
  406. }
  407. /**
  408. * Destroy original files.
  409. *
  410. * @return void.
  411. */
  412. public function destroy()
  413. {
  414. if ($this->retainable) {
  415. return;
  416. }
  417. $this->deleteFile($this->original);
  418. }
  419. /**
  420. * Destroy original files.
  421. *
  422. * @param $file
  423. */
  424. public function destroyIfChanged($file)
  425. {
  426. if (! $file || ! $this->original) {
  427. return $this->destroy();
  428. }
  429. $file = array_filter((array)$file);
  430. $original = (array)$this->original;
  431. $this->deleteFile(Arr::except(array_combine($original, $original), $file));
  432. }
  433. /**
  434. * Destroy files.
  435. *
  436. * @param $path
  437. */
  438. public function deleteFile($path)
  439. {
  440. if (!$path) {
  441. return;
  442. }
  443. if (is_array($path)) {
  444. foreach ($path as $v) {
  445. $this->deleteFile($v);
  446. }
  447. return;
  448. }
  449. $storage = $this->getStorage();
  450. if ($storage->exists($path)) {
  451. $storage->delete($path);
  452. } else {
  453. $prefix = $storage->url('');
  454. $path = str_replace($prefix, '', $path);
  455. if ($storage->exists($path)) {
  456. $storage->delete($path);
  457. }
  458. }
  459. }
  460. /**
  461. * Get storage instance.
  462. *
  463. * @return \Illuminate\Filesystem\Filesystem|null
  464. */
  465. public function getStorage()
  466. {
  467. if ($this->storage === null) {
  468. $this->initStorage();
  469. }
  470. return $this->storage;
  471. }
  472. /**
  473. * Set disk for storage.
  474. *
  475. * @param string $disk Disks defined in `config/filesystems.php`.
  476. *
  477. * @return $this
  478. * @throws \Exception
  479. *
  480. */
  481. public function disk($disk)
  482. {
  483. try {
  484. $this->storage = Storage::disk($disk);
  485. } catch (\Exception $exception) {
  486. if (!array_key_exists($disk, config('filesystems.disks'))) {
  487. admin_error(
  488. 'Config error.',
  489. "Disk [$disk] not configured, please add a disk config in `config/filesystems.php`."
  490. );
  491. return $this;
  492. }
  493. throw $exception;
  494. }
  495. return $this;
  496. }
  497. /**
  498. * Get file visit url.
  499. *
  500. * @param $path
  501. *
  502. * @return string
  503. */
  504. public function objectUrl($path)
  505. {
  506. if (URL::isValidUrl($path)) {
  507. return $path;
  508. }
  509. return $this->getStorage()->url($path);
  510. }
  511. /**
  512. * @param $permission
  513. * @return $this
  514. */
  515. public function storagePermission($permission)
  516. {
  517. $this->storagePermission = $permission;
  518. return $this;
  519. }
  520. }