isEnabled()) { $this->error('NAS sync is not enabled. Enable it in Admin → Settings first.'); return 1; } $dryRun = $this->option('dry-run'); $force = $this->option('force'); if (! $dryRun && ! $force) { $this->warn('Pass --dry-run to preview, or --force to delete.'); return 1; } $mode = $dryRun ? 'DRY RUN — nothing will be deleted' : 'FORCE — deleting confirmed NAS files'; $this->info($mode); $this->newLine(); // Only videos that still have a local file $videos = Video::all()->filter(function (Video $v) { return file_exists(storage_path('app/' . $v->path)); }); if ($videos->isEmpty()) { $this->info('No local video files found — nothing to do.'); return 0; } $this->info("Checking {$videos->count()} local file(s) against NAS…"); $this->newLine(); $toDelete = []; $totalBytes = 0; $bar = $this->output->createProgressBar($videos->count()); $bar->start(); foreach ($videos as $video) { $localPath = storage_path('app/' . $video->path); // meta.json is written last in syncVideo(), so its presence means // the video file was fully pushed to NAS. $dir = $nas->resolveVideoDir($video); $meta = null; try { $raw = $nas->getContent("{$dir}/meta.json"); $meta = $raw ? json_decode($raw, true) : null; } catch (\Throwable) { // SMB error — skip this file } if (is_array($meta) && ($meta['id'] ?? null) === $video->id) { $bytes = filesize($localPath); $totalBytes += $bytes; $toDelete[] = [ 'video' => $video, 'path' => $localPath, 'bytes' => $bytes, ]; } $bar->advance(); } $bar->finish(); $this->newLine(2); if (empty($toDelete)) { $this->info('No local files confirmed on NAS — nothing to delete.'); return 0; } $this->table( ['ID', 'Title', 'Local file', 'Size'], array_map(fn ($row) => [ $row['video']->id, \Illuminate\Support\Str::limit($row['video']->title, 40), basename($row['path']), $this->humanBytes($row['bytes']), ], $toDelete) ); $this->newLine(); $this->line(sprintf( 'Found %d file(s) totalling %s that can be freed.', count($toDelete), $this->humanBytes($totalBytes) )); if ($dryRun) { $this->newLine(); $this->warn('Run with --force to delete these files.'); return 0; } // ── Actually delete ─────────────────────────────────────────────────── $deleted = 0; $failed = 0; foreach ($toDelete as $row) { if (@unlink($row['path'])) { $deleted++; Log::info('nas:free-local: deleted ' . $row['path'], ['video_id' => $row['video']->id]); } else { $failed++; $this->warn("Could not delete: {$row['path']}"); } } $this->newLine(); $this->info("Deleted {$deleted} file(s), freed {$this->humanBytes($totalBytes)}."); if ($failed > 0) { $this->warn("{$failed} file(s) could not be deleted — check permissions."); } return 0; } // ── Helpers ─────────────────────────────────────────────────────────────── private function humanBytes(int $bytes): string { if ($bytes >= 1_073_741_824) return round($bytes / 1_073_741_824, 2) . ' GB'; if ($bytes >= 1_048_576) return round($bytes / 1_048_576, 2) . ' MB'; if ($bytes >= 1_024) return round($bytes / 1_024, 2) . ' KB'; return $bytes . ' B'; } }