|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace ZhenMu\Support\Utils; |
| 4 | + |
| 5 | +use Illuminate\Support\Facades\File; |
| 6 | +use PhpZip\ZipFile; |
| 7 | + |
| 8 | +class Zip |
| 9 | +{ |
| 10 | + protected $zipFile; |
| 11 | + |
| 12 | + public function __construct() |
| 13 | + { |
| 14 | + $this->zipFile = new ZipFile(); |
| 15 | + } |
| 16 | + |
| 17 | + public function pack(string $sourcePath, ?string $filename = null, ?string $targetPath = null) |
| 18 | + { |
| 19 | + if (!File::exists($sourcePath)) { |
| 20 | + throw new \RuntimeException("待解压目录不存在 {$sourcePath}"); |
| 21 | + } |
| 22 | + |
| 23 | + $filename = $filename ?? File::name($sourcePath); |
| 24 | + $targetPath = $targetPath ?? File::dirname($sourcePath); |
| 25 | + $targetPath = $targetPath ?: File::dirname($sourcePath); |
| 26 | + |
| 27 | + File::ensureDirectoryExists($targetPath); |
| 28 | + |
| 29 | + $zipFilename = str_contains($filename, '.zip') ? $filename : $filename . '.zip'; |
| 30 | + $zipFilepath = "{$targetPath}/{$zipFilename}"; |
| 31 | + |
| 32 | + while (File::exists($zipFilepath)) { |
| 33 | + $basename = File::name($zipFilepath); |
| 34 | + $zipCount = count(File::glob("{$targetPath}/{$basename}*.zip")); |
| 35 | + |
| 36 | + $zipFilename = $basename . ($zipCount) . '.zip'; |
| 37 | + $zipFilepath = "{$targetPath}/{$zipFilename}"; |
| 38 | + } |
| 39 | + |
| 40 | + // 压缩 |
| 41 | + $this->zipFile->addDirRecursive($sourcePath, $filename); |
| 42 | + $this->zipFile->saveAsFile($zipFilepath); |
| 43 | + |
| 44 | + return $targetPath; |
| 45 | + } |
| 46 | + |
| 47 | + public function unpack(string $sourcePath, ?string $dirname = null) |
| 48 | + { |
| 49 | + try { |
| 50 | + // 检测文件类型,只有 zip 文件才进行解压操作 |
| 51 | + $mimeType = File::mimeType($sourcePath); |
| 52 | + } catch (\Throwable $e) { |
| 53 | + \info("解压失败 {$e->getMessage()}"); |
| 54 | + throw new \RuntimeException("解压失败 {$e->getMessage()}"); |
| 55 | + } |
| 56 | + |
| 57 | + // 获取文件类型(只处理目录和 zip 文件) |
| 58 | + $type = match (true) { |
| 59 | + default => null, |
| 60 | + str_contains($mimeType, 'directory') => 1, |
| 61 | + str_contains($mimeType, 'zip') => 2, |
| 62 | + }; |
| 63 | + |
| 64 | + if (is_null($type)) { |
| 65 | + \info("unsupport mime type $mimeType"); |
| 66 | + throw new \RuntimeException("unsupport mime type $mimeType"); |
| 67 | + } |
| 68 | + |
| 69 | + // 确保解压目标目录存在 |
| 70 | + $targetPath = $targetPath ?? storage_path('app/extensions/.tmp'); |
| 71 | + if (empty($targetPath)) { |
| 72 | + throw new \RuntimeException("targetPath cannot be empty."); |
| 73 | + } |
| 74 | + |
| 75 | + if (!is_dir($targetPath)) { |
| 76 | + if (!File::ensureDirectoryExists($targetPath)) { |
| 77 | + throw new \RuntimeException("mkdir {$targetPath} failed."); |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + // 清空目录,避免留下其他插件的文件 |
| 82 | + File::cleanDirectory($targetPath); |
| 83 | + |
| 84 | + // 目录无需解压操作,将原目录拷贝到临时目录中 |
| 85 | + if ($type == 1) { |
| 86 | + File::copyDirectory($sourcePath, $targetPath); |
| 87 | + |
| 88 | + // 确保目录解压层级是插件目录顶层 |
| 89 | + $this->ensureDoesntHaveSubdir($targetPath); |
| 90 | + |
| 91 | + return $targetPath; |
| 92 | + } |
| 93 | + |
| 94 | + if ($type == 2) { |
| 95 | + // 解压 |
| 96 | + $zipFile = $this->zipFile->openFile($sourcePath); |
| 97 | + $zipFile->extractTo($targetPath); |
| 98 | + |
| 99 | + // 确保目录解压层级是插件目录顶层 |
| 100 | + $this->ensureDoesntHaveSubdir($targetPath); |
| 101 | + |
| 102 | + // 解压到指定目录 |
| 103 | + return $targetPath; |
| 104 | + } |
| 105 | + |
| 106 | + return null; |
| 107 | + } |
| 108 | + |
| 109 | + public function ensureDoesntHaveSubdir(string $sourcePath) |
| 110 | + { |
| 111 | + $targetPath = $sourcePath ?? storage_path('app/extensions/.tmp'); |
| 112 | + |
| 113 | + $pattern = sprintf("%s/*", rtrim($sourcePath, DIRECTORY_SEPARATOR)); |
| 114 | + $files = File::glob($pattern); |
| 115 | + |
| 116 | + if (count($files) > 1) { |
| 117 | + return false; |
| 118 | + } |
| 119 | + |
| 120 | + $tmpDir = $targetPath . '-subdir'; |
| 121 | + if (File::ensureDirectoryExists($tmpDir)) { |
| 122 | + throw new \RuntimeException("mkdir {$tmpDir} failed."); |
| 123 | + } |
| 124 | + |
| 125 | + $firstEntryname = File::name(current($files)); |
| 126 | + |
| 127 | + File::copyDirectory($targetPath . "/{$firstEntryname}", $tmpDir); |
| 128 | + File::cleanDirectory($targetPath); |
| 129 | + File::copyDirectory($tmpDir, $targetPath); |
| 130 | + File::deleteDirectory($tmpDir); |
| 131 | + |
| 132 | + return $targetPath; |
| 133 | + } |
| 134 | +} |
0 commit comments