FileLib.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. <?php
  2. namespace App\Models;
  3. use Illuminate\Support\Facades\Storage;
  4. use Illuminate\Http\UploadedFile;
  5. use DOMDocument;
  6. use Exception;
  7. class FileLib
  8. {
  9. /*
  10. * Storage 저장 경로 생성
  11. */
  12. public function setPath(string $uploadType, string|null $addPath = null): string
  13. {
  14. $uploadPath = $this->getPublicPath($uploadType, $addPath);
  15. $storagePath = $this->getStoragePath($uploadType, $addPath);
  16. // 해당 경로가 없다면 생성 해주고 index.php 파일 생성
  17. if (is_dir($storagePath) === false) {
  18. mkdir($storagePath, 0707, true);
  19. touch($storagePath . '/index.php');
  20. chmod($storagePath . '/index.php', 0644);
  21. }
  22. return $uploadPath;
  23. }
  24. /*
  25. * Public 저장 경로 조회
  26. */
  27. public function getPublicPath(string $uploadType, string|null $addPath = null): string
  28. {
  29. return (join(DIRECTORY_SEPARATOR, [
  30. UPLOAD_PATH_PUBLIC,
  31. $uploadType,
  32. date('Y'),
  33. date('m'),
  34. date('d'),
  35. $addPath
  36. ]) . DIRECTORY_SEPARATOR);
  37. }
  38. /*
  39. * Storage 저장 경로 조회
  40. */
  41. public function getStoragePath(string $uploadType, string|null $addPath = null): string
  42. {
  43. return storage_path(UPLOAD_PATH_APP . DIRECTORY_SEPARATOR . $this->getPublicPath($uploadType, $addPath));
  44. }
  45. /*
  46. * 경로 완전 삭제
  47. */
  48. public function removePath(string $uploadType, string|null $addPath = null): void
  49. {
  50. $storagePath = $this->getStoragePath($uploadType, $addPath);
  51. if(is_dir($storagePath)) {
  52. foreach (scandir($storagePath) as $file) {
  53. if (in_array($file, ['.', '..'])) {
  54. continue;
  55. }
  56. $path = ($storagePath . DIRECTORY_SEPARATOR . $file);
  57. if(is_dir($path)) {
  58. $this->removePath($uploadType, $addPath . DIRECTORY_SEPARATOR . $file);
  59. }else{
  60. unlink($path);
  61. }
  62. }
  63. rmdir($storagePath);
  64. }
  65. }
  66. /*
  67. * 본문 내용중 외부 이미지 주소를 서버로 가져온 후에 내부 주소로 변경합니다.
  68. */
  69. public function saveAsImage(string|null $contents, string $uploadType, string|null $addPath = null): string|null
  70. {
  71. return $this->saveImageFromBlob(
  72. $this->saveImageFromUrl($contents, $uploadType, $addPath), $uploadType, $addPath);
  73. }
  74. /*
  75. * CURL를 이용한 외부 이미지 다운로드
  76. */
  77. public function saveImageFromUrl(string|null $contents, string $uploadType, string|null $addPath = null): string|null
  78. {
  79. $dom = new DOMDocument('1.0', 'UTF-8');
  80. libxml_use_internal_errors(true);
  81. $dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>' . $contents);
  82. libxml_clear_errors();
  83. $images = [];
  84. foreach ($dom->getElementsByTagName('img') as $img)
  85. {
  86. $src = $img->attributes->getNamedItem("src")->value;
  87. $path = canonicalizePath($src);
  88. if(file_exists(public_path($path))) {
  89. $images[] = $path;
  90. continue;
  91. }
  92. if(filter_var($src, FILTER_VALIDATE_URL)) {
  93. $url = parse_url($src);
  94. if (!empty($url['host']) && $url['host'] !== request()->getHttpHost())
  95. {
  96. $ch = curl_init($src);
  97. curl_setopt($ch, CURLOPT_HEADER, 0);
  98. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  99. $err = curl_error($ch);
  100. $rawData = [];
  101. if (empty($err)) {
  102. $rawData = curl_exec($ch);
  103. }
  104. curl_close($ch);
  105. if ($rawData)
  106. {
  107. // 파일 경로 설정
  108. $uploadPath = $this->setPath($uploadType, $addPath);
  109. $ext = $this->getExtension($src);
  110. [$unix, $sec] = explode(' ', microtime());
  111. $fileName = (md5(uniqid(mt_rand())) . '_' . str_replace('.', '', $sec . $unix) . '.' . $ext);
  112. $savePath = Storage::path($uploadPath . $fileName);
  113. $saveURL = Storage::url($uploadPath . $fileName);
  114. $fp = fopen($savePath, 'w+');
  115. fwrite($fp, $rawData);
  116. fclose($fp);
  117. if (file_exists($savePath)) {
  118. $images[] = $saveURL;
  119. $contents = str_replace($src, $saveURL, $contents);
  120. }
  121. }
  122. }
  123. }
  124. }
  125. // 기존 이미지 삭제
  126. $this->clear($images, $uploadType, $addPath);
  127. unset($dom, $images, $uploadPath, $addPath);
  128. return $contents;
  129. }
  130. /*
  131. * blob 종류의 이미지 저장
  132. */
  133. public function saveImageFromBlob(string|null $contents, string $uploadType, string|null $addPath = null): string|null
  134. {
  135. $dom = new DOMDocument('1.0', 'UTF-8');
  136. libxml_use_internal_errors(true);
  137. $dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>' . $contents);
  138. libxml_clear_errors();
  139. $images = [];
  140. foreach ($dom->getElementsByTagName('img') as $img)
  141. {
  142. $src = $img->attributes->getNamedItem("src")?->value;
  143. $alt = $img->attributes->getNamedItem("alt")?->value;
  144. $target = $img->attributes->getNamedItem("data-target")?->value;
  145. $path = canonicalizePath($src);
  146. if(file_exists(public_path($path)) || !$target || !strpos($src, 'base64')) {
  147. $images[] = $path;
  148. continue;
  149. }
  150. try {
  151. $mime = mime_content_type($src);
  152. $ext = $this->getMimeExtension($mime);
  153. if (!$ext) {
  154. $ext = $this->getExtension($alt);
  155. }
  156. } catch (Exception) {
  157. return null;
  158. }
  159. // 파일 경로 설정
  160. $uploadPath = $this->setPath($uploadType, $addPath);
  161. $link = str_replace('data:' . $mime . ';base64,', '', $src);
  162. $link = str_replace(' ', '+', $link);
  163. $isBase64 = isBase64($link);
  164. $link = base64_decode($link);
  165. [$unix, $sec] = explode(' ', microtime());
  166. $fileName = (md5(uniqid(mt_rand())) . '_' . str_replace('.', '', $sec . $unix) . '.' . $ext);
  167. $savePath = Storage::path($uploadPath . $fileName);
  168. $saveUrl = Storage::url($uploadPath . $fileName);
  169. // 파일저장
  170. if ($isBase64) {
  171. file_put_contents($savePath, $link, FILE_APPEND | LOCK_EX);
  172. }
  173. if (file_exists($savePath)) {
  174. $images[] = $saveUrl;
  175. $contents = str_replace($src, $saveUrl, $contents);
  176. }
  177. }
  178. // 기존 이미지 삭제
  179. $this->clear($images, $uploadType, $addPath);
  180. unset($dom, $images, $uploadPath, $addPath);
  181. return $contents;
  182. }
  183. public function isBase64($s): bool
  184. {
  185. // Check if there are valid base64 characters
  186. if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $s)) {
  187. return false;
  188. }
  189. // Decode the string in strict mode and check the results
  190. $decoded = base64_decode($s, true);
  191. if (false === $decoded) {
  192. return false;
  193. }
  194. // if string returned contains not printable chars
  195. if (0 < preg_match('/((?![[:graph:]])(?!\s)(?!\p{L}))./', $decoded, $matched)) {
  196. return false;
  197. }
  198. // Encode the string again
  199. if (base64_encode($decoded) != $s) {
  200. return false;
  201. }
  202. return true;
  203. }
  204. public function isBase64Encoded(string $s): bool
  205. {
  206. if ((bool)preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $s) === false) {
  207. return false;
  208. }
  209. $decoded = base64_decode($s, true);
  210. if ($decoded === false) {
  211. return false;
  212. }
  213. $encoding = mb_detect_encoding($decoded);
  214. if (!in_array($encoding, ['UTF-8', 'ASCII'], true)) {
  215. return false;
  216. }
  217. return ($decoded !== false && base64_encode($decoded) === $s);
  218. }
  219. /*
  220. * 파일 확장자 조회
  221. */
  222. public function getExtension(string $path): string
  223. {
  224. return pathinfo($path, PATHINFO_EXTENSION) ?? 'unknown';
  225. }
  226. /*
  227. * 이미지 파일 여부
  228. */
  229. public function isImage(string $path): bool
  230. {
  231. if(!file_exists($path)) {
  232. return false;
  233. }
  234. $mime = mime_content_type($path);
  235. if($mime) {
  236. return in_array($mime, [
  237. image_type_to_mime_type(IMAGETYPE_GIF),
  238. image_type_to_mime_type(IMAGETYPE_JPEG),
  239. image_type_to_mime_type(IMAGETYPE_PNG),
  240. image_type_to_mime_type(IMAGETYPE_BMP)]
  241. );
  242. }
  243. return false;
  244. }
  245. /*
  246. * 첨부파일 정보 조회
  247. */
  248. public function upload(UploadedFile $file, string $uploadType, string|null $addPath = null): object
  249. {
  250. $publicPath = $this->getPublicPath($uploadType, $addPath); // 파일 경로 조회
  251. $filePath = $file->store($publicPath); // 파일 저장
  252. $fullPath = Storage::path($filePath);
  253. $fullURL = Storage::url($filePath);
  254. $imageInfo = $this->getImageInfo($fullPath);
  255. return (object)[
  256. 'name' => $file->hashName(),
  257. 'path' => $fullPath,
  258. 'url' => $fullURL,
  259. 'originName' => $file->getClientOriginalName(),
  260. 'extension' => $file->getClientOriginalExtension(),
  261. 'size' => $file->getSize(),
  262. 'isImage' => $imageInfo->answer,
  263. 'imageWidth' => $imageInfo->width,
  264. 'imageHeight' => $imageInfo->height,
  265. 'imageType' => $imageInfo->type
  266. ];
  267. }
  268. /*
  269. * 이미지 크기 조정
  270. */
  271. public function resize(string $filePath, int $width, int $height): void
  272. {
  273. [$w, $h, $ext] = getimagesize($filePath);
  274. $ratio = max($width / $w, $height / $h);
  275. $h = ceil($height / $ratio);
  276. $x = ($w - $width / $ratio) / 2;
  277. $w = ceil($width / $ratio);
  278. $imgString = file_get_contents($filePath);
  279. $image = imagecreatefromstring($imgString);
  280. $tmp = imagecreatetruecolor($width, $height);
  281. imagecopyresampled($tmp, $image, 0, 0, $x, 0, $width, $height, $w, $h);
  282. switch ($ext) {
  283. case IMAGETYPE_JPEG:
  284. imagejpeg($tmp, $filePath, 100);
  285. break;
  286. case IMAGETYPE_PNG:
  287. imagepng($tmp, $filePath, 0);
  288. break;
  289. case IMAGETYPE_GIF:
  290. imagegif($tmp, $filePath);
  291. break;
  292. default:
  293. break;
  294. }
  295. imagedestroy($image);
  296. imagedestroy($tmp);
  297. }
  298. /*
  299. * mime_content_type 값의 확장자 반환
  300. */
  301. public function getMimeExtension(string $imageType): string
  302. {
  303. return ([
  304. 'image/bmp' => 'bmp',
  305. 'image/cis-cod' => 'cod',
  306. 'image/gif' => 'gif',
  307. 'image/ief' => 'ief',
  308. 'image/jpeg' => 'jpeg',
  309. 'image/pipeg' => 'jfif',
  310. 'image/tiff' => 'tif',
  311. 'image/x-cmu-raster' => 'ras',
  312. 'image/x-cmx' => 'cmx',
  313. 'image/x-icon' => 'ico',
  314. 'image/x-portable-anymap' => 'pnm',
  315. 'image/x-portable-bitmap' => 'pbm',
  316. 'image/x-portable-graymap' => 'pgm',
  317. 'image/x-portable-pixmap' => 'ppm',
  318. 'image/x-rgb' => 'rgb',
  319. 'image/x-xbitmap' => 'xbm',
  320. 'image/x-xpixmap' => 'xpm',
  321. 'image/x-xwindowdump' => 'xwd',
  322. 'image/png' => 'png',
  323. 'image/x-jps' => 'jps',
  324. 'image/x-freehand' => 'fh'
  325. ][$imageType] ?? '');
  326. }
  327. /*
  328. * Image type 반환
  329. */
  330. public function getImageType(int $imageType): string
  331. {
  332. return ([
  333. IMAGETYPE_UNKNOWN => 'UNKNOWN',
  334. IMAGETYPE_GIF => 'GIF',
  335. IMAGETYPE_JPEG => 'JPEG',
  336. IMAGETYPE_PNG => 'PNG',
  337. IMAGETYPE_SWF => 'SWF',
  338. IMAGETYPE_PSD => 'PSD',
  339. IMAGETYPE_BMP => 'BMP',
  340. IMAGETYPE_TIFF_II => 'TIFF_II',
  341. IMAGETYPE_TIFF_MM => 'TIFF_MM',
  342. IMAGETYPE_JPC => 'JPC',
  343. IMAGETYPE_JP2 => 'JP2',
  344. IMAGETYPE_JPX => 'JPX',
  345. IMAGETYPE_JB2 => 'JB2',
  346. IMAGETYPE_SWC => 'SWC',
  347. IMAGETYPE_IFF => 'IFF',
  348. IMAGETYPE_WBMP => 'WBMP',
  349. IMAGETYPE_XBM => 'XBM',
  350. IMAGETYPE_ICO => 'ICO',
  351. IMAGETYPE_WEBP => 'WEBP',
  352. IMAGETYPE_AVIF => 'AVIF',
  353. IMAGETYPE_COUNT => 'COUNT'
  354. ][$imageType] ?? '');
  355. }
  356. /*
  357. * Image Info 반환
  358. */
  359. public function getImageInfo(string $path): object
  360. {
  361. $ret = (object)[
  362. 'answer' => $this->isImage($path),
  363. 'name' => basename($path),
  364. 'size' => filesize($path),
  365. 'type' => null,
  366. 'width' => 0,
  367. 'height' => 0,
  368. 'ext' => null
  369. ];
  370. if(!file_exists($path)) {
  371. return $ret;
  372. }
  373. if($ret->answer) {
  374. [$ret->width, $ret->height, $ret->ext] = getimagesize($path);
  375. $ret->type = $this->getImageType($ret->ext);
  376. }
  377. return $ret;
  378. }
  379. /*
  380. * 파일 삭제
  381. */
  382. public function remove(string $path): bool
  383. {
  384. if (file_exists($path)) {
  385. return unlink($path);
  386. }
  387. $path = public_path($path);
  388. if (file_exists($path)) {
  389. return unlink($path);
  390. }
  391. return false;
  392. }
  393. /*
  394. * 본문 내용에 없는 이미지를 삭제한다.
  395. */
  396. public function clear(array $images, string $uploadType, string|null $addPath = null): void
  397. {
  398. $images = array_map(function ($src) {
  399. return last(explode(DIRECTORY_SEPARATOR, $src));
  400. }, $images);
  401. $storagePath = $this->getStoragePath($uploadType, $addPath);
  402. if (is_dir($storagePath)) {
  403. foreach (scandir($storagePath) as $file) {
  404. if (in_array($file, ['.', '..', 'index.php'])) {
  405. continue;
  406. }
  407. if (!in_array($file, $images)) { // 에디터에 없는 이미지면 삭제
  408. $filePath = ($storagePath . $file);
  409. if (is_dir($filePath)) {
  410. continue;
  411. }
  412. unlink($filePath);
  413. }
  414. }
  415. // 이미지가 없으면 디렉토리 삭제
  416. if (count($images) <= 0) {
  417. if(unlink($storagePath . 'index.php')) {
  418. rmdir($storagePath);
  419. }
  420. }
  421. }
  422. unset($images, $uploadType, $addPath, $storagePath);
  423. }
  424. /*
  425. * 이미지 정보 반환
  426. */
  427. public function getImageFromContent(?string $content): array
  428. {
  429. $dom = new DOMDocument('1.0', 'UTF-8');
  430. libxml_use_internal_errors(true);
  431. $dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>' . $content);
  432. libxml_clear_errors();
  433. $images = [];
  434. foreach($dom->getElementsByTagName('img') as $img) {
  435. $src = canonicalizePath($img->attributes->getNamedItem("src")?->value);
  436. $origin = $img->attributes->getNamedItem("alt")?->value;
  437. $path = public_path($src);
  438. if(!file_exists($path)) {
  439. continue;
  440. }
  441. $imageInfo = $this->getImageInfo($path);
  442. $images[] = (object)[
  443. 'origin' => $origin,
  444. 'name' => $imageInfo->name,
  445. 'path' => $path,
  446. 'url' => $src,
  447. 'size' => $imageInfo->size,
  448. 'type' => $imageInfo->type,
  449. 'width' => $imageInfo->width,
  450. 'height' => $imageInfo->height
  451. ];
  452. }
  453. return $images;
  454. }
  455. }