• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visavi / rotor / 27909067548

21 Jun 2026 03:33PM UTC coverage: 16.253% (-0.09%) from 16.344%
27909067548

push

github

visavi
Добавлено более понятное обновления модулей, кнопки заменены на скачать обновление и применить обновление, на странице обновления в админке есть возможность выбрать апгрейд или скачать полную версиию, добавлена кнопка переустановки ядра, добавлен сброс opcache чтобы кнопка обновления появлялась сразу после скачивания, испавлено экранирование в md

0 of 40 new or added lines in 4 files covered. (0.0%)

3 existing lines in 3 files now uncovered.

970 of 5968 relevant lines covered (16.25%)

2.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.04
/app/Http/Controllers/Admin/UpgradeController.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Http\Controllers\Admin;
6

7
use App\Services\GithubService;
8
use App\Services\MigrationService;
9
use App\Services\UpgradeService;
10
use Illuminate\Http\JsonResponse;
11
use Illuminate\Http\Request;
12
use Illuminate\Support\Facades\Artisan;
13
use Illuminate\View\View;
14
use Throwable;
15

16
class UpgradeController extends AdminController
17
{
18
    public function __construct(
3✔
19
        private readonly MigrationService $migrations,
20
        private readonly UpgradeService $upgrade,
21
    ) {
22
    }
3✔
23

24
    /**
25
     * Страница обновлений
26
     */
27
    public function index(GithubService $githubService): View
×
28
    {
29
        $pendingMigrations = $this->migrations->getPendingMigrations($this->migrationPaths());
×
30
        $newReleases = $this->upgrade->getNewReleases($githubService);
×
31

32
        // Резолвим архив, который реально скачается (upgrade/full), чтобы кнопка
33
        // показывала честный размер и тип, а не первый попавшийся asset
34
        foreach ($newReleases as &$release) {
×
NEW
35
            $split = $this->upgrade->splitAssets($release['assets'] ?? []);
×
36
            $asset = $this->upgrade->selectAsset($release['assets'] ?? [], $release['tag_name'] ?? '');
×
37
            $release['asset'] = $asset;
×
38
            $release['is_upgrade'] = $asset && str_ends_with($asset['name'] ?? '', '_upgrade.zip');
×
39
            // Полный архив как запасной вариант — только когда по умолчанию выбран upgrade
NEW
40
            $release['full_asset'] = $release['is_upgrade'] ? $split['full'] : null;
×
41
        }
42
        unset($release);
×
43

44
        // Переустановка текущей версии (скачать тот же релиз заново) — доступна,
45
        // пока текущий релиз ещё в списке последних релизов GitHub
NEW
46
        $reinstall = $this->reinstallInfo($githubService);
×
47

NEW
48
        $permErrors = ($newReleases || $reinstall) ? $this->upgrade->checkPermissions() : [];
×
49

50
        return view('admin/upgrade/index', compact(
×
51
            'pendingMigrations',
×
52
            'newReleases',
×
NEW
53
            'reinstall',
×
54
            'permErrors',
×
55
        ));
×
56
    }
57

58
    /**
59
     * Собирает данные для переустановки текущей версии
60
     */
NEW
61
    private function reinstallInfo(GithubService $githubService): ?array
×
62
    {
NEW
63
        $currentTag = 'v' . ROTOR_VERSION;
×
64

NEW
65
        foreach ($githubService->getLatestReleases() as $release) {
×
NEW
66
            if (($release['tag_name'] ?? null) !== $currentTag || empty($release['assets'])) {
×
NEW
67
                continue;
×
68
            }
69

NEW
70
            $split = $this->upgrade->splitAssets($release['assets']);
×
NEW
71
            $asset = $this->upgrade->selectAsset($release['assets'], $currentTag);
×
72

NEW
73
            if (! $asset) {
×
NEW
74
                return null;
×
75
            }
76

NEW
77
            $isUpgrade = str_ends_with($asset['name'] ?? '', '_upgrade.zip');
×
78

NEW
79
            return [
×
NEW
80
                'tag'        => $currentTag,
×
NEW
81
                'asset'      => $asset,
×
NEW
82
                'full_asset' => $isUpgrade ? $split['full'] : null,
×
NEW
83
            ];
×
84
        }
85

NEW
86
        return null;
×
87
    }
88

89
    /**
90
     * Скачивает и распаковывает архив релиза
91
     */
92
    public function download(Request $request, GithubService $githubService): JsonResponse
×
93
    {
94
        $tag = (string) $request->input('tag');
×
NEW
95
        $full = (bool) $request->input('full');
×
NEW
96
        $asset = $tag ? $this->upgrade->findAsset($githubService, $tag, $full) : null;
×
97

98
        if (! $asset) {
×
99
            return response()->json(['error' => __('admin.upgrade.invalid_params')], 422);
×
100
        }
101

102
        ini_set('max_execution_time', 0);
×
103
        set_time_limit(0);
×
104

105
        try {
106
            $this->upgrade->downloadRelease($tag, $asset['browser_download_url']);
×
107
        } catch (Throwable $e) {
×
108
            return response()->json(['error' => $e->getMessage()], 500);
×
109
        }
110

111
        return response()->json(['success' => true]);
×
112
    }
113

114
    /**
115
     * Применяет скачанное обновление
116
     */
117
    public function apply(Request $request): JsonResponse
×
118
    {
119
        $tag = (string) $request->input('tag');
×
120

121
        if (! $tag) {
×
122
            return response()->json(['error' => __('admin.upgrade.invalid_params')], 422);
×
123
        }
124

125
        // Копирование тысяч файлов на shared-хостинге легко превышает max_execution_time,
126
        // а фатальный таймаут не выполнит finally и оставит сайт в maintenance mode
127
        ini_set('max_execution_time', 0);
×
128
        set_time_limit(0);
×
129
        ignore_user_abort(true);
×
130

131
        Artisan::call('down');
×
132

133
        try {
134
            $errors = $this->upgrade->applyUpdate($tag);
×
135
            $this->upgrade->cleanup($tag);
×
136
        } catch (Throwable $e) {
×
137
            return response()->json(['error' => $e->getMessage()], 500);
×
138
        } finally {
139
            Artisan::call('up');
×
140
        }
141

142
        Artisan::call('cache:clear');
×
143
        Artisan::call('route:clear');
×
144
        Artisan::call('config:clear');
×
145

146
        // Обновление затирает файлы ядра — восстанавливаем симлинки
147
        // и опубликованные файлы активных модулей
148
        Artisan::call('module:sync');
×
149

150
        if (function_exists('opcache_reset')) {
×
151
            opcache_reset();
×
152
        }
153

154
        return response()->json([
×
155
            'success' => true,
×
156
            'errors'  => $errors,
×
157
        ]);
×
158
    }
159

160
    /**
161
     * Выполняет одну следующую миграцию
162
     */
163
    public function migrateNext(): JsonResponse
×
164
    {
165
        ini_set('max_execution_time', 0);
×
166
        set_time_limit(0);
×
167

168
        $pending = $this->migrations->getPendingMigrations($this->migrationPaths());
×
169

170
        if (empty($pending)) {
×
171
            Artisan::call('cache:clear');
×
172
            Artisan::call('route:clear');
×
173
            Artisan::call('config:clear');
×
174

175
            return response()->json(['done' => true, 'migration' => null, 'output' => '']);
×
176
        }
177

178
        $name = $pending[0];
×
179
        $file = $this->migrations->findFile($name);
×
180

181
        if (! $file) {
×
182
            return response()->json(['error' => "Файл миграции не найден: {$name}"], 500);
×
183
        }
184

185
        $remaining = count($pending) - 1;
×
186

187
        try {
188
            $output = $this->migrations->runOne($file);
×
189
        } catch (\Throwable $e) {
×
190
            return response()->json([
×
191
                'error' => "Ошибка миграции {$name}: " . $e->getMessage(),
×
192
            ], 500);
×
193
        }
194

195
        return response()->json([
×
196
            'done'      => $remaining === 0,
×
197
            'migration' => $name,
×
198
            'output'    => $output,
×
199
            'remaining' => $remaining,
×
200
        ]);
×
201
    }
202

203
    /**
204
     * Пути к папкам с миграциями
205
     */
206
    private function migrationPaths(): array
×
207
    {
208
        return [database_path('migrations'), database_path('upgrades')];
×
209
    }
210
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc