The merge no longer refuses when NAS is unreachable. DB changes always run; per-side file operations are best-effort. When the NAS rename can't happen it's recorded in a new pending_nas_moves table, and nas:auto-sync drains the queue (rename + best-effort parent prune) once NAS is back, retaining attempts/last_error on failure. Also plugs missing isEnabled() guards on renameNasPath and deleteFolder so the merge cleanup path can't hang on smbclient timeouts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
4.4 KiB
PHP
110 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Jobs\NasSyncVideoJob;
|
|
use App\Models\User;
|
|
use App\Models\Video;
|
|
use App\Services\NasSyncService;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class NasAutoSync extends Command
|
|
{
|
|
protected $signature = 'nas:auto-sync';
|
|
protected $description = 'Push locally-stored files to NAS when NAS is available. Runs as a scheduled task.';
|
|
|
|
public function handle(NasSyncService $nas): int
|
|
{
|
|
if (! $nas->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;
|
|
}
|
|
}
|