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

visavi / rotor / 26745210954

01 Jun 2026 08:57AM UTC coverage: 13.694% (-0.07%) from 13.763%
26745210954

push

github

visavi
Добавлено удаление файлов которых нет в архиве при обновлении

0 of 31 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

790 of 5769 relevant lines covered (13.69%)

1.21 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 FilesystemIterator;
8
use Illuminate\Support\Facades\Http;
9
use RecursiveDirectoryIterator;
10
use RecursiveIteratorIterator;
11
use RuntimeException;
12
use ZipArchive;
13

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

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

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

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

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

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

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

56
        return $failed;
×
57
    }
58

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

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

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

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

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

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

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

85
        @unlink($zipPath);
×
86
    }
87

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

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

NEW
96
        $archiveFiles = $this->collectFiles($sourcePath);
×
97

98
        $errors = [];
×
99
        $this->copyDirectory($sourcePath, base_path(), $errors);
×
NEW
100
        $this->deleteOrphans($archiveFiles);
×
101

102
        return $errors;
×
103
    }
104

105
    public function cleanup(string $tag): void
×
106
    {
107
        $zip = $this->zipPath($tag);
×
108
        $dir = storage_path('app/temp/update-' . $tag);
×
109

110
        if (file_exists($zip)) {
×
111
            unlink($zip);
×
112
        }
113

114
        if (is_dir($dir)) {
×
115
            $this->deleteDirectory($dir);
×
116
        }
117
    }
118

119
    public function sourcePath(string $tag): string
×
120
    {
121
        $base = storage_path('app/temp/update-' . $tag);
×
122

123
        if (! is_dir($base)) {
×
124
            return $base;
×
125
        }
126

127
        // If ZIP had single root dir (e.g. rotor-14.0.0/), use that
128
        $dirs = array_filter((array) glob($base . '/*'), 'is_dir');
×
129
        $files = array_filter((array) glob($base . '/*'), 'is_file');
×
130

131
        if (count($dirs) === 1 && empty($files)) {
×
132
            return array_values($dirs)[0];
×
133
        }
134

135
        return $base;
×
136
    }
137

138
    private function zipPath(string $tag): string
×
139
    {
140
        return storage_path('app/temp/update-' . $tag . '.zip');
×
141
    }
142

143
    private function extractRelease(string $zipPath, string $tag): void
×
144
    {
145
        $extractPath = storage_path('app/temp/update-' . $tag);
×
146

147
        if (is_dir($extractPath)) {
×
148
            $this->deleteDirectory($extractPath);
×
149
        }
150

151
        mkdir($extractPath, 0755, true);
×
152

153
        $zip = new ZipArchive();
×
154

155
        if ($zip->open($zipPath) !== true) {
×
156
            throw new RuntimeException('Failed to open ZIP archive');
×
157
        }
158

159
        $zip->extractTo($extractPath);
×
160
        $zip->close();
×
161
    }
162

163
    private function copyDirectory(string $src, string $dst, array &$errors): void
×
164
    {
165
        $iterator = new RecursiveIteratorIterator(
×
NEW
166
            new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS),
×
167
            RecursiveIteratorIterator::SELF_FIRST
×
168
        );
×
169

170
        foreach ($iterator as $item) {
×
171
            $relative = substr($item->getPathname(), strlen($src) + 1);
×
172
            $relative = str_replace('\\', '/', $relative);
×
173

174
            if ($this->isExcluded($relative)) {
×
175
                continue;
×
176
            }
177

178
            $dest = $dst . DIRECTORY_SEPARATOR . $relative;
×
179

180
            if ($item->isDir()) {
×
181
                if (! is_dir($dest)) {
×
182
                    mkdir($dest, 0755, true);
×
183
                }
184
            } elseif (! @copy($item->getPathname(), $dest)) {
×
185
                $errors[] = $relative;
×
186
            }
187
        }
188
    }
189

190
    /**
191
     * Собирает список относительных путей всех файлов в директории
192
     */
NEW
193
    private function collectFiles(string $path): array
×
194
    {
NEW
195
        $files = [];
×
NEW
196
        $iterator = new RecursiveIteratorIterator(
×
NEW
197
            new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)
×
NEW
198
        );
×
199

NEW
200
        foreach ($iterator as $item) {
×
NEW
201
            if ($item->isFile()) {
×
NEW
202
                $relative = substr($item->getPathname(), strlen($path) + 1);
×
NEW
203
                $files[] = str_replace('\\', '/', $relative);
×
204
            }
205
        }
206

NEW
207
        return $files;
×
208
    }
209

210
    /**
211
     * Удаляет файлы в управляемых директориях которых нет в архиве
212
     */
NEW
213
    private function deleteOrphans(array $archiveFiles): void
×
214
    {
NEW
215
        $archiveSet = array_flip($archiveFiles);
×
216

NEW
217
        foreach ($this->writableDirs as $dir) {
×
NEW
218
            $dirPath = base_path($dir);
×
219

NEW
220
            if (! is_dir($dirPath)) {
×
NEW
221
                continue;
×
222
            }
223

NEW
224
            $iterator = new RecursiveIteratorIterator(
×
NEW
225
                new RecursiveDirectoryIterator($dirPath, FilesystemIterator::SKIP_DOTS)
×
NEW
226
            );
×
227

NEW
228
            foreach ($iterator as $item) {
×
NEW
229
                if (! $item->isFile()) {
×
NEW
230
                    continue;
×
231
                }
232

NEW
233
                $relative = str_replace('\\', '/', substr($item->getPathname(), strlen(base_path()) + 1));
×
234

NEW
235
                if ($this->isExcluded($relative)) {
×
NEW
236
                    continue;
×
237
                }
238

NEW
239
                if (! isset($archiveSet[$relative])) {
×
NEW
240
                    @unlink($item->getPathname());
×
241
                }
242
            }
243
        }
244
    }
245

UNCOV
246
    private function isExcluded(string $relative): bool
×
247
    {
248
        foreach ($this->excluded as $exclude) {
×
249
            if ($relative === $exclude || str_starts_with($relative, $exclude . '/')) {
×
250
                return true;
×
251
            }
252
        }
253

254
        return false;
×
255
    }
256

257
    private function deleteDirectory(string $path): void
×
258
    {
259
        $items = new RecursiveIteratorIterator(
×
NEW
260
            new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
×
261
            RecursiveIteratorIterator::CHILD_FIRST
×
262
        );
×
263

264
        foreach ($items as $item) {
×
265
            $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
×
266
        }
267

268
        rmdir($path);
×
269
    }
270
}
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