diff --git a/app/Containers/VikonIntegration/Actions/UpdateCoreAction.php b/app/Containers/VikonIntegration/Actions/UpdateCoreAction.php index 60b0452..56f8641 100644 --- a/app/Containers/VikonIntegration/Actions/UpdateCoreAction.php +++ b/app/Containers/VikonIntegration/Actions/UpdateCoreAction.php @@ -24,49 +24,203 @@ class UpdateCoreAction $modulePath = $this->basePath . '/' . $config['path']; $tempPath = $this->storagePath . '/temp/' . $config['path']; - try { - Log::info('Vikon: downloading module core', ['module' => $moduleId]); + Log::info('Vikon: starting full update', ['module' => $moduleId]); - if ($moduleId === 2) { - return $this->initAbiturModule($modulePath, $accessToken); + $this->downloadCore($moduleId, $modulePath, $tempPath, $accessToken); + $this->syncFromFM($moduleId, $modulePath, $accessToken); + + File::put($modulePath . '/.vikon', date('Y-m-d H:i:s')); + + Log::info('Vikon: full update complete', ['module' => $config['name']]); + return 'Модуль "' . $config['name'] . '" полностью обновлён.'; + } + + private function downloadCore(int $moduleId, string $modulePath, string $tempPath, string $accessToken): void + { + if ($moduleId === 2) { + $this->initAbiturModule($modulePath, $accessToken); + return; + } + + $zipContent = $this->http->downloadWithToken( + 'pull_updates/generateEmptyModuleCore/' . $moduleId, + $accessToken + ); + + if (File::exists($tempPath)) File::deleteDirectory($tempPath); + File::makeDirectory($tempPath, 0755, true, true); + + $zipFile = $tempPath . '/module.zip'; + file_put_contents($zipFile, $zipContent); + + $this->extractZip($zipFile, $tempPath); + + $blocked = $this->fs->validateFileTypes($tempPath); + if (!empty($blocked)) { + throw new \RuntimeException('Запрещённые файлы: ' . implode(', ', $blocked)); + } + + File::delete($zipFile); + + $extractedDirs = File::directories($tempPath); + $syncSource = $extractedDirs[0] ?? $tempPath; + File::makeDirectory($modulePath, 0755, true, true); + $this->copyFiles($syncSource, $modulePath); + + Log::info('Vikon: core downloaded', ['module' => $moduleId]); + } + + private function syncFromFM(int $moduleId, string $modulePath, string $accessToken): void + { + $filesDir = $modulePath . '/files'; + if (!File::isDirectory($filesDir)) { + File::makeDirectory($filesDir, 0755, true, true); + } + + $dirIds = $this->getUsedDirNames($moduleId, $accessToken); + Log::info('Vikon FM: dir identifiers', ['count' => count($dirIds), 'module' => $moduleId]); + + $allIdentities = $this->getFileIdentitiesFromRoot($moduleId, $accessToken); + Log::info('Vikon FM: root identities', ['count' => count($allIdentities)]); + + foreach ($dirIds as $dirId) { + $ids = $this->getFileIdentitiesFromSubDir($dirId, $moduleId, $accessToken); + $allIdentities = array_merge($allIdentities, $ids); + } + Log::info('Vikon FM: total identities', ['count' => count($allIdentities)]); + + $synced = 0; + foreach ($allIdentities as $identity) { + if ($this->downloadByIdentity($identity, $moduleId, $filesDir, $accessToken)) { + $synced++; } + } - $zipContent = $this->http->downloadWithToken( - 'pull_updates/generateEmptyModuleCore/' . $moduleId, - $accessToken + $newSynced = $this->syncNewFiles($moduleId, $accessToken, $filesDir); + + Log::info('Vikon FM: sync done', ['synced' => $synced, 'new' => $newSynced]); + } + + private function getUsedDirNames(int $moduleId, string $accessToken): array + { + $response = $this->http->getWithToken( + "sync/getUsedDirNamesByModule?moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + return $response->json()['directories'] ?? []; + } + + private function getFileIdentitiesFromRoot(int $moduleId, string $accessToken): array + { + $response = $this->http->getWithToken( + "sync/getFileNamesFromRootDirectoryByModule?moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + $files = $response->json()['files'] ?? []; + return array_map(fn($f) => $f['i'], array_filter($files, fn($f) => !empty($f['i']))); + } + + private function getFileIdentitiesFromSubDir(string $dirId, int $moduleId, string $accessToken): array + { + $response = $this->http->getWithToken( + "sync/getFileNamesFromSubDirectoryByModule?dir={$dirId}&moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + $files = $response->json()['files'] ?? []; + return array_map(fn($f) => $f['i'], array_filter($files, fn($f) => !empty($f['i']))); + } + + private function downloadByIdentity(string $identity, int $moduleId, string $filesDir, string $accessToken): bool + { + try { + $infoResp = $this->http->getWithToken( + "sync/getFileByIdentityInfo?identity={$identity}", + $accessToken, + 'filemanager' + ); + $info = $infoResp->json(); + + if (empty($info['identity']) || empty($info['file_name'])) return false; + + $dirName = $info['dir_name'] ?? null; + $targetDir = $dirName ? $filesDir . '/' . $dirName : $filesDir; + + $content = $this->http->downloadWithToken( + "sync/downloadFileBinaryForSync?identity={$info['identity']}&moduleId={$moduleId}", + $accessToken, + 'filemanager' ); - if (File::exists($tempPath)) File::deleteDirectory($tempPath); - File::makeDirectory($tempPath, 0755, true, true); - - $zipFile = $tempPath . '/module.zip'; - file_put_contents($zipFile, $zipContent); - - $this->extractZip($zipFile, $tempPath); - - $blocked = $this->fs->validateFileTypes($tempPath); - if (!empty($blocked)) { - throw new \RuntimeException( - 'Запрещённые файлы: ' . implode(', ', $blocked) . '. Обновление отклонено.' - ); + if (!File::isDirectory($targetDir)) { + File::makeDirectory($targetDir, 0755, true, true); } - $extractedDirs = File::directories($tempPath); - $syncSource = $extractedDirs[0] ?? $tempPath; - File::makeDirectory($modulePath, 0755, true, true); - $this->syncFiles($syncSource, $modulePath); - File::put($modulePath . '/.vikon', date('Y-m-d H:i:s')); - - Log::info('Vikon: module updated', ['module' => $config['name']]); - return 'Модуль "' . $config['name'] . '" обновлён.'; - + file_put_contents($targetDir . '/' . $info['file_name'], $content); + return true; } catch (\Throwable $e) { - Log::error('Vikon update failed', ['module' => $moduleId, 'error' => $e->getMessage()]); - File::deleteDirectory($tempPath); - throw new \RuntimeException('Ошибка обновления: ' . $e->getMessage()); + Log::warning('Vikon FM: download failed', ['identity' => $identity, 'error' => $e->getMessage()]); + return false; } } + private function syncNewFiles(int $moduleId, string $accessToken, string $filesDir): int + { + $synced = 0; + while (true) { + $response = $this->http->getWithToken( + "sync/getNewFileInfoByModule?moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + $body = $response->json(); + + if (empty($body['file_name']) && empty($body['identity'])) break; + + $identity = $body['identity']; + $filename = $body['file_name']; + $directory = $body['dir_name'] ?? null; + + $targetDir = $directory ? $filesDir . '/' . $directory : $filesDir; + + $content = $this->http->downloadWithToken( + "sync/downloadFileBinary?identity={$identity}", + $accessToken, + 'filemanager' + ); + + if (!File::isDirectory($targetDir)) { + File::makeDirectory($targetDir, 0755, true, true); + } + + file_put_contents($targetDir . '/' . $filename, $content); + + $this->http->getWithToken( + "sync/markNewFileAsLoaded?identity={$identity}&moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + + $synced++; + } + return $synced; + } + + private function initAbiturModule(string $modulePath, string $accessToken): void + { + $response = $this->http->getWithToken('pull_updates/generateEmptyModuleCore/2', $accessToken); + $body = $response->json(); + + if (!isset($body['success']) || $body['success'] !== true) { + throw new \RuntimeException('ABITUR init failed: ' . ($body['message'] ?? 'Unknown')); + } + + File::makeDirectory($modulePath, 0755, true, true); + File::makeDirectory($modulePath . '/files', 0755, true, true); + } + private function extractZip(string $zipPath, string $destination): void { $zip = new ZipArchive; @@ -79,76 +233,33 @@ class UpdateCoreAction for ($i = 0; $i < $zip->numFiles; $i++) { $name = $zip->getNameIndex($i); - - if (str_contains($name, '..')) { - $zip->close(); - throw new \RuntimeException("Zip Slip: {$name}"); - } - + if (str_contains($name, '..')) { $zip->close(); throw new \RuntimeException("Zip Slip: {$name}"); } $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); - if (in_array($ext, $blocked, true)) { - $zip->close(); - throw new \RuntimeException("Blocked executable: {$name}"); - } - + if (in_array($ext, $blocked, true)) { $zip->close(); throw new \RuntimeException("Blocked: {$name}"); } $full = realpath($realDest . '/' . $name); - if ($full !== false && !str_starts_with($full, $realDest)) { - $zip->close(); - throw new \RuntimeException("Path escape: {$name}"); - } + if ($full !== false && !str_starts_with($full, $realDest)) { $zip->close(); throw new \RuntimeException("Escape: {$name}"); } } $zip->extractTo($destination); $zip->close(); } - private function syncFiles(string $source, string $target): void + private function copyFiles(string $source, string $target): void { File::makeDirectory($target, 0755, true, true); foreach (File::files($source) as $file) { - $name = $file->getFilename(); - $targetPath = $target . '/' . $name; - rename($file->getPathname(), $targetPath); + rename($file->getPathname(), $target . '/' . $file->getFilename()); } foreach (File::directories($source) as $dir) { $name = basename($dir); $targetPath = $target . '/' . $name; - if (!File::isDirectory($targetPath)) { rename($dir, $targetPath); } else { - $this->syncFiles($dir, $targetPath); + $this->copyFiles($dir, $targetPath); } } } - - private function initAbiturModule(string $modulePath, string $accessToken): string - { - $response = $this->http->getWithToken( - 'pull_updates/generateEmptyModuleCore/2', - $accessToken - ); - $body = $response->json(); - - if (!isset($body['success']) || $body['success'] !== true) { - throw new \RuntimeException('ABITUR init failed: ' . ($body['message'] ?? 'Unknown error')); - } - - if (!File::isDirectory($modulePath)) { - File::makeDirectory($modulePath, 0755, true, true); - } - if (!File::isDirectory($modulePath . '/files')) { - File::makeDirectory($modulePath . '/files', 0755, true, true); - } - File::put($modulePath . '/.vikon', date('Y-m-d H:i:s')); - - $tempPath = $this->storagePath . '/temp/abitur'; - File::makeDirectory($tempPath, 0755, true, true); - File::put($tempPath . '/api_response.json', json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - - Log::info('Vikon: ABITUR module initialized'); - return 'Модуль "Абитуриент" инициализирован.'; - } } diff --git a/resources/js/Pages/Dashboard/VikonUpdates/Index.vue b/resources/js/Pages/Dashboard/VikonUpdates/Index.vue index 5d7ecd1..0649705 100644 --- a/resources/js/Pages/Dashboard/VikonUpdates/Index.vue +++ b/resources/js/Pages/Dashboard/VikonUpdates/Index.vue @@ -79,10 +79,6 @@ class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50"> {{ updating ? 'Обновление...' : 'Обновить' }} - @@ -139,7 +135,6 @@ const currentVersion = ref(props.current_version); const checkingVersion = ref(false); const checkingAccess = ref(false); const updating = ref(false); -const syncing = ref(false); const progress = ref(0); const updateError = ref(null); const versionInfo = ref({ current_version: props.current_version, has_update: false, latest_version: null }); @@ -247,24 +242,4 @@ async function logout() { console.error('Logout failed:', e); } } - -async function syncModuleFiles(moduleId) { - if (!confirm('Синхронизировать файлы модуля?')) return; - - syncing.value = true; - updateError.value = null; - - try { - const res = await axios.post(route('dashboard.vikon-updates.sync-files'), { module_id: moduleId }); - syncing.value = false; - if (res.data.success) { - alert(res.data.message); - } else { - updateError.value = res.data.message; - } - } catch (e) { - syncing.value = false; - updateError.value = e.response?.data?.message || 'Ошибка синхронизации'; - } -}