初始化-安装依赖
This commit is contained in:
51
server/plugin/saipackage/app/controller/IndexController.php
Normal file
51
server/plugin/saipackage/app/controller/IndexController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\saipackage\app\controller;
|
||||
|
||||
use plugin\saiadmin\basic\OpenController;
|
||||
use Saithink\Saipackage\service\Terminal;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Workerman\Protocols\Http\ServerSentEvents;
|
||||
|
||||
class IndexController extends OpenController
|
||||
{
|
||||
/**
|
||||
* 执行终端
|
||||
* @param Request $request
|
||||
* @return void
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function terminal(Request $request): void
|
||||
{
|
||||
// SSE 消息
|
||||
$connection = $request->connection;
|
||||
$connection->send(new Response(200, [
|
||||
'Content-Type' => 'text/event-stream',
|
||||
'Cache-Control' => 'no-cache',
|
||||
'Connection' => 'keep-alive',
|
||||
'X-Accel-Buffering' => 'no',
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Expose-Headers' => 'Content-Type',
|
||||
], "\r\n"));
|
||||
|
||||
// 消息开始
|
||||
$connection->send(new ServerSentEvents([
|
||||
'event' => 'message', 'data' => 'start'
|
||||
]));
|
||||
|
||||
// 生成器
|
||||
$generator = (new Terminal())->exec();
|
||||
foreach ($generator as $chunk) {
|
||||
$connection->send(new ServerSentEvents([
|
||||
'event' => 'message', 'data' => $chunk
|
||||
]));
|
||||
}
|
||||
|
||||
// 关闭链接
|
||||
$connection->close();
|
||||
}
|
||||
|
||||
}
|
||||
389
server/plugin/saipackage/app/controller/InstallController.php
Normal file
389
server/plugin/saipackage/app/controller/InstallController.php
Normal file
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\saipackage\app\controller;
|
||||
|
||||
use plugin\saiadmin\app\cache\UserMenuCache;
|
||||
use plugin\saiadmin\app\middleware\SystemLog;
|
||||
use plugin\saiadmin\app\middleware\CheckLogin;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saipackage\app\logic\InstallLogic;
|
||||
use Saithink\Saipackage\service\Server;
|
||||
use Saithink\Saipackage\service\Version;
|
||||
use support\annotation\Middleware;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
#[Middleware(CheckLogin::class, SystemLog::class)]
|
||||
class InstallController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 构造
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
if ($this->adminId > 1) {
|
||||
throw new ApiException('仅超级管理员能够操作');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 环境检查状态
|
||||
*/
|
||||
static string $ok = 'ok';
|
||||
static string $fail = 'fail';
|
||||
static string $warn = 'warn';
|
||||
|
||||
static array $needDependentVersion = [
|
||||
'php' => '8.1.0',
|
||||
'saiadmin' => '6.0.0',
|
||||
'saipackage' => '6.0.0',
|
||||
];
|
||||
|
||||
/**
|
||||
* 应用列表
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$data = Server::installedList(runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR);
|
||||
|
||||
$phpVersion = phpversion();
|
||||
$phpVersionCompare = Version::compare(self::$needDependentVersion['php'], $phpVersion);
|
||||
$phpVersionNotes = '正常';
|
||||
if (!$phpVersionCompare) {
|
||||
$phpVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['php'];
|
||||
}
|
||||
|
||||
$saiadminVersion = config('plugin.saiadmin.app.version');
|
||||
$saiadminVersionCompare = Version::compare(self::$needDependentVersion['saiadmin'], $saiadminVersion);
|
||||
$saiadminVersionNotes = '正常';
|
||||
if (!$saiadminVersionCompare) {
|
||||
$saiadminVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['saiadmin'];
|
||||
}
|
||||
|
||||
$saithinkVersion = config('plugin.saipackage.app.version');
|
||||
$saithinkVersionCompare = Version::compare(self::$needDependentVersion['saipackage'], $saithinkVersion);
|
||||
$saithinkVersionNotes = '正常';
|
||||
if (!$saithinkVersionCompare) {
|
||||
$saithinkVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['saipackage'];
|
||||
}
|
||||
|
||||
|
||||
return $this->success([
|
||||
'version' => [
|
||||
'php_version' => [
|
||||
'describe' => $phpVersion,
|
||||
'state' => $phpVersionCompare ? self::$ok : self::$fail,
|
||||
'notes' => $phpVersionNotes,
|
||||
],
|
||||
'saiadmin_version' => [
|
||||
'describe' => $saiadminVersion,
|
||||
'state' => $saiadminVersionCompare ? self::$ok : self::$fail,
|
||||
'notes' => $saiadminVersionNotes,
|
||||
],
|
||||
'saipackage_version' => [
|
||||
'describe' => $saithinkVersion,
|
||||
'state' => $saithinkVersionCompare ? self::$ok : self::$fail,
|
||||
'notes' => $saithinkVersionNotes,
|
||||
],
|
||||
],
|
||||
'data' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传插件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function upload(Request $request): Response
|
||||
{
|
||||
$spl_file = current($request->file());
|
||||
if (!$spl_file->isValid()) {
|
||||
return $this->fail('上传文件校验失败');
|
||||
}
|
||||
$config = config('plugin.saipackage.upload', [
|
||||
'size' => 1024 * 1024 * 5,
|
||||
'type' => ['zip']
|
||||
]);
|
||||
if (!in_array($spl_file->getUploadExtension(), $config['type'])) {
|
||||
return $this->fail('文件格式上传失败,请选择zip格式文件上传');
|
||||
}
|
||||
if ($spl_file->getSize() > $config['size']) {
|
||||
return $this->fail('文件大小不能超过5M');
|
||||
}
|
||||
$install = new InstallLogic();
|
||||
$info = $install->upload($spl_file);
|
||||
return $this->success($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function install(Request $request): Response
|
||||
{
|
||||
$appName = $request->post("appName", '');
|
||||
if (empty($appName)) {
|
||||
return $this->fail('参数错误');
|
||||
}
|
||||
$install = new InstallLogic($appName);
|
||||
$info = $install->install();
|
||||
UserMenuCache::clearMenuCache();
|
||||
return $this->success($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function uninstall(Request $request): Response
|
||||
{
|
||||
$appName = $request->post("appName", '');
|
||||
if (empty($appName)) {
|
||||
return $this->fail('参数错误');
|
||||
}
|
||||
$install = new InstallLogic($appName);
|
||||
$install->uninstall();
|
||||
UserMenuCache::clearMenuCache();
|
||||
return $this->success('卸载插件成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function reload(Request $request): Response
|
||||
{
|
||||
Server::restart();
|
||||
|
||||
return $this->success('重载成功');
|
||||
}
|
||||
|
||||
// ========== 商店代理接口 ==========
|
||||
|
||||
/**
|
||||
* 代理请求封装
|
||||
*/
|
||||
protected function proxyRequest(string $url, string $method = 'GET', ?string $token = null, ?array $postData = null, int $timeout = 10): array
|
||||
{
|
||||
$headers = [];
|
||||
if ($token) {
|
||||
$headers[] = "Authorization: Bearer {$token}";
|
||||
}
|
||||
if ($postData !== null) {
|
||||
$headers[] = "Content-Type: application/json";
|
||||
}
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'header' => implode("\r\n", $headers),
|
||||
'content' => $postData ? json_encode($postData) : null,
|
||||
'timeout' => $timeout,
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
return ['success' => false, 'message' => '请求失败'];
|
||||
}
|
||||
|
||||
// 尝试解析 JSON
|
||||
$data = json_decode($response, true);
|
||||
if ($data && isset($data['code'])) {
|
||||
if ($data['code'] === 200) {
|
||||
return ['success' => true, 'data' => $data['data'] ?? null];
|
||||
}
|
||||
return ['success' => false, 'message' => $data['message'] ?? '请求失败'];
|
||||
}
|
||||
|
||||
// 非 JSON 响应(可能是文件)
|
||||
return ['success' => true, 'raw' => $response, 'headers' => $http_response_header ?? []];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用商店列表
|
||||
*/
|
||||
public function appList(Request $request): Response
|
||||
{
|
||||
$params = http_build_query([
|
||||
'page' => $request->input('page', 1),
|
||||
'limit' => $request->input('limit', 16),
|
||||
'price' => $request->input('price', 'all'),
|
||||
'type' => $request->input('type', ''),
|
||||
'keywords' => $request->input('keywords', ''),
|
||||
]);
|
||||
|
||||
$result = $this->proxyRequest("https://saas.saithink.top/api/app/appstore/store/appList?{$params}");
|
||||
|
||||
return $result['success']
|
||||
? $this->success($result['data'])
|
||||
: $this->fail($result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商店验证码
|
||||
*/
|
||||
public function storeCaptcha(): Response
|
||||
{
|
||||
$result = $this->proxyRequest("https://saas.saithink.top/api/app/appstore/index/captcha");
|
||||
|
||||
return $result['success']
|
||||
? $this->success($result['data'])
|
||||
: $this->fail($result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 商店登录
|
||||
*/
|
||||
public function storeLogin(Request $request): Response
|
||||
{
|
||||
$result = $this->proxyRequest(
|
||||
"https://saas.saithink.top/api/app/appstore/index/login",
|
||||
'POST',
|
||||
null,
|
||||
[
|
||||
'username' => $request->input('username'),
|
||||
'password' => $request->input('password'),
|
||||
'code' => $request->input('code'),
|
||||
'uuid' => $request->input('uuid'),
|
||||
]
|
||||
);
|
||||
|
||||
return $result['success']
|
||||
? $this->success($result['data'])
|
||||
: $this->fail($result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商店用户信息
|
||||
*/
|
||||
public function storeUserInfo(Request $request): Response
|
||||
{
|
||||
$token = $request->input('token');
|
||||
if (empty($token)) {
|
||||
return $this->fail('未登录');
|
||||
}
|
||||
|
||||
$result = $this->proxyRequest(
|
||||
"https://saas.saithink.top/api/app/appstore/user/info",
|
||||
'GET',
|
||||
$token
|
||||
);
|
||||
|
||||
return $result['success']
|
||||
? $this->success($result['data'])
|
||||
: $this->fail($result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已购应用列表
|
||||
*/
|
||||
public function storePurchasedApps(Request $request): Response
|
||||
{
|
||||
$token = $request->input('token');
|
||||
if (empty($token)) {
|
||||
return $this->fail('未登录');
|
||||
}
|
||||
|
||||
$result = $this->proxyRequest(
|
||||
"https://saas.saithink.top/api/app/appstore/user/appList",
|
||||
'GET',
|
||||
$token
|
||||
);
|
||||
|
||||
return $result['success']
|
||||
? $this->success($result['data'])
|
||||
: $this->fail($result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用版本列表
|
||||
*/
|
||||
public function storeAppVersions(Request $request): Response
|
||||
{
|
||||
$token = $request->input('token');
|
||||
$appId = $request->input('app_id');
|
||||
|
||||
if (empty($token)) {
|
||||
return $this->fail('未登录');
|
||||
}
|
||||
|
||||
$result = $this->proxyRequest(
|
||||
"https://saas.saithink.top/api/app/appstore/user/versionList?app_id={$appId}",
|
||||
'GET',
|
||||
$token
|
||||
);
|
||||
|
||||
return $result['success']
|
||||
? $this->success($result['data'])
|
||||
: $this->fail($result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载应用 - 下载并调用 InstallLogic 处理
|
||||
*/
|
||||
public function storeDownloadApp(Request $request): Response
|
||||
{
|
||||
$token = $request->input('token');
|
||||
$versionId = $request->input('id');
|
||||
|
||||
if (empty($token)) {
|
||||
return $this->fail('未登录');
|
||||
}
|
||||
|
||||
if (empty($versionId)) {
|
||||
return $this->fail('版本ID不能为空');
|
||||
}
|
||||
|
||||
$result = $this->proxyRequest(
|
||||
"https://saas.saithink.top/api/app/appstore/user/downloadApp",
|
||||
'POST',
|
||||
$token,
|
||||
['id' => (int) $versionId],
|
||||
60
|
||||
);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $this->fail($result['message'] ?? '下载失败');
|
||||
}
|
||||
|
||||
if (!isset($result['raw'])) {
|
||||
return $this->fail('下载失败');
|
||||
}
|
||||
|
||||
// 保存临时 zip 文件
|
||||
$tempZip = runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR . 'downloadTemp' . date('YmdHis') . '.zip';
|
||||
if (!is_dir(dirname($tempZip))) {
|
||||
mkdir(dirname($tempZip), 0755, true);
|
||||
}
|
||||
file_put_contents($tempZip, $result['raw']);
|
||||
|
||||
try {
|
||||
// 调用 InstallLogic 处理
|
||||
$install = new InstallLogic();
|
||||
$info = $install->uploadFromPath($tempZip);
|
||||
|
||||
return $this->success($info, '下载成功,请在插件列表中安装');
|
||||
} catch (Throwable $e) {
|
||||
@unlink($tempZip);
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
6
server/plugin/saipackage/app/functions.php
Normal file
6
server/plugin/saipackage/app/functions.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
|
||||
|
||||
532
server/plugin/saipackage/app/logic/InstallLogic.php
Normal file
532
server/plugin/saipackage/app/logic/InstallLogic.php
Normal file
@@ -0,0 +1,532 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\saipackage\app\logic;
|
||||
|
||||
use Throwable;
|
||||
use Saithink\Saipackage\service\Server;
|
||||
use Saithink\Saipackage\service\Version;
|
||||
use Saithink\Saipackage\service\Filesystem;
|
||||
use Saithink\Saipackage\service\Depends;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\app\cache\UserMenuCache;
|
||||
|
||||
class InstallLogic
|
||||
{
|
||||
public const UNINSTALLED = 0;
|
||||
public const INSTALLED = 1;
|
||||
public const WAIT_INSTALL = 2;
|
||||
public const CONFLICT_PENDING = 3;
|
||||
public const DEPENDENT_WAIT_INSTALL = 4;
|
||||
public const DIRECTORY_OCCUPIED = 5;
|
||||
|
||||
/**
|
||||
* @var string 安装目录
|
||||
*/
|
||||
protected string $installDir;
|
||||
|
||||
/**
|
||||
* @var string 备份目录
|
||||
*/
|
||||
protected string $backupsDir;
|
||||
|
||||
/**
|
||||
* @var string 插件名称
|
||||
*/
|
||||
protected string $appName;
|
||||
|
||||
/**
|
||||
* @var string 插件根目录
|
||||
*/
|
||||
protected string $appDir;
|
||||
|
||||
public function __construct(string $appName = '')
|
||||
{
|
||||
$this->installDir = runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR;
|
||||
$this->backupsDir = $this->installDir . 'backups' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($this->installDir)) {
|
||||
mkdir($this->installDir, 0755, true);
|
||||
}
|
||||
if (!is_dir($this->backupsDir)) {
|
||||
mkdir($this->backupsDir, 0755, true);
|
||||
}
|
||||
|
||||
if ($appName) {
|
||||
$this->appName = $appName;
|
||||
$this->appDir = $this->installDir . $appName . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
}
|
||||
|
||||
public function getInstallState()
|
||||
{
|
||||
if (!is_dir($this->appDir)) {
|
||||
return self::UNINSTALLED;
|
||||
}
|
||||
$info = $this->getInfo();
|
||||
if ($info && isset($info['state'])) {
|
||||
return $info['state'];
|
||||
}
|
||||
|
||||
// 目录已存在,但非正常的模块
|
||||
return Filesystem::dirIsEmpty($this->appDir) ? self::UNINSTALLED : self::DIRECTORY_OCCUPIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取允许覆盖的目录
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAllowedPath(): array
|
||||
{
|
||||
$backend = 'plugin' . DIRECTORY_SEPARATOR . $this->appName;
|
||||
$frontend = env('FRONTEND_DIR', 'saiadmin-artd') . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'plugin' . DIRECTORY_SEPARATOR . $this->appName;
|
||||
return [
|
||||
$this->appDir . $backend => base_path() . DIRECTORY_SEPARATOR . $backend,
|
||||
$this->appDir . $frontend => dirname(base_path()) . DIRECTORY_SEPARATOR . $frontend
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传安装
|
||||
* @param mixed $file
|
||||
* @return array 模块的基本信息
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function upload(mixed $file): array
|
||||
{
|
||||
$copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip';
|
||||
$file->move($copyTo);
|
||||
|
||||
// 解压
|
||||
$copyToDir = Filesystem::unzip($copyTo);
|
||||
$copyToDir .= DIRECTORY_SEPARATOR;
|
||||
|
||||
// 删除zip
|
||||
@unlink($file);
|
||||
@unlink($copyTo);
|
||||
|
||||
// 读取ini
|
||||
$info = Server::getIni($copyToDir);
|
||||
if (empty($info['app'])) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
// 基本配置不完整
|
||||
throw new ApiException('插件的基础配置信息错误');
|
||||
}
|
||||
|
||||
|
||||
$this->appName = $info['app'];
|
||||
$this->appDir = $this->installDir . $info['app'] . DIRECTORY_SEPARATOR;
|
||||
|
||||
$upgrade = false;
|
||||
if (is_dir($this->appDir)) {
|
||||
$oldInfo = $this->getInfo();
|
||||
if ($oldInfo && !empty($oldInfo['app'])) {
|
||||
$versions = explode('.', $oldInfo['version']);
|
||||
if (isset($versions[2])) {
|
||||
$versions[2]++;
|
||||
}
|
||||
$nextVersion = implode('.', $versions);
|
||||
$upgrade = Version::compare($nextVersion, $info['version']);
|
||||
if (!$upgrade) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw new ApiException('插件已经存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (Filesystem::dirIsEmpty($this->appDir) || (!Filesystem::dirIsEmpty($this->appDir) && !$upgrade)) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
// 模块目录被占
|
||||
throw new ApiException('该插件的安装目录已经被占用');
|
||||
}
|
||||
}
|
||||
|
||||
$newInfo = ['state' => self::WAIT_INSTALL];
|
||||
if ($upgrade) {
|
||||
$newInfo['update'] = 1;
|
||||
|
||||
// 清理旧版本代码
|
||||
Filesystem::delDir($this->appDir);
|
||||
}
|
||||
|
||||
// 放置新模块
|
||||
rename($copyToDir, $this->appDir);
|
||||
|
||||
// 检查新包是否完整
|
||||
$this->checkPackage();
|
||||
|
||||
// 设置为待安装状态
|
||||
$this->setInfo($newInfo);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地 zip 文件路径安装(用于在线下载后安装)
|
||||
* @param string $zipPath zip 文件完整路径
|
||||
* @return array 模块的基本信息
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function uploadFromPath(string $zipPath): array
|
||||
{
|
||||
if (!is_file($zipPath)) {
|
||||
throw new ApiException('文件不存在');
|
||||
}
|
||||
|
||||
// 解压
|
||||
$copyToDir = Filesystem::unzip($zipPath);
|
||||
$copyToDir .= DIRECTORY_SEPARATOR;
|
||||
|
||||
// 删除 zip
|
||||
@unlink($zipPath);
|
||||
|
||||
// 读取 ini
|
||||
$info = Server::getIni($copyToDir);
|
||||
if (empty($info['app'])) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw new ApiException('插件的基础配置信息错误');
|
||||
}
|
||||
|
||||
$this->appName = $info['app'];
|
||||
$this->appDir = $this->installDir . $info['app'] . DIRECTORY_SEPARATOR;
|
||||
|
||||
$upgrade = false;
|
||||
if (is_dir($this->appDir)) {
|
||||
$oldInfo = $this->getInfo();
|
||||
if ($oldInfo && !empty($oldInfo['app'])) {
|
||||
$versions = explode('.', $oldInfo['version']);
|
||||
if (isset($versions[2])) {
|
||||
$versions[2]++;
|
||||
}
|
||||
$nextVersion = implode('.', $versions);
|
||||
$upgrade = Version::compare($nextVersion, $info['version']);
|
||||
if (!$upgrade) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw new ApiException('插件已经存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (Filesystem::dirIsEmpty($this->appDir) || (!Filesystem::dirIsEmpty($this->appDir) && !$upgrade)) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw new ApiException('该插件的安装目录已经被占用');
|
||||
}
|
||||
}
|
||||
|
||||
$newInfo = ['state' => self::WAIT_INSTALL];
|
||||
if ($upgrade) {
|
||||
$newInfo['update'] = 1;
|
||||
Filesystem::delDir($this->appDir);
|
||||
}
|
||||
|
||||
// 放置新模块
|
||||
rename($copyToDir, $this->appDir);
|
||||
|
||||
// 检查新包是否完整
|
||||
$this->checkPackage();
|
||||
|
||||
// 设置为待安装状态
|
||||
$this->setInfo($newInfo);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安装或更新
|
||||
* @return array
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function install(): array
|
||||
{
|
||||
$state = $this->getInstallState();
|
||||
if ($state == self::INSTALLED || $state == self::DIRECTORY_OCCUPIED) {
|
||||
throw new ApiException('插件已经存在');
|
||||
}
|
||||
|
||||
if ($state == self::DEPENDENT_WAIT_INSTALL) {
|
||||
throw new ApiException('等待依赖安装');
|
||||
}
|
||||
|
||||
echo '开始安装[' . $this->appName . ']' . PHP_EOL;
|
||||
|
||||
$info = $this->getInfo();
|
||||
|
||||
if ($state == self::WAIT_INSTALL) {
|
||||
echo '安装数据库' . PHP_EOL;
|
||||
$sql = $this->appDir . 'install.sql';
|
||||
Server::importSql($sql);
|
||||
}
|
||||
|
||||
if (isset($info['update']) && $info['update'] == 1) {
|
||||
echo '更新数据库' . PHP_EOL;
|
||||
$sql = $this->appDir . 'update.sql';
|
||||
Server::importSql($sql);
|
||||
|
||||
unset($info['update']);
|
||||
$this->setInfo([], $info);
|
||||
}
|
||||
|
||||
// 依赖检查
|
||||
$this->dependConflictHandle();
|
||||
|
||||
// 执行安装脚本
|
||||
echo '安装文件' . PHP_EOL;
|
||||
$pathRelation = $this->getAllowedPath();
|
||||
Server::installByRelation($pathRelation);
|
||||
|
||||
// 依赖更新
|
||||
echo '依赖更新' . PHP_EOL;
|
||||
$this->dependUpdateHandle();
|
||||
|
||||
// 清理菜单缓存
|
||||
UserMenuCache::clearMenuCache();
|
||||
|
||||
// 重启后端
|
||||
Server::restart();
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function uninstall(): void
|
||||
{
|
||||
$state = $this->getInstallState();
|
||||
if ($state != self::INSTALLED) {
|
||||
echo PHP_EOL . '删除插件[' . $this->appName . ']' . PHP_EOL;
|
||||
$pathRelation = $this->getAllowedPath();
|
||||
foreach ($pathRelation as $key => $value) {
|
||||
if (is_dir($value)) {
|
||||
Filesystem::delDir($value);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除临时目录
|
||||
Filesystem::delDir($this->appDir);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
echo '开始卸载[' . $this->appName . ']' . PHP_EOL;
|
||||
|
||||
echo '卸载数据库' . PHP_EOL;
|
||||
$sql = $this->appDir . 'uninstall.sql';
|
||||
Server::importSql($sql);
|
||||
|
||||
echo '备份文件' . PHP_EOL;
|
||||
$backFiles = [];
|
||||
$pathRelation = $this->getAllowedPath();
|
||||
$index = 1;
|
||||
foreach ($pathRelation as $key => $value) {
|
||||
if (is_dir($value)) {
|
||||
$backFiles[$this->appName . '-' . $index] = $value;
|
||||
$index++;
|
||||
}
|
||||
}
|
||||
$backupsZip = $this->backupsDir . $this->appName . '-uninstall-' . date('YmdHis') . '.zip';
|
||||
Filesystem::zipDir($backFiles, $backupsZip);
|
||||
|
||||
echo '卸载文件' . PHP_EOL;
|
||||
$pathRelation = $this->getAllowedPath();
|
||||
foreach ($pathRelation as $key => $value) {
|
||||
if (is_dir($value)) {
|
||||
Filesystem::delDir($value);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除临时目录
|
||||
Filesystem::delDir($this->appDir);
|
||||
|
||||
// 清理菜单缓存
|
||||
UserMenuCache::clearMenuCache();
|
||||
|
||||
// 重启后端
|
||||
Server::restart();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查包是否完整
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function checkPackage(): bool
|
||||
{
|
||||
if (!is_dir($this->appDir)) {
|
||||
throw new ApiException('插件目录不存在');
|
||||
}
|
||||
$info = $this->getInfo();
|
||||
$infoKeys = ['app', 'title', 'about', 'author', 'version', 'state'];
|
||||
foreach ($infoKeys as $value) {
|
||||
if (!array_key_exists($value, $info)) {
|
||||
Filesystem::delDir($this->appDir);
|
||||
throw new ApiException('该插件的基础配置信息不完善');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依赖安装完成标记
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function dependentInstallComplete(string $type): void
|
||||
{
|
||||
$info = $this->getInfo();
|
||||
if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) {
|
||||
if ($type == 'npm') {
|
||||
unset($info['npm_dependent_wait_install']);
|
||||
}
|
||||
if ($type == 'composer') {
|
||||
unset($info['composer_dependent_wait_install']);
|
||||
}
|
||||
if ($type == 'all') {
|
||||
unset($info['npm_dependent_wait_install'], $info['composer_dependent_wait_install']);
|
||||
}
|
||||
if (!isset($info['npm_dependent_wait_install']) && !isset($info['composer_dependent_wait_install'])) {
|
||||
$info['state'] = self::INSTALLED;
|
||||
}
|
||||
$this->setInfo([], $info);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 依赖冲突检查
|
||||
* @return bool
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function dependConflictHandle(): bool
|
||||
{
|
||||
$info = $this->getInfo();
|
||||
if ($info['state'] != self::WAIT_INSTALL && $info['state'] != self::CONFLICT_PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$coverFiles = [];// 要覆盖的文件-备份
|
||||
$depends = Server::getDepend($this->appDir);
|
||||
|
||||
$serverDep = new Depends(base_path() . DIRECTORY_SEPARATOR . 'composer.json', 'composer');
|
||||
$webDep = new Depends(dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-vue') . DIRECTORY_SEPARATOR . 'package.json');
|
||||
|
||||
// 如果有依赖更新,增加要备份的文件
|
||||
if ($depends) {
|
||||
foreach ($depends as $key => $item) {
|
||||
if (!$item) {
|
||||
continue;
|
||||
}
|
||||
if ($key == 'require' || $key == 'require-dev') {
|
||||
$coverFiles[] = base_path() . DIRECTORY_SEPARATOR . 'composer.json';
|
||||
continue;
|
||||
}
|
||||
if ($key == 'dependencies' || $key == 'devDependencies') {
|
||||
$coverFiles[] = dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-vue') . DIRECTORY_SEPARATOR . 'package.json';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 备份将被覆盖的文件
|
||||
if ($coverFiles) {
|
||||
$backupsZip = $this->backupsDir . $this->appName . '-cover-' . date('YmdHis') . '.zip';
|
||||
Filesystem::zip($coverFiles, $backupsZip);
|
||||
}
|
||||
|
||||
if ($depends) {
|
||||
$npm = false;
|
||||
$composer = false;
|
||||
|
||||
// composer config 更新
|
||||
$composerConfig = Server::getConfig($this->appDir, 'composerConfig');
|
||||
if ($composerConfig) {
|
||||
$serverDep->setComposerConfig($composerConfig);
|
||||
}
|
||||
|
||||
foreach ($depends as $key => $item) {
|
||||
if (!$item) {
|
||||
continue;
|
||||
}
|
||||
if ($key == 'require') {
|
||||
$composer = true;
|
||||
$serverDep->addDepends($item, false, true);
|
||||
} elseif ($key == 'require-dev') {
|
||||
$composer = true;
|
||||
$serverDep->addDepends($item, true, true);
|
||||
} elseif ($key == 'dependencies') {
|
||||
$npm = true;
|
||||
$webDep->addDepends($item, false, true);
|
||||
} elseif ($key == 'devDependencies') {
|
||||
$npm = true;
|
||||
$webDep->addDepends($item, true, true);
|
||||
}
|
||||
}
|
||||
if ($npm) {
|
||||
$info['npm_dependent_wait_install'] = 1;
|
||||
$info['state'] = self::DEPENDENT_WAIT_INSTALL;
|
||||
}
|
||||
if ($composer) {
|
||||
$info['composer_dependent_wait_install'] = 1;
|
||||
$info['state'] = self::DEPENDENT_WAIT_INSTALL;
|
||||
}
|
||||
if ($info['state'] != self::DEPENDENT_WAIT_INSTALL) {
|
||||
// 无冲突
|
||||
$this->setInfo([
|
||||
'state' => self::INSTALLED,
|
||||
]);
|
||||
} else {
|
||||
$this->setInfo([], $info);
|
||||
}
|
||||
} else {
|
||||
// 无冲突
|
||||
$this->setInfo([
|
||||
'state' => self::INSTALLED,
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依赖升级处理
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function dependUpdateHandle(): void
|
||||
{
|
||||
$info = $this->getInfo();
|
||||
if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) {
|
||||
$waitInstall = [];
|
||||
if (isset($info['composer_dependent_wait_install'])) {
|
||||
$waitInstall[] = 'composer_dependent_wait_install';
|
||||
}
|
||||
if (isset($info['npm_dependent_wait_install'])) {
|
||||
$waitInstall[] = 'npm_dependent_wait_install';
|
||||
}
|
||||
if (empty($waitInstall)) {
|
||||
$this->setInfo([
|
||||
'state' => self::INSTALLED,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块基本信息
|
||||
*/
|
||||
public function getInfo(): array
|
||||
{
|
||||
return Server::getIni($this->appDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模块基本信息
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function setInfo(array $kv = [], array $arr = []): bool
|
||||
{
|
||||
if ($kv) {
|
||||
$info = $this->getInfo();
|
||||
foreach ($kv as $k => $v) {
|
||||
$info[$k] = $v;
|
||||
}
|
||||
return Server::setIni($this->appDir, $info);
|
||||
} elseif ($arr) {
|
||||
return Server::setIni($this->appDir, $arr);
|
||||
}
|
||||
throw new ApiException('参数错误');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user