204 lines
7.4 KiB
PHP
204 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Setting;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
class SystemUpdateController extends Controller
|
|
{
|
|
public function __invoke(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
if (!$user || !$user->roles()->where('name', 'ROLE_ADMIN')->exists()) {
|
|
return response()->json(['message' => 'Forbidden'], 403);
|
|
}
|
|
|
|
set_time_limit(0);
|
|
|
|
$owner = env('GITEA_OWNER');
|
|
$repo = env('GITEA_REPO');
|
|
$apiBase = rtrim((string) env('GITEA_API_BASE', 'https://git.24unix.net/api/v1'), '/');
|
|
$token = env('GITEA_TOKEN');
|
|
|
|
if (!$owner || !$repo) {
|
|
return response()->json(['message' => 'Missing Gitea configuration.'], 422);
|
|
}
|
|
|
|
$log = [];
|
|
$append = function (string $line) use (&$log) {
|
|
$log[] = $line;
|
|
};
|
|
|
|
try {
|
|
$client = Http::acceptJson();
|
|
if ($token) {
|
|
$client = $client->withHeaders(['Authorization' => "token {$token}"]);
|
|
}
|
|
|
|
$append('Fetching latest release...');
|
|
$response = $client->get("{$apiBase}/repos/{$owner}/{$repo}/releases/latest");
|
|
if (!$response->successful()) {
|
|
return response()->json([
|
|
'message' => "Release check failed: {$response->status()}",
|
|
'log' => $log,
|
|
], 500);
|
|
}
|
|
|
|
$tag = (string) ($response->json('tag_name') ?? '');
|
|
if ($tag === '') {
|
|
return response()->json([
|
|
'message' => 'Release tag not found.',
|
|
'log' => $log,
|
|
], 500);
|
|
}
|
|
|
|
$tarballUrl = (string) ($response->json('tarball_url') ?? '');
|
|
if ($tarballUrl === '') {
|
|
$tarballUrl = env('GITEA_TGZ_URL_TEMPLATE');
|
|
if ($tarballUrl) {
|
|
$tarballUrl = str_replace('{{TAG}}', $tag, $tarballUrl);
|
|
$tarballUrl = str_replace('{{VERSION}}', ltrim($tag, 'v'), $tarballUrl);
|
|
}
|
|
}
|
|
|
|
if ($tarballUrl === '') {
|
|
return response()->json([
|
|
'message' => 'No tarball URL available.',
|
|
'log' => $log,
|
|
], 500);
|
|
}
|
|
|
|
$append("Downloading {$tag}...");
|
|
$archivePath = storage_path('app/updates/' . $tag . '.tar.gz');
|
|
File::ensureDirectoryExists(dirname($archivePath));
|
|
|
|
$download = $client->withOptions(['stream' => true])->get($tarballUrl);
|
|
if (!$download->successful()) {
|
|
return response()->json([
|
|
'message' => "Download failed: {$download->status()}",
|
|
'log' => $log,
|
|
], 500);
|
|
}
|
|
File::put($archivePath, $download->body());
|
|
|
|
$extractDir = storage_path('app/updates/extract-' . Str::random(8));
|
|
File::ensureDirectoryExists($extractDir);
|
|
|
|
$append('Extracting archive...');
|
|
$tar = new Process(['tar', '-xzf', $archivePath, '-C', $extractDir]);
|
|
$tar->setTimeout(300);
|
|
$tar->run();
|
|
if (!$tar->isSuccessful()) {
|
|
return response()->json([
|
|
'message' => 'Failed to extract archive.',
|
|
'log' => array_merge($log, [$tar->getErrorOutput()]),
|
|
], 500);
|
|
}
|
|
|
|
$entries = collect(File::directories($extractDir))->values();
|
|
if ($entries->isEmpty()) {
|
|
return response()->json([
|
|
'message' => 'No extracted folder found.',
|
|
'log' => $log,
|
|
], 500);
|
|
}
|
|
$sourceDir = $entries->first();
|
|
|
|
$append('Syncing files...');
|
|
$usedRsync = false;
|
|
$rsyncPath = trim((string) shell_exec('command -v rsync'));
|
|
if ($rsyncPath !== '') {
|
|
$usedRsync = true;
|
|
$rsync = new Process([
|
|
'rsync',
|
|
'-a',
|
|
'--delete',
|
|
'--exclude=.env',
|
|
'--exclude=storage',
|
|
'--exclude=public/storage',
|
|
$sourceDir . '/',
|
|
base_path() . '/',
|
|
]);
|
|
$rsync->setTimeout(600);
|
|
$rsync->run();
|
|
if (!$rsync->isSuccessful()) {
|
|
return response()->json([
|
|
'message' => 'rsync failed.',
|
|
'log' => array_merge($log, [$rsync->getErrorOutput()]),
|
|
], 500);
|
|
}
|
|
} else {
|
|
File::copyDirectory($sourceDir, base_path());
|
|
}
|
|
|
|
$append('Installing composer dependencies...');
|
|
$composer = new Process(['composer', 'install', '--no-dev', '--optimize-autoloader'], base_path());
|
|
$composer->setTimeout(600);
|
|
$composer->run();
|
|
if (!$composer->isSuccessful()) {
|
|
return response()->json([
|
|
'message' => 'Composer install failed.',
|
|
'log' => array_merge($log, [$composer->getErrorOutput()]),
|
|
], 500);
|
|
}
|
|
|
|
$append('Installing npm dependencies...');
|
|
$npmInstall = new Process(['npm', 'install'], base_path());
|
|
$npmInstall->setTimeout(600);
|
|
$npmInstall->run();
|
|
if (!$npmInstall->isSuccessful()) {
|
|
return response()->json([
|
|
'message' => 'npm install failed.',
|
|
'log' => array_merge($log, [$npmInstall->getErrorOutput()]),
|
|
], 500);
|
|
}
|
|
|
|
$append('Building assets...');
|
|
$npmBuild = new Process(['npm', 'run', 'build'], base_path());
|
|
$npmBuild->setTimeout(900);
|
|
$npmBuild->run();
|
|
if (!$npmBuild->isSuccessful()) {
|
|
return response()->json([
|
|
'message' => 'npm run build failed.',
|
|
'log' => array_merge($log, [$npmBuild->getErrorOutput()]),
|
|
], 500);
|
|
}
|
|
|
|
$phpBinary = trim((string) Setting::where('key', 'system.php_binary')->value('value'));
|
|
if ($phpBinary === '') {
|
|
$phpBinary = env('SYSTEM_UPDATE_PHP_BINARY') ?: (PHP_BINARY ?: 'php');
|
|
}
|
|
$append("Running migrations (using {$phpBinary})...");
|
|
$migrate = new Process([$phpBinary, 'artisan', 'migrate', '--force'], base_path());
|
|
$migrate->setTimeout(600);
|
|
$migrate->run();
|
|
if (!$migrate->isSuccessful()) {
|
|
return response()->json([
|
|
'message' => 'Migrations failed.',
|
|
'log' => array_merge($log, [$migrate->getErrorOutput()]),
|
|
], 500);
|
|
}
|
|
|
|
$append('Update complete.');
|
|
|
|
return response()->json([
|
|
'message' => 'Update finished.',
|
|
'log' => $log,
|
|
'tag' => $tag,
|
|
'used_rsync' => $usedRsync,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'message' => 'Update failed.',
|
|
'log' => $log,
|
|
], 500);
|
|
}
|
|
}
|
|
}
|