config = Config::get('upload'); if ($config) { $this->config = array_merge($this->config, $config); } if ($file) { $this->setFile($file); } } /** * 设置文件 * @param ?UploadedFile $file * @return array 文件信息 * @throws Throwable */ public function setFile(?UploadedFile $file): array { if (empty($file)) { throw new Exception(__('No files were uploaded'), 10001); } $suffix = strtolower($file->extension()); $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file'; $fileInfo['suffix'] = $suffix; $fileInfo['type'] = $file->getOriginalMime(); $fileInfo['size'] = $file->getSize(); $fileInfo['name'] = $file->getOriginalName(); $fileInfo['sha1'] = $file->sha1(); $this->file = $file; $this->fileInfo = $fileInfo; return $fileInfo; } /** * 设置细目(存储目录) */ public function setTopic(string $topic): Upload { $this->topic = $topic; return $this; } /** * 检查文件类型是否允许上传 * @return bool * @throws Throwable */ protected function checkMimetype(): bool { $mimetypeArr = explode(',', strtolower($this->config['mimetype'])); $typeArr = explode('/', $this->fileInfo['type']); // 验证文件后缀 if ($this->config['mimetype'] === '*' || in_array($this->fileInfo['suffix'], $mimetypeArr) || in_array('.' . $this->fileInfo['suffix'], $mimetypeArr) || in_array($this->fileInfo['type'], $mimetypeArr) || in_array($typeArr[0] . "/*", $mimetypeArr)) { return true; } throw new Exception(__('The uploaded file format is not allowed'), 10002); } /** * 是否是图片并设置好相关属性 * @return bool * @throws Throwable */ protected function checkIsImage(): bool { 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'])) { $imgInfo = getimagesize($this->file->getPathname()); if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) { throw new Exception(__('The uploaded image file is not a valid image')); } $this->fileInfo['width'] = $imgInfo[0]; $this->fileInfo['height'] = $imgInfo[1]; $this->isImage = true; return true; } return false; } /** * 上传的文件是否为图片 * @return bool */ public function isImage(): bool { return $this->isImage; } /** * 检查文件大小是否允许上传 * @throws Throwable */ protected function checkSize(): void { $size = Filesystem::fileUnitToByte($this->config['maxsize']); if ($this->fileInfo['size'] > $size) { throw new Exception(__('The uploaded file is too large (%sMiB), Maximum file size:%sMiB', [ round($this->fileInfo['size'] / pow(1024, 2), 2), round($size / pow(1024, 2), 2) ])); } } /** * 获取文件后缀 * @return string */ public function getSuffix(): string { return $this->fileInfo['suffix'] ?: 'file'; } /** * 获取文件保存名 * @param ?string $saveName * @param ?string $filename * @param ?string $sha1 * @return string */ public function getSaveName(?string $saveName = null, ?string $filename = null, ?string $sha1 = null): string { if ($filename) { $suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file'; } else { $suffix = $this->fileInfo['suffix']; } $filename = $filename ?: ($suffix ? substr($this->fileInfo['name'], 0, strripos($this->fileInfo['name'], '.')) : $this->fileInfo['name']); $sha1 = $sha1 ?: $this->fileInfo['sha1']; $replaceArr = [ '{topic}' => $this->topic, '{year}' => date("Y"), '{mon}' => date("m"), '{day}' => date("d"), '{hour}' => date("H"), '{min}' => date("i"), '{sec}' => date("s"), '{random}' => Random::build(), '{random32}' => Random::build('alnum', 32), '{filename}' => $this->getFileNameSubstr($filename), '{suffix}' => $suffix, '{.suffix}' => $suffix ? '.' . $suffix : '', '{filesha1}' => $sha1, ]; $saveName = $saveName ?: $this->config['savename']; return str_replace(array_keys($replaceArr), array_values($replaceArr), $saveName); } /** * 上传文件 * @param ?string $saveName * @param int $adminId * @param int $userId * @return array * @throws Throwable */ public function upload(?string $saveName = null, int $adminId = 0, int $userId = 0): array { if (empty($this->file)) { throw new Exception(__('No files have been uploaded or the file size exceeds the upload limit of the server')); } $this->checkSize(); $this->checkMimetype(); $this->checkIsImage(); $params = [ 'topic' => $this->topic, 'admin_id' => $adminId, 'user_id' => $userId, 'url' => $this->getSaveName(), 'width' => $this->fileInfo['width'] ?? 0, 'height' => $this->fileInfo['height'] ?? 0, 'name' => substr(htmlspecialchars(strip_tags($this->fileInfo['name'])), 0, 100), 'size' => $this->fileInfo['size'], 'mimetype' => $this->fileInfo['type'], 'storage' => 'local', 'sha1' => $this->fileInfo['sha1'] ]; // 附件数据入库 - 不依赖模型新增前事件,确保入库前文件已经移动完成 $attachment = Attachment::where('sha1', $params['sha1']) ->where('topic', $params['topic']) ->where('storage', $params['storage']) ->find(); $filePath = Filesystem::fsFit(public_path() . ltrim($params['url'], '/')); if ($attachment && file_exists($filePath)) { $attachment->quote++; $attachment->last_upload_time = time(); } else { $this->move($saveName); $attachment = new Attachment(); $attachment->data(array_filter($params)); } $attachment->save(); return $attachment->toArray(); } public function move($saveName = null): File { $saveName = $saveName ?: $this->getSaveName(); $saveName = '/' . ltrim($saveName, '/'); $uploadDir = substr($saveName, 0, strripos($saveName, '/') + 1); $fileName = substr($saveName, strripos($saveName, '/') + 1); $destDir = Filesystem::fsFit(root_path() . 'public' . $uploadDir); if (request()->isCgi()) { return $this->file->move($destDir, $fileName); } set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); if (!is_dir($destDir) && !mkdir($destDir, 0777, true)) { restore_error_handler(); throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $destDir, strip_tags($error))); } $destination = $destDir . $fileName; if (!rename($this->file->getPathname(), $destination)) { restore_error_handler(); throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->file->getPathname(), $destination, strip_tags($error))); } restore_error_handler(); @chmod($destination, 0666 & ~umask()); return $this->file; } /** * 获取文件名称字符串的子串 */ public function getFileNameSubstr(string $fileName, int $length = 15): string { // 对 $fileName 中不利于传输的字符串进行过滤 $pattern = "/[\s:@#?&\/=',+]+/u"; $fileName = preg_replace($pattern, '', $fileName); return mb_substr($fileName, 0, $length); } }