Admin: move deploy flow into settings and use secrets file
This commit is contained in:
parent
27691ba396
commit
bc48e6959d
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,8 +15,8 @@ Thumbs.db
|
|||
.vscode/
|
||||
|
||||
# Local secrets
|
||||
deploy-config.php
|
||||
config.php
|
||||
secrets.php
|
||||
|
||||
# Logs/temp
|
||||
*.log
|
||||
|
|
|
|||
43
README.md
43
README.md
|
|
@ -42,16 +42,16 @@ photo.andr33v.ru/
|
|||
├─ admin.php # админка по токену
|
||||
├─ index-mysql.php # alias -> index.php
|
||||
├─ admin-mysql.php # alias -> admin.php
|
||||
├─ deploy.php # webhook деплоя
|
||||
├─ style.css # базовые стили
|
||||
├─ favicon.svg
|
||||
├─ config.php.example # шаблон конфига БД
|
||||
├─ deploy-config.php.example # шаблон токена/настроек деплоя
|
||||
├─ config.php.example # шаблон конфига БД и деплоя
|
||||
├─ secrets.php.example # шаблон секретов админки
|
||||
├─ lib/
|
||||
│ ├─ db.php # PDO + загрузка config.php
|
||||
│ ├─ db_gallery.php # доступ к данным галереи
|
||||
│ ├─ thumbs.php # генерация/чтение/удаление превью
|
||||
│ ├─ admin_http.php # JSON-ответы админки
|
||||
│ ├─ admin_deploy.php # проверка обновлений и запуск деплоя
|
||||
│ ├─ admin_get_actions.php # GET-экшены админки
|
||||
│ ├─ admin_post_actions.php # POST-экшены админки
|
||||
│ └─ admin_helpers.php # helper-функции админки
|
||||
|
|
@ -87,36 +87,49 @@ cp config.php.example config.php
|
|||
|
||||
2. Заполни доступы к БД в `config.php`.
|
||||
|
||||
3. Прогон миграций:
|
||||
3. Создай `secrets.php`:
|
||||
|
||||
```bash
|
||||
cp secrets.php.example secrets.php
|
||||
```
|
||||
|
||||
4. Заполни минимум `admin_token` в `secrets.php`.
|
||||
|
||||
5. Прогон миграций:
|
||||
|
||||
```bash
|
||||
php scripts/migrate.php
|
||||
```
|
||||
|
||||
4. Локальный запуск:
|
||||
6. Локальный запуск:
|
||||
|
||||
```bash
|
||||
php -S 127.0.0.1:8080
|
||||
```
|
||||
|
||||
5. Открой:
|
||||
7. Открой:
|
||||
|
||||
- `http://127.0.0.1:8080` — публичная часть,
|
||||
- `http://127.0.0.1:8080/admin.php?token=<SECRET>` — админка (после настройки `deploy-config.php`).
|
||||
- `http://127.0.0.1:8080/admin.php?token=<SECRET>` — админка (токен из `secrets.php`).
|
||||
|
||||
## Настройка админки
|
||||
|
||||
Админка использует `token` из `deploy-config.php`.
|
||||
Админка использует секреты из `secrets.php`.
|
||||
|
||||
Создай файл:
|
||||
|
||||
```bash
|
||||
cp deploy-config.php.example deploy-config.php
|
||||
cp secrets.php.example secrets.php
|
||||
```
|
||||
|
||||
Минимально заполни:
|
||||
|
||||
- `token` — длинный случайный секрет.
|
||||
- `admin_token` — длинный случайный секрет.
|
||||
|
||||
Опционально можно ограничить доступ:
|
||||
|
||||
- `basic_auth_user` / `basic_auth_pass` — HTTP Basic слой поверх админки,
|
||||
- `allowed_admin_ips` — белый список IP (строгое совпадение строк).
|
||||
|
||||
В админке доступны разделы:
|
||||
|
||||
|
|
@ -144,20 +157,22 @@ php scripts/generate_thumbs.php
|
|||
|
||||
## Деплой
|
||||
|
||||
Webhook:
|
||||
Деплой запускается из админки (вкладка `Настройки`):
|
||||
|
||||
- `deploy.php?token=<SECRET>`
|
||||
- кнопка `Проверить обновления` делает `git fetch` и сравнивает `HEAD` с `origin/<branch>`,
|
||||
- если локальная ветка отстает и не расходится (`behind > 0`, `ahead = 0`) — показывается кнопка `Обновить проект`.
|
||||
|
||||
Скрипт `scripts/deploy.sh`:
|
||||
|
||||
1. делает `git fetch --all --prune`,
|
||||
2. переключает код на `origin/<branch>` через `git reset --hard`,
|
||||
3. сохраняет runtime-папки (`photos`, `thumbs`, `data`).
|
||||
3. запускает миграции `php scripts/migrate.php`,
|
||||
4. сохраняет runtime-папки (`photos`, `thumbs`, `data`).
|
||||
|
||||
Важно: деплой-скрипт перетирает рабочие изменения в репозитории на сервере.
|
||||
|
||||
## Примечания
|
||||
|
||||
- Проект принудительно редиректит на HTTPS и non-www через `.htaccess`.
|
||||
- Для production рекомендуется ограничить webhook по IP и/или Basic Auth (`deploy-config.php`).
|
||||
- Для production рекомендуется включить IP whitelist и/или Basic Auth в `secrets.php`.
|
||||
- Если `config.php` отсутствует, приложение корректно падает с ошибкой подключения к БД.
|
||||
|
|
|
|||
87
admin.php
87
admin.php
|
|
@ -6,19 +6,29 @@ require_once __DIR__ . '/lib/db_gallery.php';
|
|||
require_once __DIR__ . '/lib/thumbs.php';
|
||||
require_once __DIR__ . '/lib/admin_http.php';
|
||||
require_once __DIR__ . '/lib/admin_helpers.php';
|
||||
require_once __DIR__ . '/lib/admin_deploy.php';
|
||||
require_once __DIR__ . '/lib/admin_get_actions.php';
|
||||
require_once __DIR__ . '/lib/admin_post_actions.php';
|
||||
|
||||
const MAX_UPLOAD_BYTES = 3 * 1024 * 1024;
|
||||
|
||||
$configPath = __DIR__ . '/deploy-config.php';
|
||||
if (!is_file($configPath)) {
|
||||
try {
|
||||
$config = appConfig();
|
||||
$secrets = appSecrets();
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
exit('deploy-config.php not found');
|
||||
exit($e->getMessage());
|
||||
}
|
||||
$config = require $configPath;
|
||||
$basicUser = (string)($config['basic_auth_user'] ?? '');
|
||||
$basicPass = (string)($config['basic_auth_pass'] ?? '');
|
||||
|
||||
$allowedAdminIps = array_values(array_filter(array_map(static fn($ip): string => trim((string)$ip), (array)($secrets['allowed_admin_ips'] ?? [])), static fn(string $ip): bool => $ip !== ''));
|
||||
$clientIp = (string)($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
if ($allowedAdminIps !== [] && !in_array($clientIp, $allowedAdminIps, true)) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
$basicUser = (string)($secrets['basic_auth_user'] ?? '');
|
||||
$basicPass = (string)($secrets['basic_auth_pass'] ?? '');
|
||||
|
||||
if ($basicUser !== '' || $basicPass !== '') {
|
||||
$authUser = (string)($_SERVER['PHP_AUTH_USER'] ?? '');
|
||||
|
|
@ -30,13 +40,27 @@ if ($basicUser !== '' || $basicPass !== '') {
|
|||
}
|
||||
}
|
||||
|
||||
$tokenExpected = (string)($config['token'] ?? '');
|
||||
$tokenExpected = (string)($secrets['admin_token'] ?? '');
|
||||
$tokenIncoming = (string)($_REQUEST['token'] ?? '');
|
||||
if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
$deployConfig = (array)($config['deploy'] ?? []);
|
||||
$deployBranch = trim((string)($deployConfig['branch'] ?? 'main'));
|
||||
if ($deployBranch === '') {
|
||||
$deployBranch = 'main';
|
||||
}
|
||||
$deployScript = trim((string)($deployConfig['script'] ?? (__DIR__ . '/scripts/deploy.sh')));
|
||||
if ($deployScript !== '' && !str_starts_with($deployScript, '/')) {
|
||||
$deployScript = __DIR__ . '/' . ltrim($deployScript, '/');
|
||||
}
|
||||
$deployPhpBin = trim((string)($deployConfig['php_bin'] ?? 'php'));
|
||||
if ($deployPhpBin === '') {
|
||||
$deployPhpBin = 'php';
|
||||
}
|
||||
|
||||
$requestAction = (string)($_REQUEST['action'] ?? '');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
adminHandleGetAction($requestAction);
|
||||
|
|
@ -44,6 +68,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|||
|
||||
$message = '';
|
||||
$errors = [];
|
||||
$deployStatus = null;
|
||||
$deployOutput = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
|
|
@ -51,8 +77,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
|| strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
|
||||
|
||||
try {
|
||||
$result = adminHandlePostAction($action, $isAjax, __DIR__);
|
||||
$result = adminHandlePostAction($action, $isAjax, __DIR__, [
|
||||
'branch' => $deployBranch,
|
||||
'script' => $deployScript,
|
||||
'php_bin' => $deployPhpBin,
|
||||
]);
|
||||
$message = (string)($result['message'] ?? '');
|
||||
$deployStatus = $result['deploy_status'] ?? null;
|
||||
$deployOutput = (string)($result['deploy_output'] ?? '');
|
||||
if (isset($result['errors']) && is_array($result['errors']) && $result['errors'] !== []) {
|
||||
$errors = array_merge($errors, $result['errors']);
|
||||
}
|
||||
|
|
@ -134,6 +166,8 @@ function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $
|
|||
.sec a{display:block;padding:8px 10px;border-radius:8px;text-decoration:none;color:#111}
|
||||
.sec a.active{background:#eef4ff;color:#1f6feb}
|
||||
.small{font-size:12px;color:#667085}
|
||||
.deploy-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
|
||||
.deploy-output{margin-top:10px;padding:10px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc;font-size:12px;white-space:pre-wrap;line-height:1.4;max-height:220px;overflow:auto}
|
||||
.inline-form{margin:0}
|
||||
.after-slot{display:flex;flex-direction:column;align-items:flex-start;gap:6px}
|
||||
.preview-actions{display:flex;gap:6px;margin-top:6px;flex-wrap:nowrap}
|
||||
|
|
@ -242,6 +276,43 @@ function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $
|
|||
</p>
|
||||
<button class="btn" type="submit">Сохранить настройки</button>
|
||||
</form>
|
||||
|
||||
<hr style="border:none;border-top:1px solid #eee;margin:12px 0">
|
||||
<h4 style="margin:0 0 8px">Обновление проекта</h4>
|
||||
<p class="small" style="margin:0">Ветка: <strong><?= h($deployBranch) ?></strong></p>
|
||||
|
||||
<?php if (is_array($deployStatus)): ?>
|
||||
<?php $deployState = (string)($deployStatus['state'] ?? ''); ?>
|
||||
<?php $deployStateMessage = $deployState === 'update_available'
|
||||
? 'Доступна новая версия.'
|
||||
: ($deployState === 'up_to_date'
|
||||
? 'Установлена актуальная версия.'
|
||||
: ($deployState === 'local_ahead'
|
||||
? 'Локальная ветка опережает origin. Автообновление отключено.'
|
||||
: 'Ветка расходится с origin. Нужна ручная синхронизация.')); ?>
|
||||
<div class="<?= in_array($deployState, ['local_ahead', 'diverged'], true) ? 'err' : 'ok' ?>" style="margin-top:8px">
|
||||
<?= h($deployStateMessage) ?><br>
|
||||
<span class="small">Локально: <?= h((string)($deployStatus['local_ref'] ?? '-')) ?> · origin/<?= h($deployBranch) ?>: <?= h((string)($deployStatus['remote_ref'] ?? '-')) ?> · behind: <?= (int)($deployStatus['behind'] ?? 0) ?> · ahead: <?= (int)($deployStatus['ahead'] ?? 0) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="deploy-actions">
|
||||
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&mode=welcome">
|
||||
<input type="hidden" name="action" value="check_updates"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>">
|
||||
<button class="btn btn-secondary" type="submit">Проверить обновления</button>
|
||||
</form>
|
||||
|
||||
<?php if (is_array($deployStatus) && !empty($deployStatus['can_deploy'])): ?>
|
||||
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&mode=welcome" onsubmit="return confirm('Обновить код из origin/<?= h($deployBranch) ?> и запустить миграции?')">
|
||||
<input type="hidden" name="action" value="deploy_updates"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>">
|
||||
<button class="btn" type="submit">Обновить проект</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deployOutput !== ''): ?>
|
||||
<pre class="deploy-output"><?= h($deployOutput) ?></pre>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,4 +8,9 @@ return [
|
|||
'pass' => 'change_me',
|
||||
'charset' => 'utf8mb4',
|
||||
],
|
||||
'deploy' => [
|
||||
'branch' => 'main',
|
||||
'script' => __DIR__ . '/scripts/deploy.sh',
|
||||
'php_bin' => 'php',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Copy this file to deploy-config.php and fill secrets.
|
||||
* deploy-config.php is ignored by git.
|
||||
*/
|
||||
return [
|
||||
// REQUIRED: long random token (40+ chars)
|
||||
'token' => 'CHANGE_ME_TO_LONG_RANDOM_TOKEN',
|
||||
|
||||
// Optional: HTTP Basic auth layer
|
||||
'basic_auth_user' => '',
|
||||
'basic_auth_pass' => '',
|
||||
|
||||
// Optional: allow only these client IPs (empty = allow all)
|
||||
'allowed_ips' => [
|
||||
// '1.2.3.4',
|
||||
],
|
||||
|
||||
// Deploy options
|
||||
'branch' => 'main',
|
||||
'deploy_script' => __DIR__ . '/scripts/deploy.sh',
|
||||
|
||||
// Where to write webhook logs
|
||||
'log_file' => __DIR__ . '/data/deploy-webhook.log',
|
||||
];
|
||||
86
deploy.php
86
deploy.php
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$configPath = __DIR__ . '/deploy-config.php';
|
||||
if (!is_file($configPath)) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo "deploy-config.php not found. Create it from deploy-config.php.example\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
/** @var array<string,mixed> $config */
|
||||
$config = require $configPath;
|
||||
$tokenExpected = (string)($config['token'] ?? '');
|
||||
$allowedIps = (array)($config['allowed_ips'] ?? []);
|
||||
$basicUser = (string)($config['basic_auth_user'] ?? '');
|
||||
$basicPass = (string)($config['basic_auth_pass'] ?? '');
|
||||
$branch = (string)($config['branch'] ?? 'main');
|
||||
$deployScript = (string)($config['deploy_script'] ?? (__DIR__ . '/scripts/deploy.sh'));
|
||||
$logFile = (string)($config['log_file'] ?? (__DIR__ . '/data/deploy-webhook.log'));
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
if ($basicUser !== '' || $basicPass !== '') {
|
||||
$authUser = $_SERVER['PHP_AUTH_USER'] ?? '';
|
||||
$authPass = $_SERVER['PHP_AUTH_PW'] ?? '';
|
||||
if (!hash_equals($basicUser, (string)$authUser) || !hash_equals($basicPass, (string)$authPass)) {
|
||||
header('WWW-Authenticate: Basic realm="Deploy"');
|
||||
http_response_code(401);
|
||||
echo "Unauthorized\n";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$clientIp = (string)($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
||||
if ($allowedIps !== [] && !in_array($clientIp, $allowedIps, true)) {
|
||||
http_response_code(403);
|
||||
echo "Forbidden: IP not allowed\n";
|
||||
logLine($logFile, "DENY ip={$clientIp} reason=ip_not_allowed");
|
||||
exit;
|
||||
}
|
||||
|
||||
$tokenIncoming = (string)($_REQUEST['token'] ?? '');
|
||||
if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
|
||||
http_response_code(403);
|
||||
echo "Forbidden: invalid token\n";
|
||||
logLine($logFile, "DENY ip={$clientIp} reason=bad_token");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!is_file($deployScript)) {
|
||||
http_response_code(500);
|
||||
echo "Deploy script not found\n";
|
||||
logLine($logFile, "ERROR ip={$clientIp} reason=script_missing path={$deployScript}");
|
||||
exit;
|
||||
}
|
||||
|
||||
$cmd = 'BRANCH=' . escapeshellarg($branch) . ' bash ' . escapeshellarg($deployScript) . ' 2>&1';
|
||||
exec($cmd, $output, $code);
|
||||
|
||||
$preview = implode("\n", array_slice($output, -30));
|
||||
logLine(
|
||||
$logFile,
|
||||
"RUN ip={$clientIp} code={$code} branch={$branch} output=" . str_replace(["\n", "\r"], ['\\n', ''], $preview)
|
||||
);
|
||||
|
||||
if ($code !== 0) {
|
||||
http_response_code(500);
|
||||
echo "Deploy failed\n\n";
|
||||
echo $preview . "\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "OK: deploy completed\n\n";
|
||||
echo $preview . "\n";
|
||||
|
||||
function logLine(string $path, string $line): void
|
||||
{
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0775, true);
|
||||
}
|
||||
$ts = date('Y-m-d H:i:s');
|
||||
@file_put_contents($path, "[{$ts}] {$line}\n", FILE_APPEND);
|
||||
}
|
||||
97
lib/admin_deploy.php
Normal file
97
lib/admin_deploy.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function adminCheckForUpdates(string $projectRoot, string $branch): array
|
||||
{
|
||||
if (!is_dir($projectRoot . '/.git')) {
|
||||
throw new RuntimeException('Репозиторий не найден: .git отсутствует');
|
||||
}
|
||||
|
||||
$fetch = adminRunShellCommand('git fetch origin ' . escapeshellarg($branch) . ' --prune', $projectRoot);
|
||||
if ($fetch['code'] !== 0) {
|
||||
throw new RuntimeException('Не удалось обновить данные из origin: ' . adminTailOutput($fetch['output']));
|
||||
}
|
||||
|
||||
$local = adminRunShellCommand('git rev-parse --short=12 HEAD', $projectRoot);
|
||||
$remote = adminRunShellCommand('git rev-parse --short=12 origin/' . escapeshellarg($branch), $projectRoot);
|
||||
$behindRaw = adminRunShellCommand('git rev-list --count HEAD..origin/' . escapeshellarg($branch), $projectRoot);
|
||||
$aheadRaw = adminRunShellCommand('git rev-list --count origin/' . escapeshellarg($branch) . '..HEAD', $projectRoot);
|
||||
|
||||
if ($local['code'] !== 0 || $remote['code'] !== 0 || $behindRaw['code'] !== 0 || $aheadRaw['code'] !== 0) {
|
||||
throw new RuntimeException('Не удалось определить состояние ветки');
|
||||
}
|
||||
|
||||
$behind = (int)trim($behindRaw['output']);
|
||||
$ahead = (int)trim($aheadRaw['output']);
|
||||
|
||||
$state = 'up_to_date';
|
||||
if ($behind > 0 && $ahead === 0) {
|
||||
$state = 'update_available';
|
||||
} elseif ($ahead > 0 && $behind === 0) {
|
||||
$state = 'local_ahead';
|
||||
} elseif ($ahead > 0 && $behind > 0) {
|
||||
$state = 'diverged';
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => $state,
|
||||
'branch' => $branch,
|
||||
'local_ref' => trim($local['output']),
|
||||
'remote_ref' => trim($remote['output']),
|
||||
'behind' => $behind,
|
||||
'ahead' => $ahead,
|
||||
'can_deploy' => $state === 'update_available',
|
||||
];
|
||||
}
|
||||
|
||||
function adminRunDeployScript(string $projectRoot, string $branch, string $scriptPath, string $phpBin): array
|
||||
{
|
||||
if (!is_file($scriptPath)) {
|
||||
throw new RuntimeException('Скрипт деплоя не найден: ' . $scriptPath);
|
||||
}
|
||||
|
||||
$run = adminRunShellCommand('bash ' . escapeshellarg($scriptPath), $projectRoot, [
|
||||
'BRANCH' => $branch,
|
||||
'PHP_BIN' => $phpBin,
|
||||
]);
|
||||
|
||||
return [
|
||||
'ok' => $run['code'] === 0,
|
||||
'code' => $run['code'],
|
||||
'output' => adminTailOutput($run['output']),
|
||||
];
|
||||
}
|
||||
|
||||
function adminRunShellCommand(string $command, string $cwd, array $env = []): array
|
||||
{
|
||||
$envPrefix = '';
|
||||
foreach ($env as $key => $value) {
|
||||
if (!preg_match('/^[A-Z_][A-Z0-9_]*$/', (string)$key)) {
|
||||
continue;
|
||||
}
|
||||
$envPrefix .= $key . '=' . escapeshellarg((string)$value) . ' ';
|
||||
}
|
||||
|
||||
$fullCommand = 'cd ' . escapeshellarg($cwd) . ' && ' . $envPrefix . $command . ' 2>&1';
|
||||
$output = [];
|
||||
$code = 0;
|
||||
exec($fullCommand, $output, $code);
|
||||
|
||||
return ['code' => $code, 'output' => implode("\n", $output)];
|
||||
}
|
||||
|
||||
function adminTailOutput(string $output, int $maxLines = 80): string
|
||||
{
|
||||
$output = trim($output);
|
||||
if ($output === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = preg_split('/\r\n|\r|\n/', $output);
|
||||
if (!is_array($lines)) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
return implode("\n", array_slice($lines, -$maxLines));
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot): array
|
||||
function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot, array $deployOptions = []): array
|
||||
{
|
||||
$message = '';
|
||||
$errors = [];
|
||||
$deployStatus = null;
|
||||
$deployOutput = '';
|
||||
|
||||
switch ($action) {
|
||||
case 'create_section': {
|
||||
|
|
@ -141,6 +143,56 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
|
|||
break;
|
||||
}
|
||||
|
||||
case 'check_updates': {
|
||||
$branch = (string)($deployOptions['branch'] ?? 'main');
|
||||
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
|
||||
$state = (string)($deployStatus['state'] ?? '');
|
||||
|
||||
if ($state === 'update_available') {
|
||||
$message = 'Найдена новая версия. Можно обновиться.';
|
||||
} elseif ($state === 'up_to_date') {
|
||||
$message = 'Обновлений нет: установлена актуальная версия.';
|
||||
} elseif ($state === 'local_ahead') {
|
||||
$message = 'Локальная ветка опережает origin. Автообновление отключено.';
|
||||
} else {
|
||||
$message = 'Ветка расходится с origin. Нужна ручная синхронизация.';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'deploy_updates': {
|
||||
$branch = (string)($deployOptions['branch'] ?? 'main');
|
||||
$scriptPath = (string)($deployOptions['script'] ?? ($projectRoot . '/scripts/deploy.sh'));
|
||||
$phpBin = (string)($deployOptions['php_bin'] ?? 'php');
|
||||
|
||||
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
|
||||
if (!(bool)($deployStatus['can_deploy'] ?? false)) {
|
||||
$state = (string)($deployStatus['state'] ?? '');
|
||||
if ($state === 'up_to_date') {
|
||||
$message = 'Обновление не требуется: уже актуальная версия.';
|
||||
break;
|
||||
}
|
||||
if ($state === 'local_ahead') {
|
||||
throw new RuntimeException('Локальная ветка опережает origin. Автообновление отключено.');
|
||||
}
|
||||
if ($state === 'diverged') {
|
||||
throw new RuntimeException('Ветка расходится с origin. Выполни ручную синхронизацию.');
|
||||
}
|
||||
throw new RuntimeException('Нельзя применить обновление в текущем состоянии ветки.');
|
||||
}
|
||||
|
||||
$deployResult = adminRunDeployScript($projectRoot, $branch, $scriptPath, $phpBin);
|
||||
$deployOutput = (string)($deployResult['output'] ?? '');
|
||||
if (!(bool)($deployResult['ok'] ?? false)) {
|
||||
throw new RuntimeException('Деплой завершился с ошибкой: ' . ($deployOutput !== '' ? $deployOutput : ('код ' . (int)($deployResult['code'] ?? 1))));
|
||||
}
|
||||
|
||||
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
|
||||
$message = 'Обновление выполнено.';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'upload_before_bulk': {
|
||||
$sectionId = (int)($_POST['section_id'] ?? 0);
|
||||
if ($sectionId < 1 || !sectionById($sectionId)) {
|
||||
|
|
@ -392,5 +444,10 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
|
|||
}
|
||||
}
|
||||
|
||||
return ['message' => $message, 'errors' => $errors];
|
||||
return [
|
||||
'message' => $message,
|
||||
'errors' => $errors,
|
||||
'deploy_status' => $deployStatus,
|
||||
'deploy_output' => $deployOutput,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
20
lib/db.php
20
lib/db.php
|
|
@ -22,6 +22,26 @@ function appConfig(): array
|
|||
return $cfg;
|
||||
}
|
||||
|
||||
function appSecrets(): array
|
||||
{
|
||||
static $secrets = null;
|
||||
if ($secrets !== null) {
|
||||
return $secrets;
|
||||
}
|
||||
|
||||
$path = __DIR__ . '/../secrets.php';
|
||||
if (!is_file($path)) {
|
||||
throw new RuntimeException('secrets.php not found. Copy secrets.php.example');
|
||||
}
|
||||
|
||||
$secrets = require $path;
|
||||
if (!is_array($secrets)) {
|
||||
throw new RuntimeException('Invalid secrets.php format');
|
||||
}
|
||||
|
||||
return $secrets;
|
||||
}
|
||||
|
||||
function db(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ set -euo pipefail
|
|||
# Optional env:
|
||||
# APP_DIR=/home/USER/www/photo-gallery
|
||||
# BRANCH=main
|
||||
# PHP_BIN=php
|
||||
|
||||
APP_DIR="${APP_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
PHP_BIN="${PHP_BIN:-php}"
|
||||
|
||||
cd "$APP_DIR"
|
||||
|
||||
|
|
@ -42,6 +44,9 @@ fi
|
|||
git fetch --all --prune
|
||||
git reset --hard "origin/$BRANCH"
|
||||
|
||||
# Run DB migrations required by current code
|
||||
"$PHP_BIN" scripts/migrate.php
|
||||
|
||||
# Make sure runtime files exist
|
||||
[ -f data/last_indexed.txt ] || echo "0" > data/last_indexed.txt
|
||||
|
||||
|
|
|
|||
10
secrets.php.example
Normal file
10
secrets.php.example
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'admin_token' => 'CHANGE_ME_TO_LONG_RANDOM_TOKEN',
|
||||
'basic_auth_user' => '',
|
||||
'basic_auth_pass' => '',
|
||||
'allowed_admin_ips' => [
|
||||
// '1.2.3.4',
|
||||
],
|
||||
];
|
||||
Loading…
Reference in New Issue
Block a user