isEnabled()) { return 0; // NAS down or disabled — nothing to do } // Drain queued NAS file/folder moves recorded while NAS was down // (e.g. music-video merges). Best-effort per row; failures stay // in the table with attempts/last_error for later retries. $drained = 0; $failed = 0; DB::table('pending_nas_moves')->orderBy('id')->get()->each( function ($row) use ($nas, &$drained, &$failed) { try { $nas->renameNasPath($row->from_path, $row->to_path); // Best-effort prune of the now-empty source parent // (e.g. the source video folder after all its track // subfolders were adopted by the target). $parent = dirname($row->from_path); if ($parent && $parent !== '.') { $nas->deleteFile("{$parent}/meta.json"); $nas->deleteFolder($parent); } DB::table('pending_nas_moves')->where('id', $row->id)->delete(); $drained++; } catch (\Throwable $e) { DB::table('pending_nas_moves')->where('id', $row->id)->update([ 'attempts' => (int) $row->attempts + 1, 'last_attempt_at' => now(), 'last_error' => mb_substr($e->getMessage(), 0, 500), 'updated_at' => now(), ]); $failed++; } } ); if ($drained || $failed) { $this->info("Drained {$drained} pending NAS move(s)" . ($failed ? " (${failed} failed)" : '')); } // Videos whose file OR thumbnail/slides are still on local disk $synced = 0; Video::with(['user', 'slides']) ->where('status', 'ready') ->where('path', 'like', 'users/%') ->get() ->each(function (Video $video) use ($nas, &$synced) { $hasLocalVideo = file_exists(storage_path('app/' . $video->path)); // Check for locally-stored thumbnail (NAS push failed during edit) $hasLocalThumb = $video->thumbnail && str_starts_with($video->thumbnail, 'users/') && file_exists(storage_path('app/' . $video->thumbnail)); // Check for locally-stored slides $hasLocalSlide = false; foreach ($video->slides as $slide) { if (str_starts_with($slide->filename, 'users/') && file_exists(storage_path('app/' . $slide->filename))) { $hasLocalSlide = true; break; } } if ($hasLocalVideo || $hasLocalThumb || $hasLocalSlide) { NasSyncVideoJob::dispatch($video); $synced++; } }); // Avatars and banners stuck locally User::whereNotNull('avatar')->orWhereNotNull('banner')->get() ->each(function (User $user) use ($nas) { if ($user->avatar && str_starts_with($user->avatar, 'users/') && file_exists(storage_path('app/' . $user->avatar))) { $nas->syncAvatar($user, storage_path('app/' . $user->avatar)); $nas->deleteLocalAvatar($user); } if ($user->banner && str_starts_with($user->banner, 'users/') && file_exists(storage_path('app/' . $user->banner))) { $nas->syncCover($user, storage_path('app/' . $user->banner)); $nas->deleteLocalBanner($user); } }); if ($synced > 0) { $this->info("Queued {$synced} video(s) for NAS sync."); } return 0; } }