From 8e5f26da8dc1916d685e6e94e432c52b12c7a960 Mon Sep 17 00:00:00 2001 From: F4ilji Date: Sat, 4 Jul 2026 20:41:49 +0500 Subject: [PATCH] feat(vikon): add file sync from VIKON file manager (FM) --- .../Actions/SyncFilesAction.php | 245 ++++++++++++++++++ .../Providers/VikonServiceProvider.php | 6 + .../UI/WEB/Controllers/VikonController.php | 18 ++ .../VikonIntegration/UI/WEB/Routes/web.php | 1 + .../js/Pages/Dashboard/VikonUpdates/Index.vue | 25 ++ 5 files changed, 295 insertions(+) create mode 100644 app/Containers/VikonIntegration/Actions/SyncFilesAction.php diff --git a/app/Containers/VikonIntegration/Actions/SyncFilesAction.php b/app/Containers/VikonIntegration/Actions/SyncFilesAction.php new file mode 100644 index 0000000..6034b66 --- /dev/null +++ b/app/Containers/VikonIntegration/Actions/SyncFilesAction.php @@ -0,0 +1,245 @@ +publicPath . '/' . $this->getModuleName($moduleId); + + Log::info('Vikon FM: starting file sync', ['module' => $moduleId]); + + $dirs = $this->getUsedDirNames($moduleId, $accessToken); + $this->cleanupRemovedDirs($modulePath, $dirs); + + $filesDir = $modulePath . '/files'; + if (!File::isDirectory($filesDir)) { + File::makeDirectory($filesDir, 0755, true, true); + } + + $synced = 0; + + $synced += $this->syncRootDir($moduleId, $accessToken, $filesDir, $dirs); + + foreach ($dirs as $dir) { + if ($dir === 'files') continue; + $synced += $this->syncSubDir($dir, $moduleId, $accessToken, $filesDir); + } + + $synced += $this->syncNewFiles($moduleId, $accessToken, $filesDir); + + Log::info('Vikon FM: sync complete', ['module' => $moduleId, 'synced' => $synced]); + return "Синхронизировано файлов: {$synced}"; + } + + private function getModuleName(int $moduleId): string + { + return match ($moduleId) { + 1 => 'sveden', + 2 => 'abitur', + 6 => 'vsoko', + default => throw new \RuntimeException("Unknown module: {$moduleId}"), + }; + } + + private function getUsedDirNames(int $moduleId, string $accessToken): array + { + $response = $this->http->getWithToken( + "sync/getUsedDirNamesByModule?moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + + $body = $response->json(); + + if (!isset($body['directories']) || !is_array($body['directories'])) { + return []; + } + + return $body['directories']; + } + + private function cleanupRemovedDirs(string $modulePath, array $knownDirs): void + { + $filesDir = $modulePath . '/files'; + if (!File::isDirectory($filesDir)) return; + + foreach (File::directories($filesDir) as $dir) { + $name = basename($dir); + if (!in_array($name, $knownDirs) && !is_link($dir)) { + File::deleteDirectory($dir); + } + } + } + + private function syncRootDir(int $moduleId, string $accessToken, string $filesDir, array $knownDirs): int + { + $response = $this->http->getWithToken( + "sync/getFileNamesFromRootDirectoryByModule?moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + + $body = $response->json(); + if (!isset($body['files']) || !is_array($body['files'])) { + return 0; + } + + $filesByIdentity = []; + foreach ($body['files'] as $file) { + $filesByIdentity[$file['n']] = $file['i']; + } + + $existingItems = []; + foreach (File::allFiles($filesDir) as $file) { + $name = $file->getFilename(); + $relative = str_replace($filesDir . '/', '', $file->getPathname()); + $existingItems[$relative] = $relative; + } + + foreach ($existingItems as $relative) { + $name = basename($relative); + if (!isset($filesByIdentity[$name]) && !is_dir($filesDir . '/' . $name)) { + @unlink($filesDir . '/' . $relative); + } + } + + $synced = 0; + foreach ($filesByIdentity as $fileName => $identity) { + $filePath = $filesDir . '/' . $fileName; + if (!File::exists($filePath) || filesize($filePath) === 0) { + $this->downloadFile($identity, $moduleId, $filePath, $accessToken); + $synced++; + } + } + + return $synced; + } + + private function syncSubDir(string $dir, int $moduleId, string $accessToken, string $filesDir): int + { + $response = $this->http->getWithToken( + "sync/getFileNamesFromSubDirectoryByModule?dir={$dir}&moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + + $body = $response->json(); + if (!isset($body['files']) || !is_array($body['files'])) { + return 0; + } + + $filesByIdentity = []; + foreach ($body['files'] as $file) { + $filesByIdentity[$file['n']] = $file['i']; + } + + $dirPath = $filesDir . '/' . $dir; + if (!File::isDirectory($dirPath)) { + File::makeDirectory($dirPath, 0775, true, true); + } + + $existingItems = []; + foreach (File::files($dirPath) as $file) { + $existingItems[$file->getFilename()] = true; + } + + foreach ($existingItems as $name => $_) { + if (!isset($filesByIdentity[$name])) { + @unlink($dirPath . '/' . $name); + } + } + + $synced = 0; + foreach ($filesByIdentity as $fileName => $identity) { + $filePath = $dirPath . '/' . $fileName; + if (!File::exists($filePath) || filesize($filePath) === 0) { + $this->downloadFile($identity, $moduleId, $filePath, $accessToken); + $synced++; + } + } + + return $synced; + } + + 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 (($body['file_name'] ?? null) === null && ($body['identity'] ?? null) === null) { + break; + } + + $identity = $body['identity']; + $filename = $body['file_name']; + $directory = $body['dir_name'] ?? null; + + $targetDir = $filesDir; + if ($directory !== null) { + $targetDir = $filesDir . '/' . $directory; + if (!File::isDirectory($targetDir)) { + File::makeDirectory($targetDir, 0775, true, true); + } + } + + $filePath = $targetDir . '/' . $filename; + $this->downloadFile($identity, $moduleId, $filePath, $accessToken); + + $this->http->getWithToken( + "sync/markNewFileAsLoaded?identity={$identity}&moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + + $synced++; + } + + return $synced; + } + + private function downloadFile(string $identity, int $moduleId, string $targetPath, string $accessToken): void + { + $response = $this->http->getWithToken( + "sync/getFileByIdentityInfo?identity={$identity}", + $accessToken, + 'filemanager' + ); + + $info = $response->json(); + if (!isset($info['file_name'], $info['identity'])) { + return; + } + + $content = $this->http->downloadWithToken( + "sync/downloadFileBinaryForSync?identity={$info['identity']}&moduleId={$moduleId}", + $accessToken, + 'filemanager' + ); + + $dir = dirname($targetPath); + if (!File::isDirectory($dir)) { + File::makeDirectory($dir, 0755, true, true); + } + + file_put_contents($targetPath, $content); + } +} diff --git a/app/Containers/VikonIntegration/Providers/VikonServiceProvider.php b/app/Containers/VikonIntegration/Providers/VikonServiceProvider.php index 395b97f..5d1d183 100644 --- a/app/Containers/VikonIntegration/Providers/VikonServiceProvider.php +++ b/app/Containers/VikonIntegration/Providers/VikonServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Containers\VikonIntegration\Providers; use App\Containers\VikonIntegration\Actions\Auth\AuthenticateAction; use App\Containers\VikonIntegration\Actions\CheckAccessAction; use App\Containers\VikonIntegration\Actions\CheckVersionAction; +use App\Containers\VikonIntegration\Actions\SyncFilesAction; use App\Containers\VikonIntegration\Actions\UpdateCoreAction; use App\Containers\VikonIntegration\Tasks\FilesystemTask; use App\Containers\VikonIntegration\Tasks\HttpTask; @@ -63,6 +64,11 @@ class VikonServiceProvider extends ServiceProvider storagePath: config('vikon.storage_path'), basePath: public_path(), )); + + $this->app->singleton(SyncFilesAction::class, fn () => new SyncFilesAction( + http: $app->make(HttpTask::class), + publicPath: public_path(), + )); } public function boot(): void diff --git a/app/Containers/VikonIntegration/UI/WEB/Controllers/VikonController.php b/app/Containers/VikonIntegration/UI/WEB/Controllers/VikonController.php index 356f261..700c0cf 100644 --- a/app/Containers/VikonIntegration/UI/WEB/Controllers/VikonController.php +++ b/app/Containers/VikonIntegration/UI/WEB/Controllers/VikonController.php @@ -5,6 +5,7 @@ namespace App\Containers\VikonIntegration\UI\WEB\Controllers; use App\Containers\VikonIntegration\Actions\Auth\AuthenticateAction; use App\Containers\VikonIntegration\Actions\CheckAccessAction; use App\Containers\VikonIntegration\Actions\CheckVersionAction; +use App\Containers\VikonIntegration\Actions\SyncFilesAction; use App\Containers\VikonIntegration\Actions\UpdateCoreAction; use App\Containers\VikonIntegration\Tasks\RefreshTokenTask; use App\Containers\VikonIntegration\Tasks\ValidateTokenTask; @@ -23,6 +24,7 @@ class VikonController extends Controller private readonly CheckAccessAction $checkAccess, private readonly CheckVersionAction $checkVersion, private readonly UpdateCoreAction $updateCore, + private readonly SyncFilesAction $syncFiles, private readonly RefreshTokenTask $refreshToken, private readonly ValidateTokenTask $validateToken, ) {} @@ -145,6 +147,22 @@ class VikonController extends Controller } } + public function syncFiles(UpdateModuleRequest $request): JsonResponse + { + $token = Session::get('vikon_access_token'); + if (!$token) { + return response()->json(['success' => false, 'requires_auth' => true], 401); + } + + try { + $message = $this->syncFiles->run($request->validated('module_id'), $token); + return response()->json(['success' => true, 'message' => $message]); + } catch (\Throwable $e) { + Log::error('Vikon sync failed', ['error' => $e->getMessage()]); + return response()->json(['success' => false, 'message' => $e->getMessage()], 500); + } + } + public function logout(): JsonResponse { Session::forget(['vikon_access_token', 'vikon_refresh_token']); diff --git a/app/Containers/VikonIntegration/UI/WEB/Routes/web.php b/app/Containers/VikonIntegration/UI/WEB/Routes/web.php index a2b28f5..fcb2c1c 100644 --- a/app/Containers/VikonIntegration/UI/WEB/Routes/web.php +++ b/app/Containers/VikonIntegration/UI/WEB/Routes/web.php @@ -15,5 +15,6 @@ Route::prefix('/dashboard/vikon-updates') Route::post('/check-access', [VikonController::class, 'checkAccess'])->name('check-access'); Route::post('/check-version', [VikonController::class, 'checkVersion'])->name('check-version'); Route::post('/update-module', [VikonController::class, 'updateModule'])->name('update-module'); + Route::post('/sync-files', [VikonController::class, 'syncFiles'])->name('sync-files'); Route::post('/logout', [VikonController::class, 'logout'])->name('logout'); }); diff --git a/resources/js/Pages/Dashboard/VikonUpdates/Index.vue b/resources/js/Pages/Dashboard/VikonUpdates/Index.vue index 0649705..5d7ecd1 100644 --- a/resources/js/Pages/Dashboard/VikonUpdates/Index.vue +++ b/resources/js/Pages/Dashboard/VikonUpdates/Index.vue @@ -79,6 +79,10 @@ class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50"> {{ updating ? 'Обновление...' : 'Обновить' }} + @@ -135,6 +139,7 @@ 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 }); @@ -242,4 +247,24 @@ 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 || 'Ошибка синхронизации'; + } +}