WIP: storage-fix-local-nas work before playlist controls feature
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
6b3ab5b65e
commit
c160242dbc
46 files changed
+5365
-815
No files matched your search
@@ -12,7 +12,8 @@ class NasFreeLocalStorage extends Command
|
||||
{
|
||||
protected $signature = 'nas:free-local
|
||||
{--dry-run : Preview what would be deleted without deleting}
|
||||
{--force : Actually delete local files confirmed on NAS}';
|
||||
{--force : Actually delete local files confirmed on NAS}
|
||||
{--cache-ttl=24 : Hours after which NAS stream-cache files are evicted (0 = all)}';
|
||||
|
||||
protected $description = 'Delete local files (videos, thumbnails, avatars, banners) already stored on the NAS';
|
||||
|
||||
@@ -140,6 +141,7 @@ class NasFreeLocalStorage extends Command
|
||||
$this->newLine();
|
||||
$this->info('Scanning avatar files…');
|
||||
|
||||
// Legacy flat dir
|
||||
$avatarDir = storage_path('app/public/avatars');
|
||||
if (is_dir($avatarDir)) {
|
||||
foreach (glob($avatarDir . '/*') as $file) {
|
||||
@@ -148,23 +150,42 @@ class NasFreeLocalStorage extends Command
|
||||
$user = User::where('avatar', $filename)->first();
|
||||
if (! $user) continue;
|
||||
|
||||
// Confirm avatar.webp is on NAS
|
||||
$dir = "users/{$nas->userSlug($user)}/profile";
|
||||
$raw = null;
|
||||
try { $raw = $nas->getContent("{$dir}/avatar.webp"); } catch (\Throwable) {}
|
||||
if ($raw !== null) {
|
||||
$bytes = filesize($file);
|
||||
$totalBytes += $bytes;
|
||||
$toDelete[] = ['label' => "avatar:{$filename}", 'path' => $file, 'bytes' => $bytes];
|
||||
$bytes = filesize($file); $totalBytes += $bytes;
|
||||
$toDelete[] = ['label' => "avatar:{$filename}", 'path' => $file, 'bytes' => $bytes];
|
||||
}
|
||||
}
|
||||
$this->line(' Done scanning avatars.');
|
||||
}
|
||||
|
||||
// New structured dir: users/{slug}/profile/avatar.*
|
||||
$usersBase = storage_path('app/users');
|
||||
if (is_dir($usersBase)) {
|
||||
foreach (glob("{$usersBase}/*/profile/avatar.*") ?: [] as $file) {
|
||||
if (! is_file($file)) continue;
|
||||
$relPath = 'users/' . ltrim(str_replace(storage_path('app/users') . '/', '', str_replace('\\', '/', $file)), '/');
|
||||
$user = User::where('avatar', $relPath)->first();
|
||||
if (! $user) continue;
|
||||
|
||||
$dir = "users/{$nas->userSlug($user)}/profile";
|
||||
$raw = null;
|
||||
try { $raw = $nas->getContent("{$dir}/avatar.webp"); } catch (\Throwable) {}
|
||||
if ($raw !== null) {
|
||||
$bytes = filesize($file); $totalBytes += $bytes;
|
||||
$toDelete[] = ['label' => 'avatar:' . basename($file), 'path' => $file, 'bytes' => $bytes];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->line(' Done scanning avatars.');
|
||||
|
||||
// ── Banners ───────────────────────────────────────────────────────────
|
||||
$this->newLine();
|
||||
$this->info('Scanning banner files…');
|
||||
|
||||
// Legacy flat dir
|
||||
$bannerDir = storage_path('app/public/banners');
|
||||
if (is_dir($bannerDir)) {
|
||||
foreach (glob($bannerDir . '/*') as $file) {
|
||||
@@ -177,14 +198,32 @@ class NasFreeLocalStorage extends Command
|
||||
$raw = null;
|
||||
try { $raw = $nas->getContent("{$dir}/cover.webp"); } catch (\Throwable) {}
|
||||
if ($raw !== null) {
|
||||
$bytes = filesize($file);
|
||||
$totalBytes += $bytes;
|
||||
$toDelete[] = ['label' => "banner:{$filename}", 'path' => $file, 'bytes' => $bytes];
|
||||
$bytes = filesize($file); $totalBytes += $bytes;
|
||||
$toDelete[] = ['label' => "banner:{$filename}", 'path' => $file, 'bytes' => $bytes];
|
||||
}
|
||||
}
|
||||
$this->line(' Done scanning banners.');
|
||||
}
|
||||
|
||||
// New structured dir: users/{slug}/profile/cover.*
|
||||
if (is_dir($usersBase)) {
|
||||
foreach (glob("{$usersBase}/*/profile/cover.*") ?: [] as $file) {
|
||||
if (! is_file($file)) continue;
|
||||
$relPath = 'users/' . ltrim(str_replace(storage_path('app/users') . '/', '', str_replace('\\', '/', $file)), '/');
|
||||
$user = User::where('banner', $relPath)->first();
|
||||
if (! $user) continue;
|
||||
|
||||
$dir = "users/{$nas->userSlug($user)}/profile";
|
||||
$raw = null;
|
||||
try { $raw = $nas->getContent("{$dir}/cover.webp"); } catch (\Throwable) {}
|
||||
if ($raw !== null) {
|
||||
$bytes = filesize($file); $totalBytes += $bytes;
|
||||
$toDelete[] = ['label' => 'banner:' . basename($file), 'path' => $file, 'bytes' => $bytes];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->line(' Done scanning banners.');
|
||||
|
||||
// ── Slideshow cache directories ───────────────────────────────────────
|
||||
// The slideshow/ directory is a render cache that is always regenerated on
|
||||
// demand, so its contents are safe to delete unconditionally.
|
||||
@@ -205,6 +244,27 @@ class NasFreeLocalStorage extends Command
|
||||
$this->line(' Done scanning slideshow cache.');
|
||||
}
|
||||
|
||||
// ── NAS stream cache (nas_cache/videos/) ──────────────────────────────
|
||||
// These are on-demand local copies of NAS videos used for HTTP streaming.
|
||||
// Always safe to delete — they are re-downloaded from NAS on next play.
|
||||
$this->newLine();
|
||||
$this->info('Scanning NAS stream cache…');
|
||||
|
||||
$nasCacheDir = storage_path('app/nas_cache/videos');
|
||||
$ttl = (int) $this->option('cache-ttl');
|
||||
$cutoff = time() - ($ttl * 3600);
|
||||
|
||||
if (is_dir($nasCacheDir)) {
|
||||
foreach (glob("{$nasCacheDir}/*") as $file) {
|
||||
if (! is_file($file)) continue;
|
||||
if ($ttl > 0 && filemtime($file) >= $cutoff) continue;
|
||||
$bytes = filesize($file);
|
||||
$totalBytes += $bytes;
|
||||
$toDelete[] = ['label' => 'nas-cache', 'path' => $file, 'bytes' => $bytes];
|
||||
}
|
||||
$this->line(' Done scanning NAS stream cache.');
|
||||
}
|
||||
|
||||
// ── Summary ───────────────────────────────────────────────────────────
|
||||
$this->newLine();
|
||||
|
||||
@@ -249,6 +309,15 @@ class NasFreeLocalStorage extends Command
|
||||
}
|
||||
}
|
||||
|
||||
// Prune empty directories left behind under storage/app/users/ and flat asset dirs
|
||||
$this->pruneEmptyDirs(storage_path('app/users'));
|
||||
foreach (['public/avatars', 'public/banners', 'public/thumbnails'] as $rel) {
|
||||
$path = storage_path("app/{$rel}");
|
||||
if (is_dir($path) && empty(array_diff(scandir($path) ?: [], ['.', '..']))) {
|
||||
@rmdir($path);
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Deleted {$deleted} file(s), freed {$this->humanBytes($totalBytes)}.");
|
||||
|
||||
@@ -259,6 +328,32 @@ class NasFreeLocalStorage extends Command
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bottom-up prune: remove dirs that are empty or contain only meta.json.
|
||||
*/
|
||||
private function pruneEmptyDirs(string $root): void
|
||||
{
|
||||
if (! is_dir($root)) return;
|
||||
|
||||
$iter = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iter as $item) {
|
||||
if (! $item->isDir()) continue;
|
||||
$path = $item->getPathname();
|
||||
$contents = array_diff(scandir($path) ?: [], ['.', '..']);
|
||||
$nonMeta = array_diff($contents, ['meta.json']);
|
||||
if (empty($contents)) {
|
||||
@rmdir($path);
|
||||
} elseif (empty($nonMeta)) {
|
||||
@unlink("{$path}/meta.json");
|
||||
@rmdir($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private function humanBytes(int $bytes): string
|
||||
|
||||
Reference in new issue
Block a user