UploadField.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <?php
  2. namespace Dcat\Admin\Form\Field;
  3. use Illuminate\Support\Arr;
  4. use Illuminate\Support\Facades\Storage;
  5. use Illuminate\Support\Facades\URL;
  6. use Illuminate\Support\Facades\Validator;
  7. use Symfony\Component\Finder\Finder;
  8. use Symfony\Component\HttpFoundation\File\UploadedFile;
  9. use Symfony\Component\HttpFoundation\Response;
  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. *
  140. * @return Response
  141. */
  142. public function upload(UploadedFile $file)
  143. {
  144. try {
  145. $id = request('_id');
  146. if (! $id) {
  147. return $this->responseError(403, 'Missing id');
  148. }
  149. if (! ($file = $this->mergeChunks($id, $file))) {
  150. return $this->response(['merge' => 1]);
  151. }
  152. if ($errors = $this->getErrorMessages($file)) {
  153. $this->deleteTempFile();
  154. return $this->responseError(101, $errors);
  155. }
  156. $this->name = $this->getStoreName($file);
  157. $this->renameIfExists($file);
  158. $this->prepareFile($file);
  159. if (! is_null($this->storagePermission)) {
  160. $result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name, $this->storagePermission);
  161. } else {
  162. $result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name);
  163. }
  164. $this->deleteTempFile();
  165. if ($result) {
  166. $path = $this->getUploadPath();
  167. return $this->response([
  168. 'status' => true,
  169. 'id' => $path,
  170. 'name' => $this->name,
  171. 'path' => basename($path),
  172. 'url' => $this->objectUrl($path),
  173. ]);
  174. }
  175. return $this->responseError(107, trans('admin.upload.upload_failed'));
  176. } catch (\Throwable $e) {
  177. $this->deleteTempFile();
  178. throw $e;
  179. }
  180. }
  181. /**
  182. * @param UploadedFile $file
  183. */
  184. protected function prepareFile(UploadedFile $file)
  185. {
  186. }
  187. /**
  188. * @param string $id
  189. * @param UploadedFile $file
  190. *
  191. * @return UploadedFile|null
  192. */
  193. protected function mergeChunks($id, UploadedFile $file)
  194. {
  195. $chunk = request('chunk', 0);
  196. $chunks = request('chunks', 1);
  197. if ($chunks <= 1) {
  198. return $file;
  199. }
  200. $tmpDir = $this->getTempDir($id);
  201. $newFilename = md5($file->getClientOriginalName());
  202. $file->move($tmpDir, "{$newFilename}.{$chunk}.part");
  203. $done = true;
  204. for ($index = 0; $index < $chunks; $index++) {
  205. if (! is_file("{$tmpDir}/{$newFilename}.{$index}.part")) {
  206. $done = false;
  207. break;
  208. }
  209. }
  210. if (! $done) {
  211. return;
  212. }
  213. $this->tempFilePath = $tmpDir.'/'.$newFilename.'.tmp';
  214. $this->putTempFileContent($chunks, $tmpDir, $newFilename);
  215. return new UploadedFile(
  216. $this->tempFilePath,
  217. $file->getClientOriginalName(),
  218. 'application/octet-stream',
  219. UPLOAD_ERR_OK,
  220. true
  221. );
  222. }
  223. /**
  224. * Deletes the temporary file.
  225. */
  226. public function deleteTempFile()
  227. {
  228. if (! $this->tempFilePath) {
  229. return;
  230. }
  231. @unlink($this->tempFilePath);
  232. if (
  233. ! Finder::create()
  234. ->in($dir = dirname($this->tempFilePath))
  235. ->files()
  236. ->count()
  237. ) {
  238. @rmdir($dir);
  239. }
  240. }
  241. /**
  242. * @param int $chunks
  243. * @param string $tmpDir
  244. * @param string $newFilename
  245. */
  246. protected function putTempFileContent($chunks, $tmpDir, $newFileame)
  247. {
  248. $out = fopen($this->tempFilePath, 'wb');
  249. if (flock($out, LOCK_EX)) {
  250. for ($index = 0; $index < $chunks; $index++) {
  251. $partPath = "{$tmpDir}/{$newFileame}.{$index}.part";
  252. if (! $in = @fopen($partPath, 'rb')) {
  253. break;
  254. }
  255. while ($buff = fread($in, 4096)) {
  256. fwrite($out, $buff);
  257. }
  258. @fclose($in);
  259. @unlink($partPath);
  260. }
  261. flock($out, LOCK_UN);
  262. }
  263. fclose($out);
  264. }
  265. /**
  266. * @param mixed $id
  267. *
  268. * @return string
  269. */
  270. protected function getTempDir($id)
  271. {
  272. $tmpDir = storage_path('tmp/'.$id);
  273. if (! is_dir($tmpDir)) {
  274. app('files')->makeDirectory($tmpDir, 0755, true);
  275. }
  276. return $tmpDir;
  277. }
  278. /**
  279. * Response the error messages.
  280. *
  281. * @param $code
  282. * @param $error
  283. *
  284. * @return \Illuminate\Http\JsonResponse
  285. */
  286. protected function responseError($code, $error)
  287. {
  288. return $this->response([
  289. 'error' => ['code' => $code, 'message' => $error], 'status' => false,
  290. ]);
  291. }
  292. /**
  293. * @param array $message
  294. *
  295. * @return \Illuminate\Http\JsonResponse
  296. */
  297. protected function response(array $message)
  298. {
  299. return response()->json($message);
  300. }
  301. /**
  302. * Specify the directory and name for upload file.
  303. *
  304. * @param string $directory
  305. * @param null|string $name
  306. *
  307. * @return $this
  308. */
  309. public function move($directory, $name = null)
  310. {
  311. $this->dir($directory);
  312. $this->name($name);
  313. return $this;
  314. }
  315. /**
  316. * Specify the directory upload file.
  317. *
  318. * @param string $dir
  319. *
  320. * @return $this
  321. */
  322. public function dir($dir)
  323. {
  324. if ($dir) {
  325. $this->directory = $dir;
  326. }
  327. return $this;
  328. }
  329. /**
  330. * Set name of store name.
  331. *
  332. * @param string|callable $name
  333. *
  334. * @return $this
  335. */
  336. public function name($name)
  337. {
  338. if ($name) {
  339. $this->name = $name;
  340. }
  341. return $this;
  342. }
  343. /**
  344. * Use unique name for store upload file.
  345. *
  346. * @return $this
  347. */
  348. public function uniqueName()
  349. {
  350. $this->useUniqueName = true;
  351. return $this;
  352. }
  353. /**
  354. * Use sequence name for store upload file.
  355. *
  356. * @return $this
  357. */
  358. public function sequenceName()
  359. {
  360. $this->useSequenceName = true;
  361. return $this;
  362. }
  363. /**
  364. * Generate a unique name for uploaded file.
  365. *
  366. * @param UploadedFile $file
  367. *
  368. * @return string
  369. */
  370. protected function generateUniqueName(UploadedFile $file)
  371. {
  372. return md5(uniqid()).'.'.$file->getClientOriginalExtension();
  373. }
  374. /**
  375. * Generate a sequence name for uploaded file.
  376. *
  377. * @param UploadedFile $file
  378. *
  379. * @return string
  380. */
  381. protected function generateSequenceName(UploadedFile $file)
  382. {
  383. $index = 1;
  384. $extension = $file->getClientOriginalExtension();
  385. $originalName = $file->getClientOriginalName();
  386. $newName = $originalName.'_'.$index.'.'.$extension;
  387. while ($this->getStorage()->exists("{$this->getDirectory()}/$newName")) {
  388. $index++;
  389. $newName = $originalName.'_'.$index.'.'.$extension;
  390. }
  391. return $newName;
  392. }
  393. /**
  394. * @param UploadedFile $file
  395. *
  396. * @return bool|\Illuminate\Support\MessageBag
  397. */
  398. protected function getErrorMessages(UploadedFile $file)
  399. {
  400. $rules = $attributes = [];
  401. if (! $fieldRules = $this->getRules()) {
  402. return false;
  403. }
  404. $rules[$this->column] = $fieldRules;
  405. $attributes[$this->column] = $this->label;
  406. /* @var \Illuminate\Validation\Validator $validator */
  407. $validator = Validator::make([$this->column => $file], $rules, $this->validationMessages, $attributes);
  408. if (! $validator->passes()) {
  409. $errors = $validator->errors()->getMessages()[$this->column];
  410. return implode('; ', $errors);
  411. }
  412. }
  413. /**
  414. * Destroy original files.
  415. *
  416. * @return void.
  417. */
  418. public function destroy()
  419. {
  420. if ($this->retainable) {
  421. return;
  422. }
  423. $this->deleteFile($this->original);
  424. }
  425. /**
  426. * Destroy original files.
  427. *
  428. * @param $file
  429. */
  430. public function destroyIfChanged($file)
  431. {
  432. if (! $file || ! $this->original) {
  433. return $this->destroy();
  434. }
  435. $file = array_filter((array) $file);
  436. $original = (array) $this->original;
  437. $this->deleteFile(Arr::except(array_combine($original, $original), $file));
  438. }
  439. /**
  440. * Destroy files.
  441. *
  442. * @param string|array $path
  443. */
  444. public function deleteFile($paths)
  445. {
  446. if (! $paths) {
  447. return;
  448. }
  449. $storage = $this->getStorage();
  450. foreach ((array) $paths as $path) {
  451. if ($storage->exists($path)) {
  452. $storage->delete($path);
  453. } else {
  454. $prefix = $storage->url('');
  455. $path = str_replace($prefix, '', $path);
  456. if ($storage->exists($path)) {
  457. $storage->delete($path);
  458. }
  459. }
  460. }
  461. }
  462. /**
  463. * Get storage instance.
  464. *
  465. * @return \Illuminate\Filesystem\Filesystem|null
  466. */
  467. public function getStorage()
  468. {
  469. if ($this->storage === null) {
  470. $this->initStorage();
  471. }
  472. return $this->storage;
  473. }
  474. /**
  475. * Set disk for storage.
  476. *
  477. * @param string $disk Disks defined in `config/filesystems.php`.
  478. *
  479. * @throws \Exception
  480. *
  481. * @return $this
  482. */
  483. public function disk($disk)
  484. {
  485. try {
  486. $this->storage = Storage::disk($disk);
  487. } catch (\Exception $exception) {
  488. if (! array_key_exists($disk, config('filesystems.disks'))) {
  489. admin_error(
  490. 'Config error.',
  491. "Disk [$disk] not configured, please add a disk config in `config/filesystems.php`."
  492. );
  493. return $this;
  494. }
  495. throw $exception;
  496. }
  497. return $this;
  498. }
  499. /**
  500. * Get file visit url.
  501. *
  502. * @param string $path
  503. *
  504. * @return string
  505. */
  506. public function objectUrl($path)
  507. {
  508. if (URL::isValidUrl($path)) {
  509. return $path;
  510. }
  511. return $this->getStorage()->url($path);
  512. }
  513. /**
  514. * @param $permission
  515. *
  516. * @return $this
  517. */
  518. public function storagePermission($permission)
  519. {
  520. $this->storagePermission = $permission;
  521. return $this;
  522. }
  523. }