feat(vikon): add file sync from VIKON file manager (FM)
This commit is contained in:
@@ -0,0 +1,245 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Containers\VikonIntegration\Actions;
|
||||||
|
|
||||||
|
use App\Containers\VikonIntegration\Tasks\HttpTask;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class SyncFilesAction
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly HttpTask $http,
|
||||||
|
private readonly string $publicPath,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function run(int $moduleId, string $accessToken): string
|
||||||
|
{
|
||||||
|
$modulePath = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace App\Containers\VikonIntegration\Providers;
|
|||||||
use App\Containers\VikonIntegration\Actions\Auth\AuthenticateAction;
|
use App\Containers\VikonIntegration\Actions\Auth\AuthenticateAction;
|
||||||
use App\Containers\VikonIntegration\Actions\CheckAccessAction;
|
use App\Containers\VikonIntegration\Actions\CheckAccessAction;
|
||||||
use App\Containers\VikonIntegration\Actions\CheckVersionAction;
|
use App\Containers\VikonIntegration\Actions\CheckVersionAction;
|
||||||
|
use App\Containers\VikonIntegration\Actions\SyncFilesAction;
|
||||||
use App\Containers\VikonIntegration\Actions\UpdateCoreAction;
|
use App\Containers\VikonIntegration\Actions\UpdateCoreAction;
|
||||||
use App\Containers\VikonIntegration\Tasks\FilesystemTask;
|
use App\Containers\VikonIntegration\Tasks\FilesystemTask;
|
||||||
use App\Containers\VikonIntegration\Tasks\HttpTask;
|
use App\Containers\VikonIntegration\Tasks\HttpTask;
|
||||||
@@ -63,6 +64,11 @@ class VikonServiceProvider extends ServiceProvider
|
|||||||
storagePath: config('vikon.storage_path'),
|
storagePath: config('vikon.storage_path'),
|
||||||
basePath: public_path(),
|
basePath: public_path(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
$this->app->singleton(SyncFilesAction::class, fn () => new SyncFilesAction(
|
||||||
|
http: $app->make(HttpTask::class),
|
||||||
|
publicPath: public_path(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Containers\VikonIntegration\UI\WEB\Controllers;
|
|||||||
use App\Containers\VikonIntegration\Actions\Auth\AuthenticateAction;
|
use App\Containers\VikonIntegration\Actions\Auth\AuthenticateAction;
|
||||||
use App\Containers\VikonIntegration\Actions\CheckAccessAction;
|
use App\Containers\VikonIntegration\Actions\CheckAccessAction;
|
||||||
use App\Containers\VikonIntegration\Actions\CheckVersionAction;
|
use App\Containers\VikonIntegration\Actions\CheckVersionAction;
|
||||||
|
use App\Containers\VikonIntegration\Actions\SyncFilesAction;
|
||||||
use App\Containers\VikonIntegration\Actions\UpdateCoreAction;
|
use App\Containers\VikonIntegration\Actions\UpdateCoreAction;
|
||||||
use App\Containers\VikonIntegration\Tasks\RefreshTokenTask;
|
use App\Containers\VikonIntegration\Tasks\RefreshTokenTask;
|
||||||
use App\Containers\VikonIntegration\Tasks\ValidateTokenTask;
|
use App\Containers\VikonIntegration\Tasks\ValidateTokenTask;
|
||||||
@@ -23,6 +24,7 @@ class VikonController extends Controller
|
|||||||
private readonly CheckAccessAction $checkAccess,
|
private readonly CheckAccessAction $checkAccess,
|
||||||
private readonly CheckVersionAction $checkVersion,
|
private readonly CheckVersionAction $checkVersion,
|
||||||
private readonly UpdateCoreAction $updateCore,
|
private readonly UpdateCoreAction $updateCore,
|
||||||
|
private readonly SyncFilesAction $syncFiles,
|
||||||
private readonly RefreshTokenTask $refreshToken,
|
private readonly RefreshTokenTask $refreshToken,
|
||||||
private readonly ValidateTokenTask $validateToken,
|
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
|
public function logout(): JsonResponse
|
||||||
{
|
{
|
||||||
Session::forget(['vikon_access_token', 'vikon_refresh_token']);
|
Session::forget(['vikon_access_token', 'vikon_refresh_token']);
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ Route::prefix('/dashboard/vikon-updates')
|
|||||||
Route::post('/check-access', [VikonController::class, 'checkAccess'])->name('check-access');
|
Route::post('/check-access', [VikonController::class, 'checkAccess'])->name('check-access');
|
||||||
Route::post('/check-version', [VikonController::class, 'checkVersion'])->name('check-version');
|
Route::post('/check-version', [VikonController::class, 'checkVersion'])->name('check-version');
|
||||||
Route::post('/update-module', [VikonController::class, 'updateModule'])->name('update-module');
|
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');
|
Route::post('/logout', [VikonController::class, 'logout'])->name('logout');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,6 +79,10 @@
|
|||||||
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50">
|
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50">
|
||||||
{{ updating ? 'Обновление...' : 'Обновить' }}
|
{{ updating ? 'Обновление...' : 'Обновить' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="syncModuleFiles(id)" :disabled="syncing || !accessInfo.has_access"
|
||||||
|
class="px-4 py-2 bg-surface border border-layer-line rounded-lg hover:bg-muted-hover disabled:opacity-50">
|
||||||
|
{{ syncing ? 'Синхронизация...' : 'Синхронизировать файлы' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,6 +139,7 @@ const currentVersion = ref(props.current_version);
|
|||||||
const checkingVersion = ref(false);
|
const checkingVersion = ref(false);
|
||||||
const checkingAccess = ref(false);
|
const checkingAccess = ref(false);
|
||||||
const updating = ref(false);
|
const updating = ref(false);
|
||||||
|
const syncing = ref(false);
|
||||||
const progress = ref(0);
|
const progress = ref(0);
|
||||||
const updateError = ref(null);
|
const updateError = ref(null);
|
||||||
const versionInfo = ref({ current_version: props.current_version, has_update: false, latest_version: 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);
|
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 || 'Ошибка синхронизации';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user