Upload.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <?php
  2. namespace app\common\library;
  3. use Throwable;
  4. use ba\Random;
  5. use think\File;
  6. use ba\Filesystem;
  7. use think\Exception;
  8. use think\facade\Config;
  9. use think\file\UploadedFile;
  10. use app\common\model\Attachment;
  11. use think\exception\FileException;
  12. /**
  13. * 上传
  14. */
  15. class Upload
  16. {
  17. /**
  18. * 配置信息
  19. * @var array
  20. */
  21. protected array $config = [];
  22. /**
  23. * @var ?UploadedFile
  24. */
  25. protected ?UploadedFile $file = null;
  26. /**
  27. * 是否是图片
  28. * @var bool
  29. */
  30. protected bool $isImage = false;
  31. /**
  32. * 文件信息
  33. * @var array
  34. */
  35. protected array $fileInfo;
  36. /**
  37. * 细目(存储目录)
  38. * @var string
  39. */
  40. protected string $topic = 'default';
  41. /**
  42. * 构造方法
  43. * @param ?UploadedFile $file 上传的文件
  44. * @param array $config 配置
  45. * @throws Throwable
  46. */
  47. public function __construct(?UploadedFile $file = null, array $config = [])
  48. {
  49. $this->config = Config::get('upload');
  50. if ($config) {
  51. $this->config = array_merge($this->config, $config);
  52. }
  53. if ($file) {
  54. $this->setFile($file);
  55. }
  56. }
  57. /**
  58. * 设置文件
  59. * @param ?UploadedFile $file
  60. * @return array 文件信息
  61. * @throws Throwable
  62. */
  63. public function setFile(?UploadedFile $file): array
  64. {
  65. if (empty($file)) {
  66. throw new Exception(__('No files were uploaded'), 10001);
  67. }
  68. $suffix = strtolower($file->extension());
  69. $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
  70. $fileInfo['suffix'] = $suffix;
  71. $fileInfo['type'] = $file->getOriginalMime();
  72. $fileInfo['size'] = $file->getSize();
  73. $fileInfo['name'] = $file->getOriginalName();
  74. $fileInfo['sha1'] = $file->sha1();
  75. $this->file = $file;
  76. $this->fileInfo = $fileInfo;
  77. return $fileInfo;
  78. }
  79. /**
  80. * 设置细目(存储目录)
  81. */
  82. public function setTopic(string $topic): Upload
  83. {
  84. $this->topic = $topic;
  85. return $this;
  86. }
  87. /**
  88. * 检查文件类型是否允许上传
  89. * @return bool
  90. * @throws Throwable
  91. */
  92. protected function checkMimetype(): bool
  93. {
  94. $mimetypeArr = explode(',', strtolower($this->config['mimetype']));
  95. $typeArr = explode('/', $this->fileInfo['type']);
  96. // 验证文件后缀
  97. if ($this->config['mimetype'] === '*'
  98. || in_array($this->fileInfo['suffix'], $mimetypeArr) || in_array('.' . $this->fileInfo['suffix'], $mimetypeArr)
  99. || in_array($this->fileInfo['type'], $mimetypeArr) || in_array($typeArr[0] . "/*", $mimetypeArr)) {
  100. return true;
  101. }
  102. throw new Exception(__('The uploaded file format is not allowed'), 10002);
  103. }
  104. /**
  105. * 是否是图片并设置好相关属性
  106. * @return bool
  107. * @throws Throwable
  108. */
  109. protected function checkIsImage(): bool
  110. {
  111. if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
  112. $imgInfo = getimagesize($this->file->getPathname());
  113. if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
  114. throw new Exception(__('The uploaded image file is not a valid image'));
  115. }
  116. $this->fileInfo['width'] = $imgInfo[0];
  117. $this->fileInfo['height'] = $imgInfo[1];
  118. $this->isImage = true;
  119. return true;
  120. }
  121. return false;
  122. }
  123. /**
  124. * 上传的文件是否为图片
  125. * @return bool
  126. */
  127. public function isImage(): bool
  128. {
  129. return $this->isImage;
  130. }
  131. /**
  132. * 检查文件大小是否允许上传
  133. * @throws Throwable
  134. */
  135. protected function checkSize(): void
  136. {
  137. $size = Filesystem::fileUnitToByte($this->config['maxsize']);
  138. if ($this->fileInfo['size'] > $size) {
  139. throw new Exception(__('The uploaded file is too large (%sMiB), Maximum file size:%sMiB', [
  140. round($this->fileInfo['size'] / pow(1024, 2), 2),
  141. round($size / pow(1024, 2), 2)
  142. ]));
  143. }
  144. }
  145. /**
  146. * 获取文件后缀
  147. * @return string
  148. */
  149. public function getSuffix(): string
  150. {
  151. return $this->fileInfo['suffix'] ?: 'file';
  152. }
  153. /**
  154. * 获取文件保存名
  155. * @param ?string $saveName
  156. * @param ?string $filename
  157. * @param ?string $sha1
  158. * @return string
  159. */
  160. public function getSaveName(?string $saveName = null, ?string $filename = null, ?string $sha1 = null): string
  161. {
  162. if ($filename) {
  163. $suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
  164. $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
  165. } else {
  166. $suffix = $this->fileInfo['suffix'];
  167. }
  168. $filename = $filename ?: ($suffix ? substr($this->fileInfo['name'], 0, strripos($this->fileInfo['name'], '.')) : $this->fileInfo['name']);
  169. $sha1 = $sha1 ?: $this->fileInfo['sha1'];
  170. $replaceArr = [
  171. '{topic}' => $this->topic,
  172. '{year}' => date("Y"),
  173. '{mon}' => date("m"),
  174. '{day}' => date("d"),
  175. '{hour}' => date("H"),
  176. '{min}' => date("i"),
  177. '{sec}' => date("s"),
  178. '{random}' => Random::build(),
  179. '{random32}' => Random::build('alnum', 32),
  180. '{filename}' => $this->getFileNameSubstr($filename),
  181. '{suffix}' => $suffix,
  182. '{.suffix}' => $suffix ? '.' . $suffix : '',
  183. '{filesha1}' => $sha1,
  184. ];
  185. $saveName = $saveName ?: $this->config['savename'];
  186. return str_replace(array_keys($replaceArr), array_values($replaceArr), $saveName);
  187. }
  188. /**
  189. * 上传文件
  190. * @param ?string $saveName
  191. * @param int $adminId
  192. * @param int $userId
  193. * @return array
  194. * @throws Throwable
  195. */
  196. public function upload(?string $saveName = null, int $adminId = 0, int $userId = 0): array
  197. {
  198. if (empty($this->file)) {
  199. throw new Exception(__('No files have been uploaded or the file size exceeds the upload limit of the server'));
  200. }
  201. $this->checkSize();
  202. $this->checkMimetype();
  203. $this->checkIsImage();
  204. $params = [
  205. 'topic' => $this->topic,
  206. 'admin_id' => $adminId,
  207. 'user_id' => $userId,
  208. 'url' => $this->getSaveName(),
  209. 'width' => $this->fileInfo['width'] ?? 0,
  210. 'height' => $this->fileInfo['height'] ?? 0,
  211. 'name' => substr(htmlspecialchars(strip_tags($this->fileInfo['name'])), 0, 100),
  212. 'size' => $this->fileInfo['size'],
  213. 'mimetype' => $this->fileInfo['type'],
  214. 'storage' => 'local',
  215. 'sha1' => $this->fileInfo['sha1']
  216. ];
  217. // 附件数据入库 - 不依赖模型新增前事件,确保入库前文件已经移动完成
  218. $attachment = Attachment::where('sha1', $params['sha1'])
  219. ->where('topic', $params['topic'])
  220. ->where('storage', $params['storage'])
  221. ->find();
  222. $filePath = Filesystem::fsFit(public_path() . ltrim($params['url'], '/'));
  223. if ($attachment && file_exists($filePath)) {
  224. $attachment->quote++;
  225. $attachment->last_upload_time = time();
  226. } else {
  227. $this->move($saveName);
  228. $attachment = new Attachment();
  229. $attachment->data(array_filter($params));
  230. }
  231. $attachment->save();
  232. return $attachment->toArray();
  233. }
  234. public function move($saveName = null): File
  235. {
  236. $saveName = $saveName ?: $this->getSaveName();
  237. $saveName = '/' . ltrim($saveName, '/');
  238. $uploadDir = substr($saveName, 0, strripos($saveName, '/') + 1);
  239. $fileName = substr($saveName, strripos($saveName, '/') + 1);
  240. $destDir = Filesystem::fsFit(root_path() . 'public' . $uploadDir);
  241. if (request()->isCgi()) {
  242. return $this->file->move($destDir, $fileName);
  243. }
  244. set_error_handler(function ($type, $msg) use (&$error) {
  245. $error = $msg;
  246. });
  247. if (!is_dir($destDir) && !mkdir($destDir, 0777, true)) {
  248. restore_error_handler();
  249. throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $destDir, strip_tags($error)));
  250. }
  251. $destination = $destDir . $fileName;
  252. if (!rename($this->file->getPathname(), $destination)) {
  253. restore_error_handler();
  254. throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->file->getPathname(), $destination, strip_tags($error)));
  255. }
  256. restore_error_handler();
  257. @chmod($destination, 0666 & ~umask());
  258. return $this->file;
  259. }
  260. /**
  261. * 获取文件名称字符串的子串
  262. */
  263. public function getFileNameSubstr(string $fileName, int $length = 15): string
  264. {
  265. // 对 $fileName 中不利于传输的字符串进行过滤
  266. $pattern = "/[\s:@#?&\/=',+]+/u";
  267. $fileName = preg_replace($pattern, '', $fileName);
  268. return mb_substr($fileName, 0, $length);
  269. }
  270. }