Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 153
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
AdministratorApiDeployLogic
0.00% covered (danger)
0.00%
0 / 153
0.00% covered (danger)
0.00%
0 / 13
1806
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProgressFilePath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getArchiveFilePath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUploadDirectoryPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExpandDirectoryPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProgressSetting
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setProgressSetting
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 executeStartup
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 executeUpload
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 executePrepare
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
30
 executeUpdate
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
12
 validateImpl
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
210
 executeImpl
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
LocalProgressSetting
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\App\Models\Domain\Api\AdministratorApi;
6
7use DateInterval;
8use DateTime;
9use Exception;
10use ZipArchive;
11use PeServer\App\Models\AppConfiguration;
12use PeServer\App\Models\AppDatabaseConnection;
13use PeServer\App\Models\AuditLog;
14use PeServer\App\Models\Domain\Api\ApiLogicBase;
15use PeServer\App\Models\Domain\AppArchiver;
16use PeServer\App\Models\ResponseJson;
17use PeServer\App\Models\Setup\SetupRunner;
18use PeServer\Core\Binary;
19use PeServer\Core\Collection\Arr;
20use PeServer\Core\IO\Directory;
21use PeServer\Core\IO\File;
22use PeServer\Core\IO\IOUtility;
23use PeServer\Core\IO\Path;
24use PeServer\Core\Log\ILoggerFactory;
25use PeServer\Core\Mvc\LogicCallMode;
26use PeServer\Core\Mvc\LogicParameter;
27use PeServer\Core\ProgramContext;
28use PeServer\Core\Serialization\BuiltinSerializer;
29use PeServer\Core\Serialization\JsonSerializer;
30use PeServer\Core\Text;
31use PeServer\Core\Throws\InvalidOperationException;
32use PeServer\Core\Throws\NotImplementedException;
33use PeServer\Core\Utc;
34
35class AdministratorApiDeployLogic extends ApiLogicBase
36{
37    #region define
38
39    private const MODE_STARTUP = 'startup';
40    private const MODE_UPLOAD = 'upload';
41    private const MODE_PREPARE = 'prepare';
42    private const MODE_UPDATE = 'update';
43
44    public const PROGRESS_FILE_NAME = 'deploy.dat';
45    private const ARCHIVE_FILE_NAME = 'deploy.zip';
46    private const ENABLED_RANGE_TIME = 'P1M';
47
48    #endregion
49
50    public function __construct(
51        LogicParameter $parameter,
52        private ProgramContext $programContext,
53        private AppConfiguration $appConfig,
54        private AppArchiver $appArchiver,
55        private AppDatabaseConnection $databaseConnection,
56        private ILoggerFactory $loggerFactory
57    ) {
58        parent::__construct($parameter);
59    }
60
61    #region function
62
63    private function getProgressFilePath(): string
64    {
65        return Path::combine($this->appConfig->setting->cache->deploy, self::PROGRESS_FILE_NAME);
66    }
67
68    private function getArchiveFilePath(): string
69    {
70        return Path::combine($this->appConfig->setting->cache->deploy, self::ARCHIVE_FILE_NAME);
71    }
72
73    private function getUploadDirectoryPath(): string
74    {
75        return Path::combine($this->appConfig->setting->cache->deploy, 'upload');
76    }
77
78    private function getExpandDirectoryPath(): string
79    {
80        return Path::combine($this->appConfig->setting->cache->deploy, 'expand');
81    }
82
83    private function getProgressSetting(): LocalProgressSetting
84    {
85        $path = $this->getProgressFilePath();
86        $binary = File::readContent($path);
87        $serializer = new BuiltinSerializer();
88        /** @var LocalProgressSetting */
89        return $serializer->load($binary);
90    }
91
92    private function setProgressSetting(LocalProgressSetting $setting): void
93    {
94        $path = $this->getProgressFilePath();
95        $serializer = new BuiltinSerializer();
96        $binary = $serializer->save($setting);
97        Directory::createParentDirectoryIfNotExists($path);
98        File::writeContent($path, $binary);
99    }
100
101    /**
102     * [デプロイ] 開始の合図。
103     *
104     * @return array<string,string>
105     */
106    private function executeStartup(): array
107    {
108        $setting = new LocalProgressSetting();
109        $setting->mode = self::MODE_STARTUP;
110        $setting->uploadedCount = 0;
111        $setting->startupTime = Utc::createDateTime();
112
113        $dirs = [
114            $this->getUploadDirectoryPath(),
115            $this->getExpandDirectoryPath(),
116        ];
117        foreach ($dirs as $dir) {
118            if (Directory::exists($dir)) {
119                Directory::removeDirectory($dir, true);
120            }
121        }
122
123        $files = [
124            $this->getArchiveFilePath(),
125        ];
126        foreach ($files as $file) {
127            if (File::exists($file)) {
128                File::removeFile($file);
129            }
130        }
131
132        $this->setProgressSetting($setting);
133
134        return [];
135    }
136
137    /**
138     * [デプロイ] アップロード処理。
139     *
140     * @return array<string,string>
141     */
142    private function executeUpload(): array
143    {
144        $sequence = $this->getRequest('sequence');
145        if (Text::isNullOrWhiteSpace($sequence)) {
146            throw new InvalidOperationException('$sequence');
147        }
148
149        $file = $this->getFile('file');
150        $fileName = "upload-$sequence.dat";
151        $filePath = Path::combine($this->getUploadDirectoryPath(), $fileName);
152        $file->move($filePath);
153
154        $setting = $this->getProgressSetting();
155        $setting->mode = self::MODE_UPLOAD;
156        $setting->uploadedCount += 1;
157        $this->setProgressSetting($setting);
158
159        return [
160            'sequence' => $sequence,
161        ];
162    }
163
164    /**
165     * [デプロイ] 展開処理。
166     *
167     * @return array<string,string>
168     */
169    private function executePrepare(): array
170    {
171        $archiveFilePath = $this->getArchiveFilePath();
172        File::removeFileIfExists($archiveFilePath);
173        File::createEmptyFileIfNotExists($archiveFilePath);
174
175        $setting = $this->getProgressSetting();
176        for ($i = 0; $i < $setting->uploadedCount; $i++) {
177            $uploadFileName = "upload-$i.dat";
178            $uploadFilePath = Path::combine($this->getUploadDirectoryPath(), $uploadFileName);
179            $content = File::readContent($uploadFilePath);
180            File::appendContent($archiveFilePath, $content);
181        }
182
183        $archiveFilePath = $this->getArchiveFilePath();
184        if (!File::exists($archiveFilePath)) {
185            throw new InvalidOperationException($archiveFilePath);
186        }
187
188        $expandDirPath = $this->getExpandDirectoryPath();
189        if (Directory::exists($expandDirPath)) {
190            Directory::removeDirectory($expandDirPath, true);
191        }
192        Directory::createDirectory($expandDirPath);
193
194        // 展開
195        $archiveFilePath = $this->getArchiveFilePath();
196        $zip = new ZipArchive();
197        $zip->open($archiveFilePath);
198        $expandDirPath = $this->getExpandDirectoryPath();
199        $zip->extractTo($expandDirPath);
200        $zip->close();
201
202        $expandFilePaths = Directory::getFiles($expandDirPath, true);
203        foreach ($expandFilePaths as $expandFilePath) {
204            $this->logger->info('{0}: {1}', $expandFilePath, File::getFileSize($expandFilePath));
205        }
206
207        $setting->mode = self::MODE_PREPARE;
208        $this->setProgressSetting($setting);
209
210        return [
211            'count' => (string)$setting->uploadedCount,
212            'size' => (string)File::getFileSize($archiveFilePath),
213        ];
214    }
215
216    /**
217     * [デプロイ] 最終処理。
218     *
219     * @return array<string,string>
220     */
221    private function executeUpdate(): array
222    {
223        $this->logger->info('各種データバックアップ');
224        $this->appArchiver->backup();
225        $this->appArchiver->rotate();
226
227        // 今のソースでDB開いとく(使わんけど)
228        $database = $this->openDatabase();
229
230        $this->logger->info('展開ファイルを公開ディレクトリに配置');
231        $dirs = [];
232        $expandDirPath = $this->getExpandDirectoryPath();
233        $expandFilePaths = Directory::getFiles($expandDirPath, true);
234        foreach ($expandFilePaths as $expandFilePath) {
235            $basePath = Text::substring($expandFilePath, Text::getLength($expandDirPath) + 1);
236            $toPath = Path::combine($this->programContext->rootDirectory, $basePath);
237
238            //$this->logger->info('UPDATE: {0}', $toPath);
239
240            $parentPath = Path::getDirectoryPath($toPath);
241            if (!isset($dirs[$parentPath])) {
242                Directory::createDirectoryIfNotExists($parentPath);
243                $dirs[$parentPath] = true;
244            }
245            File::copy($expandFilePath, $toPath);
246        }
247
248        $this->logger->info('各種マイグレーション実施');
249        $setupRunner = new SetupRunner(
250            $this->databaseConnection,
251            $this->appConfig,
252            $this->loggerFactory
253        );
254        $setupRunner->execute();
255
256        $this->logger->info('デプロイ進捗ファイル破棄');
257        $progressFilePath = Path::combine($this->appConfig->setting->cache->deploy, AdministratorApiDeployLogic::PROGRESS_FILE_NAME);
258        File::removeFile($progressFilePath);
259
260        return [
261            'success' => (string)true,
262        ];
263    }
264
265    #endregion
266
267    #region ApiLogicBase
268
269    protected function validateImpl(LogicCallMode $callMode): void
270    {
271        $mode = $this->getRequest('mode');
272        $modeIsEnabled = Arr::in([
273            self::MODE_STARTUP,
274            self::MODE_UPLOAD,
275            self::MODE_PREPARE,
276            self::MODE_UPDATE,
277        ], $mode);
278
279        if (!$modeIsEnabled) {
280            throw new Exception('$mode: ' . $mode);
281        }
282
283        if (File::exists($this->getProgressFilePath())) {
284            $setting = $this->getProgressSetting();
285            $current = Utc::createDateTime();
286            $limit = $setting->startupTime->add(new DateInterval(self::ENABLED_RANGE_TIME));
287            if ($limit < $current) {
288                $this->logger->error('進捗状況 時間制限: $current {0} < $limit {1}', Utc::toString($current), Utc::toString($limit));
289                throw new Exception('$limit: ' . Utc::toString($limit));
290            }
291
292            switch ($mode) {
293                case self::MODE_STARTUP:
294                    $this->logger->info('進捗ファイルは無視');
295                    break;
296
297                case self::MODE_UPLOAD:
298                    if (!($setting->mode === self::MODE_STARTUP || $setting->mode === self::MODE_UPLOAD)) {
299                        $this->logger->error('進捗状況不整合: {0}', $setting->mode);
300                        throw new Exception('$setting->mode: ' . $setting->mode);
301                    }
302                    break;
303
304                case self::MODE_PREPARE:
305                    if ($setting->mode !== self::MODE_UPLOAD) {
306                        $this->logger->error('進捗状況不整合: {0}', $setting->mode);
307                        throw new Exception('$setting->mode: ' . $setting->mode);
308                    }
309                    break;
310
311                case self::MODE_UPDATE:
312                    if ($setting->mode !== self::MODE_PREPARE) {
313                        $this->logger->error('進捗状況不整合: {0}', $setting->mode);
314                        throw new Exception('$setting->mode: ' . $setting->mode);
315                    }
316                    break;
317
318                default:
319                    throw new NotImplementedException();
320            }
321        } elseif ($mode !== self::MODE_STARTUP) {
322            throw new Exception('$mode: ' . $mode . ', not found progress -> ' . $this->getProgressFilePath());
323        }
324    }
325
326    protected function executeImpl(LogicCallMode $callMode): void
327    {
328        $mode = $this->getRequest('mode');
329
330        $result = match ($mode) {
331            self::MODE_STARTUP => $this->executeStartup(),
332            self::MODE_UPLOAD => $this->executeUpload(),
333            self::MODE_PREPARE => $this->executePrepare(),
334            self::MODE_UPDATE => $this->executeUpdate(),
335            default => throw new Exception()
336        };
337
338        $this->setResponseJson(ResponseJson::success($result));
339    }
340
341    #endregion
342}
343
344//phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses
345class LocalProgressSetting
346{
347    #region variable
348
349    public DateTime $startupTime;
350    public string $mode;
351    public int $uploadedCount;
352
353    #endregion
354}