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')); $protectedPaths = ['custom', 'public/custom']; if ($rsyncPath !== '') { $usedRsync = true; $rsync = new Process([ 'rsync', '-a', '--delete', '--exclude=.env', '--exclude=storage', '--exclude=public/storage', '--exclude=custom', '--exclude=public/custom', $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 { foreach ($protectedPaths as $path) { $sourcePath = $sourceDir . DIRECTORY_SEPARATOR . $path; if (File::exists($sourcePath)) { File::deleteDirectory($sourcePath); if (File::exists($sourcePath)) { File::delete($sourcePath); } } } 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); } } }