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

visavi / rotor / 26729174885

01 Jun 2026 12:38AM UTC coverage: 13.763% (-0.3%) from 14.072%
26729174885

push

github

visavi
Добавлено обновление ядра движка

1 of 131 new or added lines in 2 files covered. (0.76%)

3 existing lines in 1 file now uncovered.

790 of 5740 relevant lines covered (13.76%)

1.22 hits per line

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

0.0
/app/Services/UpgradeService.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Services;
6

7
use Illuminate\Support\Facades\Http;
8
use RecursiveDirectoryIterator;
9
use RecursiveIteratorIterator;
10
use RuntimeException;
11
use ZipArchive;
12

13
class UpgradeService
14
{
15
    private array $excluded = [
16
        '.env',
17
        'storage',
18
        'public/uploads',
19
        'modules',
20
        'app/hooks.php',
21
    ];
22

23
    private array $writableDirs = [
24
        'app',
25
        'bootstrap',
26
        'database',
27
        'public/assets',
28
        'resources',
29
        'routes',
30
        'vendor',
31
    ];
32

NEW
33
    public function getNewReleases(GithubService $github): array
×
34
    {
NEW
35
        return array_values(array_filter($github->getLatestReleases(), function (array $release) {
×
NEW
36
            $version = ltrim($release['tag_name'] ?? '', 'v');
×
NEW
37
            $version = str_replace(['-alpha', '-beta', '-rc'], '', $version);
×
38

NEW
39
            return version_compare(ROTOR_VERSION, $version, '<') && ! empty($release['assets']);
×
NEW
40
        }));
×
41
    }
42

NEW
43
    public function checkPermissions(): array
×
44
    {
NEW
45
        $failed = [];
×
46

NEW
47
        foreach ($this->writableDirs as $dir) {
×
NEW
48
            $path = base_path($dir);
×
49

NEW
50
            if (file_exists($path) && ! is_writable($path)) {
×
NEW
51
                $failed[] = $dir;
×
52
            }
53
        }
54

NEW
55
        return $failed;
×
56
    }
57

NEW
58
    public function isDownloaded(string $tag): bool
×
59
    {
NEW
60
        return is_dir(storage_path('app/temp/update-' . $tag));
×
61
    }
62

NEW
63
    public function downloadRelease(string $tag, string $url): void
×
64
    {
NEW
65
        $tempDir = storage_path('app/temp');
×
66

NEW
67
        if (! is_dir($tempDir)) {
×
NEW
68
            mkdir($tempDir, 0755, true);
×
69
        }
70

NEW
71
        $zipPath = $this->zipPath($tag);
×
72

NEW
73
        $response = Http::withOptions(['sink' => $zipPath])
×
NEW
74
            ->timeout(300)
×
NEW
75
            ->get($url);
×
76

NEW
77
        if ($response->failed()) {
×
NEW
78
            @unlink($zipPath);
×
NEW
79
            throw new RuntimeException('Download failed: HTTP ' . $response->status());
×
80
        }
81

NEW
82
        $this->extractRelease($zipPath, $tag);
×
83

NEW
84
        @unlink($zipPath);
×
85
    }
86

NEW
87
    public function applyUpdate(string $tag): array
×
88
    {
NEW
89
        $sourcePath = $this->sourcePath($tag);
×
90

NEW
91
        if (! is_dir($sourcePath)) {
×
NEW
92
            throw new RuntimeException('Update not downloaded');
×
93
        }
94

NEW
95
        $errors = [];
×
NEW
96
        $this->copyDirectory($sourcePath, base_path(), $errors);
×
97

NEW
98
        return $errors;
×
99
    }
100

NEW
101
    public function cleanup(string $tag): void
×
102
    {
NEW
103
        $zip = $this->zipPath($tag);
×
NEW
104
        $dir = storage_path('app/temp/update-' . $tag);
×
105

NEW
106
        if (file_exists($zip)) {
×
NEW
107
            unlink($zip);
×
108
        }
109

NEW
110
        if (is_dir($dir)) {
×
NEW
111
            $this->deleteDirectory($dir);
×
112
        }
113
    }
114

NEW
115
    public function sourcePath(string $tag): string
×
116
    {
NEW
117
        $base = storage_path('app/temp/update-' . $tag);
×
118

NEW
119
        if (! is_dir($base)) {
×
NEW
120
            return $base;
×
121
        }
122

123
        // If ZIP had single root dir (e.g. rotor-14.0.0/), use that
NEW
124
        $dirs = array_filter((array) glob($base . '/*'), 'is_dir');
×
NEW
125
        $files = array_filter((array) glob($base . '/*'), 'is_file');
×
126

NEW
127
        if (count($dirs) === 1 && empty($files)) {
×
NEW
128
            return array_values($dirs)[0];
×
129
        }
130

NEW
131
        return $base;
×
132
    }
133

NEW
134
    private function zipPath(string $tag): string
×
135
    {
NEW
136
        return storage_path('app/temp/update-' . $tag . '.zip');
×
137
    }
138

NEW
139
    private function extractRelease(string $zipPath, string $tag): void
×
140
    {
NEW
141
        $extractPath = storage_path('app/temp/update-' . $tag);
×
142

NEW
143
        if (is_dir($extractPath)) {
×
NEW
144
            $this->deleteDirectory($extractPath);
×
145
        }
146

NEW
147
        mkdir($extractPath, 0755, true);
×
148

NEW
149
        $zip = new ZipArchive();
×
150

NEW
151
        if ($zip->open($zipPath) !== true) {
×
NEW
152
            throw new RuntimeException('Failed to open ZIP archive');
×
153
        }
154

NEW
155
        $zip->extractTo($extractPath);
×
NEW
156
        $zip->close();
×
157
    }
158

NEW
159
    private function copyDirectory(string $src, string $dst, array &$errors): void
×
160
    {
NEW
161
        $iterator = new RecursiveIteratorIterator(
×
NEW
162
            new RecursiveDirectoryIterator($src, RecursiveDirectoryIterator::SKIP_DOTS),
×
NEW
163
            RecursiveIteratorIterator::SELF_FIRST
×
NEW
164
        );
×
165

NEW
166
        foreach ($iterator as $item) {
×
NEW
167
            $relative = substr($item->getPathname(), strlen($src) + 1);
×
NEW
168
            $relative = str_replace('\\', '/', $relative);
×
169

NEW
170
            if ($this->isExcluded($relative)) {
×
NEW
171
                continue;
×
172
            }
173

NEW
174
            $dest = $dst . DIRECTORY_SEPARATOR . $relative;
×
175

NEW
176
            if ($item->isDir()) {
×
NEW
177
                if (! is_dir($dest)) {
×
NEW
178
                    mkdir($dest, 0755, true);
×
179
                }
NEW
180
            } elseif (! @copy($item->getPathname(), $dest)) {
×
NEW
181
                $errors[] = $relative;
×
182
            }
183
        }
184
    }
185

NEW
186
    private function isExcluded(string $relative): bool
×
187
    {
NEW
188
        foreach ($this->excluded as $exclude) {
×
NEW
189
            if ($relative === $exclude || str_starts_with($relative, $exclude . '/')) {
×
NEW
190
                return true;
×
191
            }
192
        }
193

NEW
194
        return false;
×
195
    }
196

NEW
197
    private function deleteDirectory(string $path): void
×
198
    {
NEW
199
        $items = new RecursiveIteratorIterator(
×
NEW
200
            new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
×
NEW
201
            RecursiveIteratorIterator::CHILD_FIRST
×
NEW
202
        );
×
203

NEW
204
        foreach ($items as $item) {
×
NEW
205
            $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
×
206
        }
207

NEW
208
        rmdir($path);
×
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