InstallLogic.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <?php
  2. namespace plugin\saipackage\app\logic;
  3. use Throwable;
  4. use Saithink\Saipackage\service\Server;
  5. use Saithink\Saipackage\service\Version;
  6. use Saithink\Saipackage\service\Filesystem;
  7. use Saithink\Saipackage\service\Depends;
  8. use plugin\saiadmin\exception\ApiException;
  9. use plugin\saiadmin\app\cache\UserMenuCache;
  10. class InstallLogic
  11. {
  12. public const UNINSTALLED = 0;
  13. public const INSTALLED = 1;
  14. public const WAIT_INSTALL = 2;
  15. public const CONFLICT_PENDING = 3;
  16. public const DEPENDENT_WAIT_INSTALL = 4;
  17. public const DIRECTORY_OCCUPIED = 5;
  18. /**
  19. * @var string 安装目录
  20. */
  21. protected string $installDir;
  22. /**
  23. * @var string 备份目录
  24. */
  25. protected string $backupsDir;
  26. /**
  27. * @var string 插件名称
  28. */
  29. protected string $appName;
  30. /**
  31. * @var string 插件根目录
  32. */
  33. protected string $appDir;
  34. public function __construct(string $appName = '')
  35. {
  36. $this->installDir = runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR;
  37. $this->backupsDir = $this->installDir . 'backups' . DIRECTORY_SEPARATOR;
  38. if (!is_dir($this->installDir)) {
  39. mkdir($this->installDir, 0755, true);
  40. }
  41. if (!is_dir($this->backupsDir)) {
  42. mkdir($this->backupsDir, 0755, true);
  43. }
  44. if ($appName) {
  45. $this->appName = $appName;
  46. $this->appDir = $this->installDir . $appName . DIRECTORY_SEPARATOR;
  47. }
  48. }
  49. public function getInstallState()
  50. {
  51. if (!is_dir($this->appDir)) {
  52. return self::UNINSTALLED;
  53. }
  54. $info = $this->getInfo();
  55. if ($info && isset($info['state'])) {
  56. return $info['state'];
  57. }
  58. // 目录已存在,但非正常的模块
  59. return Filesystem::dirIsEmpty($this->appDir) ? self::UNINSTALLED : self::DIRECTORY_OCCUPIED;
  60. }
  61. /**
  62. * 获取允许覆盖的目录
  63. * @return string[]
  64. */
  65. public function getAllowedPath(): array
  66. {
  67. $backend = 'plugin' . DIRECTORY_SEPARATOR . $this->appName;
  68. $frontend = env('FRONTEND_DIR', 'saiadmin-artd') . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'plugin' . DIRECTORY_SEPARATOR . $this->appName;
  69. return [
  70. $this->appDir . $backend => base_path() . DIRECTORY_SEPARATOR . $backend,
  71. $this->appDir . $frontend => dirname(base_path()) . DIRECTORY_SEPARATOR . $frontend
  72. ];
  73. }
  74. /**
  75. * 上传安装
  76. * @param mixed $file
  77. * @return array 模块的基本信息
  78. * @throws Throwable
  79. */
  80. public function upload(mixed $file): array
  81. {
  82. $copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip';
  83. $file->move($copyTo);
  84. // 解压
  85. $copyToDir = Filesystem::unzip($copyTo);
  86. $copyToDir .= DIRECTORY_SEPARATOR;
  87. // 删除zip
  88. @unlink($file);
  89. @unlink($copyTo);
  90. // 读取ini
  91. $info = Server::getIni($copyToDir);
  92. if (empty($info['app'])) {
  93. Filesystem::delDir($copyToDir);
  94. // 基本配置不完整
  95. throw new ApiException('插件的基础配置信息错误');
  96. }
  97. $this->appName = $info['app'];
  98. $this->appDir = $this->installDir . $info['app'] . DIRECTORY_SEPARATOR;
  99. $upgrade = false;
  100. if (is_dir($this->appDir)) {
  101. $oldInfo = $this->getInfo();
  102. if ($oldInfo && !empty($oldInfo['app'])) {
  103. $versions = explode('.', $oldInfo['version']);
  104. if (isset($versions[2])) {
  105. $versions[2]++;
  106. }
  107. $nextVersion = implode('.', $versions);
  108. $upgrade = Version::compare($nextVersion, $info['version']);
  109. if (!$upgrade) {
  110. Filesystem::delDir($copyToDir);
  111. throw new ApiException('插件已经存在');
  112. }
  113. }
  114. if (Filesystem::dirIsEmpty($this->appDir) || (!Filesystem::dirIsEmpty($this->appDir) && !$upgrade)) {
  115. Filesystem::delDir($copyToDir);
  116. // 模块目录被占
  117. throw new ApiException('该插件的安装目录已经被占用');
  118. }
  119. }
  120. $newInfo = ['state' => self::WAIT_INSTALL];
  121. if ($upgrade) {
  122. $newInfo['update'] = 1;
  123. // 清理旧版本代码
  124. Filesystem::delDir($this->appDir);
  125. }
  126. // 放置新模块
  127. rename($copyToDir, $this->appDir);
  128. // 检查新包是否完整
  129. $this->checkPackage();
  130. // 设置为待安装状态
  131. $this->setInfo($newInfo);
  132. return $info;
  133. }
  134. /**
  135. * 从本地 zip 文件路径安装(用于在线下载后安装)
  136. * @param string $zipPath zip 文件完整路径
  137. * @return array 模块的基本信息
  138. * @throws Throwable
  139. */
  140. public function uploadFromPath(string $zipPath): array
  141. {
  142. if (!is_file($zipPath)) {
  143. throw new ApiException('文件不存在');
  144. }
  145. // 解压
  146. $copyToDir = Filesystem::unzip($zipPath);
  147. $copyToDir .= DIRECTORY_SEPARATOR;
  148. // 删除 zip
  149. @unlink($zipPath);
  150. // 读取 ini
  151. $info = Server::getIni($copyToDir);
  152. if (empty($info['app'])) {
  153. Filesystem::delDir($copyToDir);
  154. throw new ApiException('插件的基础配置信息错误');
  155. }
  156. $this->appName = $info['app'];
  157. $this->appDir = $this->installDir . $info['app'] . DIRECTORY_SEPARATOR;
  158. $upgrade = false;
  159. if (is_dir($this->appDir)) {
  160. $oldInfo = $this->getInfo();
  161. if ($oldInfo && !empty($oldInfo['app'])) {
  162. $versions = explode('.', $oldInfo['version']);
  163. if (isset($versions[2])) {
  164. $versions[2]++;
  165. }
  166. $nextVersion = implode('.', $versions);
  167. $upgrade = Version::compare($nextVersion, $info['version']);
  168. if (!$upgrade) {
  169. Filesystem::delDir($copyToDir);
  170. throw new ApiException('插件已经存在');
  171. }
  172. }
  173. if (Filesystem::dirIsEmpty($this->appDir) || (!Filesystem::dirIsEmpty($this->appDir) && !$upgrade)) {
  174. Filesystem::delDir($copyToDir);
  175. throw new ApiException('该插件的安装目录已经被占用');
  176. }
  177. }
  178. $newInfo = ['state' => self::WAIT_INSTALL];
  179. if ($upgrade) {
  180. $newInfo['update'] = 1;
  181. Filesystem::delDir($this->appDir);
  182. }
  183. // 放置新模块
  184. rename($copyToDir, $this->appDir);
  185. // 检查新包是否完整
  186. $this->checkPackage();
  187. // 设置为待安装状态
  188. $this->setInfo($newInfo);
  189. return $info;
  190. }
  191. /**
  192. * 安装或更新
  193. * @return array
  194. * @throws Throwable
  195. */
  196. public function install(): array
  197. {
  198. $state = $this->getInstallState();
  199. if ($state == self::INSTALLED || $state == self::DIRECTORY_OCCUPIED) {
  200. throw new ApiException('插件已经存在');
  201. }
  202. if ($state == self::DEPENDENT_WAIT_INSTALL) {
  203. throw new ApiException('等待依赖安装');
  204. }
  205. echo '开始安装[' . $this->appName . ']' . PHP_EOL;
  206. $info = $this->getInfo();
  207. if ($state == self::WAIT_INSTALL) {
  208. echo '安装数据库' . PHP_EOL;
  209. $sql = $this->appDir . 'install.sql';
  210. Server::importSql($sql);
  211. }
  212. if (isset($info['update']) && $info['update'] == 1) {
  213. echo '更新数据库' . PHP_EOL;
  214. $sql = $this->appDir . 'update.sql';
  215. Server::importSql($sql);
  216. unset($info['update']);
  217. $this->setInfo([], $info);
  218. }
  219. // 依赖检查
  220. $this->dependConflictHandle();
  221. // 执行安装脚本
  222. echo '安装文件' . PHP_EOL;
  223. $pathRelation = $this->getAllowedPath();
  224. Server::installByRelation($pathRelation);
  225. // 依赖更新
  226. echo '依赖更新' . PHP_EOL;
  227. $this->dependUpdateHandle();
  228. // 清理菜单缓存
  229. UserMenuCache::clearMenuCache();
  230. // 重启后端
  231. Server::restart();
  232. return $info;
  233. }
  234. /**
  235. * @return void
  236. * @throws Throwable
  237. */
  238. public function uninstall(): void
  239. {
  240. $state = $this->getInstallState();
  241. if ($state != self::INSTALLED) {
  242. echo PHP_EOL . '删除插件[' . $this->appName . ']' . PHP_EOL;
  243. $pathRelation = $this->getAllowedPath();
  244. foreach ($pathRelation as $key => $value) {
  245. if (is_dir($value)) {
  246. Filesystem::delDir($value);
  247. }
  248. }
  249. // 删除临时目录
  250. Filesystem::delDir($this->appDir);
  251. return;
  252. }
  253. echo '开始卸载[' . $this->appName . ']' . PHP_EOL;
  254. echo '卸载数据库' . PHP_EOL;
  255. $sql = $this->appDir . 'uninstall.sql';
  256. Server::importSql($sql);
  257. echo '备份文件' . PHP_EOL;
  258. $backFiles = [];
  259. $pathRelation = $this->getAllowedPath();
  260. $index = 1;
  261. foreach ($pathRelation as $key => $value) {
  262. if (is_dir($value)) {
  263. $backFiles[$this->appName . '-' . $index] = $value;
  264. $index++;
  265. }
  266. }
  267. $backupsZip = $this->backupsDir . $this->appName . '-uninstall-' . date('YmdHis') . '.zip';
  268. Filesystem::zipDir($backFiles, $backupsZip);
  269. echo '卸载文件' . PHP_EOL;
  270. $pathRelation = $this->getAllowedPath();
  271. foreach ($pathRelation as $key => $value) {
  272. if (is_dir($value)) {
  273. Filesystem::delDir($value);
  274. }
  275. }
  276. // 删除临时目录
  277. Filesystem::delDir($this->appDir);
  278. // 清理菜单缓存
  279. UserMenuCache::clearMenuCache();
  280. // 重启后端
  281. Server::restart();
  282. }
  283. /**
  284. * 检查包是否完整
  285. * @throws Throwable
  286. */
  287. public function checkPackage(): bool
  288. {
  289. if (!is_dir($this->appDir)) {
  290. throw new ApiException('插件目录不存在');
  291. }
  292. $info = $this->getInfo();
  293. $infoKeys = ['app', 'title', 'about', 'author', 'version', 'state'];
  294. foreach ($infoKeys as $value) {
  295. if (!array_key_exists($value, $info)) {
  296. Filesystem::delDir($this->appDir);
  297. throw new ApiException('该插件的基础配置信息不完善');
  298. }
  299. }
  300. return true;
  301. }
  302. /**
  303. * 依赖安装完成标记
  304. * @throws Throwable
  305. */
  306. public function dependentInstallComplete(string $type): void
  307. {
  308. $info = $this->getInfo();
  309. if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) {
  310. if ($type == 'npm') {
  311. unset($info['npm_dependent_wait_install']);
  312. }
  313. if ($type == 'composer') {
  314. unset($info['composer_dependent_wait_install']);
  315. }
  316. if ($type == 'all') {
  317. unset($info['npm_dependent_wait_install'], $info['composer_dependent_wait_install']);
  318. }
  319. if (!isset($info['npm_dependent_wait_install']) && !isset($info['composer_dependent_wait_install'])) {
  320. $info['state'] = self::INSTALLED;
  321. }
  322. $this->setInfo([], $info);
  323. }
  324. }
  325. /**
  326. * 依赖冲突检查
  327. * @return bool
  328. * @throws Throwable
  329. */
  330. public function dependConflictHandle(): bool
  331. {
  332. $info = $this->getInfo();
  333. if ($info['state'] != self::WAIT_INSTALL && $info['state'] != self::CONFLICT_PENDING) {
  334. return false;
  335. }
  336. $coverFiles = [];// 要覆盖的文件-备份
  337. $depends = Server::getDepend($this->appDir);
  338. $serverDep = new Depends(base_path() . DIRECTORY_SEPARATOR . 'composer.json', 'composer');
  339. $webDep = new Depends(dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-vue') . DIRECTORY_SEPARATOR . 'package.json');
  340. // 如果有依赖更新,增加要备份的文件
  341. if ($depends) {
  342. foreach ($depends as $key => $item) {
  343. if (!$item) {
  344. continue;
  345. }
  346. if ($key == 'require' || $key == 'require-dev') {
  347. $coverFiles[] = base_path() . DIRECTORY_SEPARATOR . 'composer.json';
  348. continue;
  349. }
  350. if ($key == 'dependencies' || $key == 'devDependencies') {
  351. $coverFiles[] = dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-vue') . DIRECTORY_SEPARATOR . 'package.json';
  352. }
  353. }
  354. }
  355. // 备份将被覆盖的文件
  356. if ($coverFiles) {
  357. $backupsZip = $this->backupsDir . $this->appName . '-cover-' . date('YmdHis') . '.zip';
  358. Filesystem::zip($coverFiles, $backupsZip);
  359. }
  360. if ($depends) {
  361. $npm = false;
  362. $composer = false;
  363. // composer config 更新
  364. $composerConfig = Server::getConfig($this->appDir, 'composerConfig');
  365. if ($composerConfig) {
  366. $serverDep->setComposerConfig($composerConfig);
  367. }
  368. foreach ($depends as $key => $item) {
  369. if (!$item) {
  370. continue;
  371. }
  372. if ($key == 'require') {
  373. $composer = true;
  374. $serverDep->addDepends($item, false, true);
  375. } elseif ($key == 'require-dev') {
  376. $composer = true;
  377. $serverDep->addDepends($item, true, true);
  378. } elseif ($key == 'dependencies') {
  379. $npm = true;
  380. $webDep->addDepends($item, false, true);
  381. } elseif ($key == 'devDependencies') {
  382. $npm = true;
  383. $webDep->addDepends($item, true, true);
  384. }
  385. }
  386. if ($npm) {
  387. $info['npm_dependent_wait_install'] = 1;
  388. $info['state'] = self::DEPENDENT_WAIT_INSTALL;
  389. }
  390. if ($composer) {
  391. $info['composer_dependent_wait_install'] = 1;
  392. $info['state'] = self::DEPENDENT_WAIT_INSTALL;
  393. }
  394. if ($info['state'] != self::DEPENDENT_WAIT_INSTALL) {
  395. // 无冲突
  396. $this->setInfo([
  397. 'state' => self::INSTALLED,
  398. ]);
  399. } else {
  400. $this->setInfo([], $info);
  401. }
  402. } else {
  403. // 无冲突
  404. $this->setInfo([
  405. 'state' => self::INSTALLED,
  406. ]);
  407. }
  408. return true;
  409. }
  410. /**
  411. * 依赖升级处理
  412. * @throws Throwable
  413. */
  414. public function dependUpdateHandle(): void
  415. {
  416. $info = $this->getInfo();
  417. if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) {
  418. $waitInstall = [];
  419. if (isset($info['composer_dependent_wait_install'])) {
  420. $waitInstall[] = 'composer_dependent_wait_install';
  421. }
  422. if (isset($info['npm_dependent_wait_install'])) {
  423. $waitInstall[] = 'npm_dependent_wait_install';
  424. }
  425. if (empty($waitInstall)) {
  426. $this->setInfo([
  427. 'state' => self::INSTALLED,
  428. ]);
  429. }
  430. }
  431. }
  432. /**
  433. * 获取模块基本信息
  434. */
  435. public function getInfo(): array
  436. {
  437. return Server::getIni($this->appDir);
  438. }
  439. /**
  440. * 设置模块基本信息
  441. * @throws Throwable
  442. */
  443. public function setInfo(array $kv = [], array $arr = []): bool
  444. {
  445. if ($kv) {
  446. $info = $this->getInfo();
  447. foreach ($kv as $k => $v) {
  448. $info[$k] = $v;
  449. }
  450. return Server::setIni($this->appDir, $info);
  451. } elseif ($arr) {
  452. return Server::setIni($this->appDir, $arr);
  453. }
  454. throw new ApiException('参数错误');
  455. }
  456. }