mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +08:00
breaking(stack): 彻底移除 stack:mode 命令(含 app/extend/base/注册)
This commit is contained in:
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\command\admin\stack;
|
|
||||||
|
|
||||||
use base\common\command\admin\stack\AdminStackModeBase;
|
|
||||||
|
|
||||||
class AdminStackMode extends AdminStackModeBase
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\service\stack;
|
|
||||||
|
|
||||||
use base\common\service\stack\StackModeServiceBase;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class StackModeService extends StackModeServiceBase
|
|
||||||
{
|
|
||||||
public function getModePlan(string $mode): array
|
|
||||||
{
|
|
||||||
$mode = trim($mode);
|
|
||||||
if ($mode === '') {
|
|
||||||
throw new RuntimeException('模式不能为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = $this->loadConfig();
|
|
||||||
$modes = $config['modes'];
|
|
||||||
if (!isset($modes[$mode])) {
|
|
||||||
throw new RuntimeException("模式不存在:{$mode}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultMode = $config['default_mode'];
|
|
||||||
$managedFiles = $config['managed_files'];
|
|
||||||
|
|
||||||
$plan = [];
|
|
||||||
$skippedFiles = [];
|
|
||||||
foreach ($managedFiles as $relativePath) {
|
|
||||||
$modeSource = $this->resolveModeFile($mode, $relativePath);
|
|
||||||
if ($modeSource !== null) {
|
|
||||||
$plan[] = [
|
|
||||||
'file' => $relativePath,
|
|
||||||
'source_mode' => $mode,
|
|
||||||
'source_path' => $modeSource,
|
|
||||||
];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultSource = $this->resolveModeFile($defaultMode, $relativePath);
|
|
||||||
if ($defaultSource !== null) {
|
|
||||||
$plan[] = [
|
|
||||||
'file' => $relativePath,
|
|
||||||
'source_mode' => $defaultMode,
|
|
||||||
'source_path' => $defaultSource,
|
|
||||||
];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// File not found in mode OR default - skip it (don't throw)
|
|
||||||
$skippedFiles[] = $relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'mode' => $mode,
|
|
||||||
'default_mode' => $defaultMode,
|
|
||||||
'managed_files' => $managedFiles,
|
|
||||||
'plan' => $plan,
|
|
||||||
'skipped_files' => $skippedFiles,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function applyMode(string $mode, string $operator = 'system'): array
|
|
||||||
{
|
|
||||||
// Use our overridden getModePlan (skips missing files instead of throwing)
|
|
||||||
$result = parent::applyMode($mode, $operator);
|
|
||||||
|
|
||||||
$appliedFiles = $result['applied_files'] ?? [];
|
|
||||||
$backupId = $result['backup_id'] ?? '';
|
|
||||||
|
|
||||||
$config = $this->loadConfig();
|
|
||||||
$managedFiles = $config['managed_files'];
|
|
||||||
|
|
||||||
$deletedFiles = [];
|
|
||||||
|
|
||||||
foreach ($managedFiles as $relativePath) {
|
|
||||||
// Skip files already handled by parent
|
|
||||||
if (in_array($relativePath, $appliedFiles)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$targetPath = $this->toRootPath($relativePath);
|
|
||||||
if (!is_file($targetPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This file was skipped by getModePlan (not in mode or default)
|
|
||||||
// Back it up before deleting
|
|
||||||
if ($backupId !== '') {
|
|
||||||
$backupDir = $this->joinPath($this->backupRoot, $backupId);
|
|
||||||
$backupFilesDir = $this->joinPath($backupDir, 'files');
|
|
||||||
$backupFilePath = $this->joinPath($backupFilesDir, $this->toSystemPath($relativePath));
|
|
||||||
|
|
||||||
$this->ensureDir(dirname($backupFilePath));
|
|
||||||
copy($targetPath, $backupFilePath);
|
|
||||||
|
|
||||||
// Update backup meta
|
|
||||||
$metaPath = $this->joinPath($backupDir, 'meta.json');
|
|
||||||
if (is_file($metaPath)) {
|
|
||||||
$meta = $this->readJsonFile($metaPath);
|
|
||||||
$meta['files'][] = [
|
|
||||||
'file' => $relativePath,
|
|
||||||
'existed' => true,
|
|
||||||
];
|
|
||||||
$this->writeJsonFile($metaPath, $meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only delete if we successfully backed up
|
|
||||||
@unlink($targetPath);
|
|
||||||
$deletedFiles[] = $relativePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$result['deleted_files'] = $deletedFiles;
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace base\common\command\admin\stack;
|
|
||||||
|
|
||||||
use app\common\console\Command;
|
|
||||||
use app\common\service\stack\StackModeService;
|
|
||||||
use RuntimeException;
|
|
||||||
use think\console\Input;
|
|
||||||
use think\console\input\Argument;
|
|
||||||
use think\console\input\Option;
|
|
||||||
use think\console\Output;
|
|
||||||
|
|
||||||
class AdminStackModeBase extends Command
|
|
||||||
{
|
|
||||||
protected function configure()
|
|
||||||
{
|
|
||||||
parent::configure();
|
|
||||||
|
|
||||||
$this->setName('admin:stack:mode')
|
|
||||||
->setDescription('管理 Stack 模式:list/use/current/rollback')
|
|
||||||
->addArgument('action', Argument::OPTIONAL, '操作:list|use|current|rollback', 'list')
|
|
||||||
->addArgument('value', Argument::OPTIONAL, 'mode 或 backup_id')
|
|
||||||
->addOption('force', 'f', Option::VALUE_NONE, '跳过确认步骤');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(Input $input, Output $output)
|
|
||||||
{
|
|
||||||
$action = strtolower((string)$input->getArgument('action'));
|
|
||||||
$value = (string)$input->getArgument('value');
|
|
||||||
$force = (bool)$input->getOption('force');
|
|
||||||
|
|
||||||
try {
|
|
||||||
/** @var StackModeService $service */
|
|
||||||
$service = app(StackModeService::class);
|
|
||||||
|
|
||||||
switch ($action) {
|
|
||||||
case 'list':
|
|
||||||
$this->handleList($service, $output);
|
|
||||||
break;
|
|
||||||
case 'current':
|
|
||||||
$this->handleCurrent($service, $output);
|
|
||||||
break;
|
|
||||||
case 'use':
|
|
||||||
$this->handleUse($service, $input, $output, $value, $force);
|
|
||||||
break;
|
|
||||||
case 'rollback':
|
|
||||||
$this->handleRollback($service, $input, $output, $value, $force);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new RuntimeException('不支持的 action:' . $action);
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$output->error($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function handleList(StackModeService $service, Output $output): void
|
|
||||||
{
|
|
||||||
$modes = $service->listModes();
|
|
||||||
if (empty($modes)) {
|
|
||||||
$output->warning('未发现可用模式');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$output->writeln('可用模式:');
|
|
||||||
foreach ($modes as $item) {
|
|
||||||
$mode = (string)$item['mode'];
|
|
||||||
$desc = (string)$item['description'];
|
|
||||||
$authorOnly = (bool)$item['author_only'] ? 'yes' : 'no';
|
|
||||||
$output->writeln("- {$mode} | author_only={$authorOnly} | {$desc}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function handleCurrent(StackModeService $service, Output $output): void
|
|
||||||
{
|
|
||||||
$state = $service->getCurrentState();
|
|
||||||
$output->writeln('当前模式:' . ((string)$state['mode'] === '' ? '未记录' : (string)$state['mode']));
|
|
||||||
$output->writeln('切换时间:' . ((string)$state['switched_at'] === '' ? '未记录' : (string)$state['switched_at']));
|
|
||||||
$output->writeln('备份ID:' . ((string)$state['backup_id'] === '' ? '未记录' : (string)$state['backup_id']));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function handleUse(
|
|
||||||
StackModeService $service,
|
|
||||||
Input $input,
|
|
||||||
Output $output,
|
|
||||||
string $mode,
|
|
||||||
bool $force
|
|
||||||
): void {
|
|
||||||
$mode = trim($mode);
|
|
||||||
if ($mode === '') {
|
|
||||||
throw new RuntimeException('请提供 mode,例如:admin:stack:mode use base-build');
|
|
||||||
}
|
|
||||||
|
|
||||||
$planData = $service->getModePlan($mode);
|
|
||||||
$output->writeln("将切换到模式:{$mode}");
|
|
||||||
foreach ($planData['plan'] as $item) {
|
|
||||||
$file = (string)$item['file'];
|
|
||||||
$sourceMode = (string)$item['source_mode'];
|
|
||||||
$output->writeln("- {$file} <= {$sourceMode}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$skippedFiles = $planData['skipped_files'] ?? [];
|
|
||||||
if (!empty($skippedFiles)) {
|
|
||||||
$output->warning('以下文件在该模式下不存在,将从项目根目录删除:');
|
|
||||||
foreach ($skippedFiles as $skipped) {
|
|
||||||
$output->writeln("- [删除] {$skipped}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$force && !$output->confirm($input, '确认执行覆盖切换?', true)) {
|
|
||||||
$output->warning('已取消');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $service->applyMode($mode, get_current_user() ?: 'cli');
|
|
||||||
$output->info('模式切换完成');
|
|
||||||
$output->writeln('mode=' . $result['mode']);
|
|
||||||
$output->writeln('backup_id=' . $result['backup_id']);
|
|
||||||
|
|
||||||
$deletedFiles = $result['deleted_files'] ?? [];
|
|
||||||
if (!empty($deletedFiles)) {
|
|
||||||
$output->warning('已删除文件:' . implode(', ', $deletedFiles));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function handleRollback(
|
|
||||||
StackModeService $service,
|
|
||||||
Input $input,
|
|
||||||
Output $output,
|
|
||||||
string $backupId,
|
|
||||||
bool $force
|
|
||||||
): void {
|
|
||||||
$backupId = trim($backupId);
|
|
||||||
if ($backupId === '') {
|
|
||||||
throw new RuntimeException('请提供 backup_id,例如:admin:stack:mode rollback 20260424120000-abcd1234');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$force && !$output->confirm($input, "确认回滚到备份 {$backupId}?", true)) {
|
|
||||||
$output->warning('已取消');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $service->rollback($backupId, get_current_user() ?: 'cli');
|
|
||||||
$output->info('回滚完成');
|
|
||||||
$output->writeln('backup_id=' . $result['backup_id']);
|
|
||||||
$output->writeln('restored_files=' . count($result['restored_files']));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace base\common\service\stack;
|
|
||||||
|
|
||||||
use RuntimeException;
|
|
||||||
use think\facade\App;
|
|
||||||
|
|
||||||
class StackModeServiceBase
|
|
||||||
{
|
|
||||||
protected string $rootPath;
|
|
||||||
protected string $stackRoot;
|
|
||||||
protected string $stackConfigPath;
|
|
||||||
protected string $backupRoot;
|
|
||||||
protected string $stateFile;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->rootPath = rtrim(App::getRootPath(), "\\/ \t\n\r\0\x0B");
|
|
||||||
$this->stackRoot = $this->joinPath($this->rootPath, 'source', 'stack');
|
|
||||||
$this->stackConfigPath = $this->joinPath($this->stackRoot, 'stack.json');
|
|
||||||
$this->backupRoot = $this->joinPath($this->rootPath, 'runtime', 'agents', 'stack-backup');
|
|
||||||
$this->stateFile = $this->joinPath($this->rootPath, 'runtime', 'agents', 'stack-current.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function listModes(): array
|
|
||||||
{
|
|
||||||
$config = $this->loadConfig();
|
|
||||||
$modes = [];
|
|
||||||
foreach ($config['modes'] as $mode => $meta) {
|
|
||||||
$modes[] = [
|
|
||||||
'mode' => $mode,
|
|
||||||
'description' => (string)($meta['description'] ?? ''),
|
|
||||||
'author_only' => (bool)($meta['author_only'] ?? false),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $modes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCurrentState(): array
|
|
||||||
{
|
|
||||||
if (!is_file($this->stateFile)) {
|
|
||||||
return [
|
|
||||||
'mode' => null,
|
|
||||||
'switched_at' => null,
|
|
||||||
'backup_id' => null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $this->readJsonFile($this->stateFile);
|
|
||||||
return [
|
|
||||||
'mode' => (string)($data['mode'] ?? ''),
|
|
||||||
'switched_at' => (string)($data['switched_at'] ?? ''),
|
|
||||||
'backup_id' => (string)($data['backup_id'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getModePlan(string $mode): array
|
|
||||||
{
|
|
||||||
$mode = trim($mode);
|
|
||||||
if ($mode === '') {
|
|
||||||
throw new RuntimeException('模式不能为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = $this->loadConfig();
|
|
||||||
$modes = $config['modes'];
|
|
||||||
if (!isset($modes[$mode])) {
|
|
||||||
throw new RuntimeException("模式不存在:{$mode}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultMode = $config['default_mode'];
|
|
||||||
$managedFiles = $config['managed_files'];
|
|
||||||
|
|
||||||
$plan = [];
|
|
||||||
foreach ($managedFiles as $relativePath) {
|
|
||||||
$modeSource = $this->resolveModeFile($mode, $relativePath);
|
|
||||||
if ($modeSource !== null) {
|
|
||||||
$plan[] = [
|
|
||||||
'file' => $relativePath,
|
|
||||||
'source_mode' => $mode,
|
|
||||||
'source_path' => $modeSource,
|
|
||||||
];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultSource = $this->resolveModeFile($defaultMode, $relativePath);
|
|
||||||
if ($defaultSource === null) {
|
|
||||||
throw new RuntimeException("默认模式缺少文件:{$relativePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$plan[] = [
|
|
||||||
'file' => $relativePath,
|
|
||||||
'source_mode' => $defaultMode,
|
|
||||||
'source_path' => $defaultSource,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'mode' => $mode,
|
|
||||||
'default_mode' => $defaultMode,
|
|
||||||
'managed_files' => $managedFiles,
|
|
||||||
'plan' => $plan,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function applyMode(string $mode, string $operator = 'system'): array
|
|
||||||
{
|
|
||||||
$planData = $this->getModePlan($mode);
|
|
||||||
$plan = $planData['plan'];
|
|
||||||
|
|
||||||
$this->ensureDir($this->backupRoot);
|
|
||||||
$backupId = date('YmdHis') . '-' . substr(md5((string)microtime(true)), 0, 8);
|
|
||||||
$backupDir = $this->joinPath($this->backupRoot, $backupId);
|
|
||||||
$backupFilesDir = $this->joinPath($backupDir, 'files');
|
|
||||||
$this->ensureDir($backupFilesDir);
|
|
||||||
|
|
||||||
$currentState = $this->getCurrentState();
|
|
||||||
$filesMeta = [];
|
|
||||||
|
|
||||||
foreach ($plan as $item) {
|
|
||||||
$relativePath = $item['file'];
|
|
||||||
$targetPath = $this->toRootPath($relativePath);
|
|
||||||
$backupFilePath = $this->joinPath($backupFilesDir, $this->toSystemPath($relativePath));
|
|
||||||
|
|
||||||
$exists = is_file($targetPath);
|
|
||||||
if ($exists) {
|
|
||||||
$this->ensureDir(dirname($backupFilePath));
|
|
||||||
$content = file_get_contents($targetPath);
|
|
||||||
if ($content === false) {
|
|
||||||
throw new RuntimeException("读取目标文件失败:{$relativePath}");
|
|
||||||
}
|
|
||||||
file_put_contents($backupFilePath, $content);
|
|
||||||
}
|
|
||||||
|
|
||||||
$filesMeta[] = [
|
|
||||||
'file' => $relativePath,
|
|
||||||
'existed' => $exists,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($plan as $item) {
|
|
||||||
$relativePath = $item['file'];
|
|
||||||
$targetPath = $this->toRootPath($relativePath);
|
|
||||||
$sourcePath = $item['source_path'];
|
|
||||||
|
|
||||||
$content = file_get_contents($sourcePath);
|
|
||||||
if ($content === false) {
|
|
||||||
throw new RuntimeException("读取模式文件失败:{$relativePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->ensureDir(dirname($targetPath));
|
|
||||||
file_put_contents($targetPath, $content);
|
|
||||||
}
|
|
||||||
|
|
||||||
$meta = [
|
|
||||||
'backup_id' => $backupId,
|
|
||||||
'mode' => $mode,
|
|
||||||
'operator' => $operator,
|
|
||||||
'created_at' => date('c'),
|
|
||||||
'files' => $filesMeta,
|
|
||||||
'prev_state' => $currentState,
|
|
||||||
];
|
|
||||||
$this->writeJsonFile($this->joinPath($backupDir, 'meta.json'), $meta);
|
|
||||||
|
|
||||||
$newState = [
|
|
||||||
'mode' => $mode,
|
|
||||||
'switched_at' => date('c'),
|
|
||||||
'backup_id' => $backupId,
|
|
||||||
'managed_files' => array_column($plan, 'file'),
|
|
||||||
];
|
|
||||||
$this->writeJsonFile($this->stateFile, $newState);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'mode' => $mode,
|
|
||||||
'backup_id' => $backupId,
|
|
||||||
'applied_files' => array_column($plan, 'file'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rollback(string $backupId, string $operator = 'system'): array
|
|
||||||
{
|
|
||||||
$backupId = trim($backupId);
|
|
||||||
if ($backupId === '') {
|
|
||||||
throw new RuntimeException('backup_id 不能为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
$backupDir = $this->joinPath($this->backupRoot, $backupId);
|
|
||||||
$metaPath = $this->joinPath($backupDir, 'meta.json');
|
|
||||||
if (!is_file($metaPath)) {
|
|
||||||
throw new RuntimeException("备份不存在:{$backupId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$meta = $this->readJsonFile($metaPath);
|
|
||||||
$files = $meta['files'] ?? [];
|
|
||||||
if (!is_array($files)) {
|
|
||||||
throw new RuntimeException("备份元数据损坏:{$backupId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$backupFilesDir = $this->joinPath($backupDir, 'files');
|
|
||||||
foreach ($files as $item) {
|
|
||||||
$relativePath = (string)($item['file'] ?? '');
|
|
||||||
if ($relativePath === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$relativePath = $this->normalizeRelativePath($relativePath);
|
|
||||||
$targetPath = $this->toRootPath($relativePath);
|
|
||||||
$existed = (bool)($item['existed'] ?? false);
|
|
||||||
$backupFilePath = $this->joinPath($backupFilesDir, $this->toSystemPath($relativePath));
|
|
||||||
|
|
||||||
if ($existed) {
|
|
||||||
if (!is_file($backupFilePath)) {
|
|
||||||
throw new RuntimeException("备份文件缺失:{$relativePath}");
|
|
||||||
}
|
|
||||||
$content = file_get_contents($backupFilePath);
|
|
||||||
if ($content === false) {
|
|
||||||
throw new RuntimeException("读取备份文件失败:{$relativePath}");
|
|
||||||
}
|
|
||||||
$this->ensureDir(dirname($targetPath));
|
|
||||||
file_put_contents($targetPath, $content);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_file($targetPath)) {
|
|
||||||
@unlink($targetPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentState = $this->getCurrentState();
|
|
||||||
$restoreState = $meta['prev_state'] ?? [];
|
|
||||||
$this->writeJsonFile($this->stateFile, [
|
|
||||||
'mode' => (string)($restoreState['mode'] ?? ''),
|
|
||||||
'switched_at' => date('c'),
|
|
||||||
'backup_id' => $backupId,
|
|
||||||
'rolled_back_by' => $operator,
|
|
||||||
'rolled_back_from' => (string)($currentState['mode'] ?? ''),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'backup_id' => $backupId,
|
|
||||||
'restored_files' => array_map(static function ($item) {
|
|
||||||
return (string)($item['file'] ?? '');
|
|
||||||
}, $files),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function loadConfig(): array
|
|
||||||
{
|
|
||||||
if (!is_file($this->stackConfigPath)) {
|
|
||||||
throw new RuntimeException('缺少全局清单:source/stack/stack.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = $this->readJsonFile($this->stackConfigPath);
|
|
||||||
|
|
||||||
$defaultMode = (string)($config['default_mode'] ?? '');
|
|
||||||
if ($defaultMode === '') {
|
|
||||||
throw new RuntimeException('stack.json 缺少 default_mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
$managedFilesRaw = $config['managed_files'] ?? [];
|
|
||||||
if (!is_array($managedFilesRaw) || empty($managedFilesRaw)) {
|
|
||||||
throw new RuntimeException('stack.json 缺少 managed_files');
|
|
||||||
}
|
|
||||||
|
|
||||||
$managedFiles = [];
|
|
||||||
foreach ($managedFilesRaw as $item) {
|
|
||||||
$managedFiles[] = $this->normalizeRelativePath((string)$item);
|
|
||||||
}
|
|
||||||
$managedFiles = array_values(array_unique($managedFiles));
|
|
||||||
|
|
||||||
$modes = $config['modes'] ?? [];
|
|
||||||
if (!is_array($modes) || empty($modes)) {
|
|
||||||
throw new RuntimeException('stack.json 缺少 modes');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($modes[$defaultMode])) {
|
|
||||||
throw new RuntimeException('default_mode 未在 modes 中声明');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'default_mode' => $defaultMode,
|
|
||||||
'managed_files' => $managedFiles,
|
|
||||||
'modes' => $modes,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function resolveModeFile(string $mode, string $relativePath): ?string
|
|
||||||
{
|
|
||||||
$filePath = $this->joinPath($this->stackRoot, $mode, $this->toSystemPath($relativePath));
|
|
||||||
if (is_file($filePath)) {
|
|
||||||
return $filePath;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function toRootPath(string $relativePath): string
|
|
||||||
{
|
|
||||||
return $this->joinPath($this->rootPath, $this->toSystemPath($relativePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function normalizeRelativePath(string $path): string
|
|
||||||
{
|
|
||||||
$path = str_replace('\\', '/', trim($path));
|
|
||||||
$path = preg_replace('#/+#', '/', $path);
|
|
||||||
$path = trim((string)$path, '/');
|
|
||||||
if ($path === '') {
|
|
||||||
throw new RuntimeException('检测到空路径');
|
|
||||||
}
|
|
||||||
if (strpos($path, '..') !== false) {
|
|
||||||
throw new RuntimeException("不允许越级路径:{$path}");
|
|
||||||
}
|
|
||||||
if (preg_match('/^[A-Za-z]:/', $path)) {
|
|
||||||
throw new RuntimeException("不允许绝对路径:{$path}");
|
|
||||||
}
|
|
||||||
return $path;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function toSystemPath(string $path): string
|
|
||||||
{
|
|
||||||
return str_replace('/', DIRECTORY_SEPARATOR, $path);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function joinPath(string ...$parts): string
|
|
||||||
{
|
|
||||||
$result = '';
|
|
||||||
foreach ($parts as $part) {
|
|
||||||
$part = trim($part);
|
|
||||||
if ($part === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($result === '') {
|
|
||||||
$result = rtrim($part, "\\/");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$result .= DIRECTORY_SEPARATOR . trim($part, "\\/");
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function ensureDir(string $dir): void
|
|
||||||
{
|
|
||||||
if ($dir === '' || is_dir($dir)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!@mkdir($dir, 0777, true) && !is_dir($dir)) {
|
|
||||||
throw new RuntimeException("创建目录失败:{$dir}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function readJsonFile(string $file): array
|
|
||||||
{
|
|
||||||
$raw = file_get_contents($file);
|
|
||||||
if ($raw === false) {
|
|
||||||
throw new RuntimeException("读取文件失败:{$file}");
|
|
||||||
}
|
|
||||||
$data = json_decode($raw, true);
|
|
||||||
if (!is_array($data)) {
|
|
||||||
throw new RuntimeException("JSON 解析失败:{$file}");
|
|
||||||
}
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function writeJsonFile(string $file, array $data): void
|
|
||||||
{
|
|
||||||
$this->ensureDir(dirname($file));
|
|
||||||
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
||||||
if ($json === false) {
|
|
||||||
throw new RuntimeException("JSON 编码失败:{$file}");
|
|
||||||
}
|
|
||||||
file_put_contents($file, $json . PHP_EOL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,6 @@ use app\common\command\admin\role\AdminRoleList;
|
|||||||
use app\common\command\admin\role\AdminRolePermissionAssign;
|
use app\common\command\admin\role\AdminRolePermissionAssign;
|
||||||
use app\common\command\admin\role\AdminRolePermissionList;
|
use app\common\command\admin\role\AdminRolePermissionList;
|
||||||
use app\common\command\admin\role\AdminRolePermissionRevoke;
|
use app\common\command\admin\role\AdminRolePermissionRevoke;
|
||||||
use app\common\command\admin\stack\AdminStackMode;
|
|
||||||
use app\common\command\admin\timer\TimerLogClean;
|
use app\common\command\admin\timer\TimerLogClean;
|
||||||
use app\common\command\admin\user\AdminUserRoleAssign;
|
use app\common\command\admin\user\AdminUserRoleAssign;
|
||||||
use app\common\command\admin\user\AdminUserRoleList;
|
use app\common\command\admin\user\AdminUserRoleList;
|
||||||
@@ -115,7 +114,6 @@ class UlthonAdminService extends Service
|
|||||||
AdminRolePermissionAssign::class,
|
AdminRolePermissionAssign::class,
|
||||||
AdminRolePermissionRevoke::class,
|
AdminRolePermissionRevoke::class,
|
||||||
AdminRolePermissionList::class,
|
AdminRolePermissionList::class,
|
||||||
AdminStackMode::class,
|
|
||||||
TimerLogClean::class,
|
TimerLogClean::class,
|
||||||
AdminUserRoleAssign::class,
|
AdminUserRoleAssign::class,
|
||||||
AdminUserRoleRevoke::class,
|
AdminUserRoleRevoke::class,
|
||||||
|
|||||||
Reference in New Issue
Block a user