182 lines
5.5 KiB
PHP
182 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
namespace App\Blog\Services;
|
|
|
|
use App\Blog\Support\PluginManager;
|
|
use App\Blog\Support\ThemeManager;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Str;
|
|
use ZipArchive;
|
|
|
|
class PackageInstaller
|
|
{
|
|
/**
|
|
* 从 ZIP 安装插件或主题。
|
|
*
|
|
* @return array{type: string, key: string}
|
|
*/
|
|
public function installZip(string $zipPath): array
|
|
{
|
|
$zip = new ZipArchive;
|
|
|
|
if ($zip->open($zipPath) !== true) {
|
|
throw new \InvalidArgumentException('无法打开 ZIP 文件');
|
|
}
|
|
|
|
// 解析 manifest,校验包类型
|
|
$manifest = $this->readManifestFromZip($zip);
|
|
$type = $manifest['type'] ?? 'plugin';
|
|
$name = $manifest['name'] ?? null;
|
|
|
|
if (! in_array($type, ['plugin', 'theme'], true)) {
|
|
throw new \InvalidArgumentException('未知的包类型:'.$type);
|
|
}
|
|
|
|
if ($type === 'plugin') {
|
|
[$vendor, $pluginName] = array_pad(explode('.', (string) $name), 2, $name);
|
|
$target = config('plugins.path').'/'.$vendor.'.'.$pluginName;
|
|
} else {
|
|
$target = config('themes.path').'/'.$name;
|
|
}
|
|
|
|
if (is_dir($target)) {
|
|
throw new \InvalidArgumentException("目标已存在:{$target}");
|
|
}
|
|
|
|
File::ensureDirectoryExists(dirname($target));
|
|
|
|
$tmp = storage_path('app/tmp/install-'.Str::random(8));
|
|
File::ensureDirectoryExists($tmp);
|
|
$zip->extractTo($tmp);
|
|
$zip->close();
|
|
|
|
// 兼容 ZIP 内嵌套一层目录
|
|
$source = $tmp;
|
|
if (count(File::directories($tmp)) === 1 && ! File::exists($tmp.'/plugin.json') && ! File::exists($tmp.'/theme.json')) {
|
|
$source = File::directories($tmp)[0];
|
|
}
|
|
|
|
$manifestFile = $type === 'plugin' ? 'plugin.json' : 'theme.json';
|
|
if (! File::exists($source.'/'.$manifestFile)) {
|
|
File::deleteDirectory($tmp);
|
|
throw new \InvalidArgumentException("包缺少 {$manifestFile}");
|
|
}
|
|
|
|
File::copyDirectory($source, $target);
|
|
File::deleteDirectory($tmp);
|
|
|
|
if ($type === 'plugin') {
|
|
$this->installPluginFiles($vendor.'.'.$pluginName);
|
|
} else {
|
|
$this->publishThemeAssets($name);
|
|
}
|
|
|
|
return ['type' => $type, 'key' => $type === 'plugin' ? $vendor.'.'.$pluginName : $name];
|
|
}
|
|
|
|
/**
|
|
* 从远程市场安装(带 checksum 校验)。
|
|
*/
|
|
public function installFromMarket(array $item): array
|
|
{
|
|
if (empty($item['download_url'])) {
|
|
throw new \InvalidArgumentException('缺少下载地址');
|
|
}
|
|
|
|
$client = app(MarketplaceClient::class);
|
|
$zipPath = $client->download($item['download_url']);
|
|
|
|
try {
|
|
return $this->installZip($zipPath);
|
|
} finally {
|
|
File::delete($zipPath);
|
|
}
|
|
}
|
|
|
|
private function readManifestFromZip(ZipArchive $zip): array
|
|
{
|
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
|
$entry = $zip->getNameIndex($i);
|
|
$base = basename($entry);
|
|
|
|
if ($base === 'plugin.json' || $base === 'theme.json') {
|
|
$content = $zip->getFromIndex($i);
|
|
$manifest = json_decode($content, true);
|
|
|
|
if (! $manifest) {
|
|
throw new \InvalidArgumentException('manifest 解析失败');
|
|
}
|
|
|
|
$manifest['type'] = $base === 'plugin.json' ? 'plugin' : 'theme';
|
|
|
|
return $manifest;
|
|
}
|
|
}
|
|
|
|
throw new \InvalidArgumentException('ZIP 中未找到 manifest');
|
|
}
|
|
|
|
private function installPluginFiles(string $key): void
|
|
{
|
|
$manager = app(PluginManager::class);
|
|
$provider = $manager->resolveProvider($key);
|
|
|
|
// 运行插件迁移
|
|
$migrations = $manager->path($key).'/database/migrations';
|
|
if (is_dir($migrations) && $provider && class_exists($provider)) {
|
|
Artisan::call('migrate', ['--path' => 'plugins/'.str_replace('.', '/', $key).'/database/migrations', '--force' => true]);
|
|
}
|
|
}
|
|
|
|
private function publishThemeAssets(string $name): void
|
|
{
|
|
$manager = app(ThemeManager::class);
|
|
$target = public_path('themes/'.$name);
|
|
|
|
if (is_dir($manager->path($name).'/assets')) {
|
|
File::copyDirectory($manager->path($name).'/assets', $target);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 卸载插件(删除目录 + 记录)。
|
|
*/
|
|
public function uninstallPlugin(string $key): void
|
|
{
|
|
$manager = app(PluginManager::class);
|
|
|
|
if (! $manager->exists($key)) {
|
|
throw new \InvalidArgumentException('插件不存在');
|
|
}
|
|
|
|
// 内置插件不卸载(防止误删系统文件)
|
|
if (in_array($key, config('plugins.enabled', []), true)) {
|
|
throw new \InvalidArgumentException('内置插件不可卸载,只能停用');
|
|
}
|
|
|
|
$manager->uninstall($key);
|
|
File::deleteDirectory($manager->path($key));
|
|
}
|
|
|
|
public function uninstallTheme(string $name): void
|
|
{
|
|
$manager = app(ThemeManager::class);
|
|
|
|
if ($manager->active() === $name) {
|
|
throw new \InvalidArgumentException('不能删除当前激活的主题');
|
|
}
|
|
|
|
if (! $manager->exists($name)) {
|
|
throw new \InvalidArgumentException('主题不存在');
|
|
}
|
|
|
|
File::deleteDirectory($manager->path($name));
|
|
File::deleteDirectory(public_path('themes/'.$name));
|
|
}
|
|
}
|