Use NAS as primary storage — direct upload when enabled

When NAS storage is enabled, uploaded files go directly to the NAS share
(users/{username}/videos/{title-slug}/) with no permanent local copy kept.
Thumbnails and video are fetched from NAS on demand for streaming/playback.
When NAS is disabled, files are organised into the same directory schema
in local storage.

- VideoController: branch upload flow on NAS enabled/disabled
- NasSyncService: add uploadDirectToNas() for direct NAS writes,
  organizeLocalFiles() for local NAS-schema, localVideoDir() resolver,
  deleteLocalAssets() for post-sync cleanup
- GenerateHlsJob: download from NAS via ensureLocalCopy() when local
  file is absent (NAS-primary mode); clean up temp after HLS generation
- CompressVideoJob: place compressed file alongside original (any dir)
- Video/VideoSlide models: localVideoPath(), localThumbnailPath(),
  thumbnailStorageKey(), localPath(), storageKey() helpers for
  format-agnostic path resolution (old flat paths + new NAS-schema paths)
- MediaController: serve thumbnails from NAS-mirrored paths with NAS fallback
- SuperAdminController: use model path helpers for file deletion
- NasFreeLocalStorage: scan new users/ tree in addition to legacy flat dirs
- Settings: rename "NAS Storage Sync" tab to "NAS Storage", update description

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
ghassanandClaude Sonnet 4.6 committed 2026-05-14 17:17:07 +03:00
1 parent 296d605864
commit 6b3ab5b65e
11 files changed
+869 -125

No files matched your search

+54
View File
@@ -317,6 +317,51 @@
</div>
</div>
{{-- ── NAS Storage ───────────────────────────────────────────── --}}
<div class="settings-section">
<div class="settings-section-header">
<i class="bi bi-hdd-network"></i>
NAS Storage
@if($settings['nas_sync_enabled'] === 'true')
<span class="chip chip-green" style="margin-left:6px;"><span class="chip-dot"></span> Enabled</span>
@else
<span class="chip chip-red" style="margin-left:6px;"><span class="chip-dot"></span> Disabled</span>
@endif
</div>
<div class="settings-section-body">
<div class="setting-row" style="padding-top:0;">
<div class="setting-label">
<strong>Use NAS as primary storage</strong>
<small>
When enabled, uploads go <strong>directly to the NAS</strong> — no permanent local copy is kept.
Files are stored at <code>users/{username}/videos/{title-slug}/</code> on the NAS share.
Thumbnails and video are fetched from NAS on demand for streaming and playback.
When disabled, files are stored in local storage using the same directory schema.
Requires the NAS connection to be configured under
<a href="{{ route('admin.nas-storage') }}" style="color:var(--brand)">NAS Storage</a>.
</small>
</div>
<div class="setting-control">
<label class="toggle-wrap">
<div class="toggle-switch">
<input type="checkbox" id="nasSyncInput" name="nas_sync_enabled_check"
{{ $settings['nas_sync_enabled'] === 'true' ? 'checked' : '' }}>
<div class="toggle-track"></div>
<div class="toggle-thumb"></div>
</div>
<span class="toggle-label" id="nasSyncLabel">
{{ $settings['nas_sync_enabled'] === 'true' ? 'Enabled' : 'Disabled' }}
</span>
</label>
<input type="hidden" name="nas_sync_enabled" id="nasSyncHidden"
value="{{ $settings['nas_sync_enabled'] }}">
</div>
</div>
</div>
</div>
{{-- ── Save ─────────────────────────────────────────────────── --}}
<div class="save-bar">
<a href="{{ route('admin.dashboard') }}" class="adm-btn">Cancel</a>
@@ -347,6 +392,15 @@ function selectEncoder(el) {
document.getElementById('gpuPresetSelect').value = isCpu ? 'fast' : 'p4';
}
// ── NAS sync toggle ───────────────────────────────────────────
const nasToggle = document.getElementById('nasSyncInput');
const nasHidden = document.getElementById('nasSyncHidden');
const nasLabel = document.getElementById('nasSyncLabel');
nasToggle.addEventListener('change', () => {
nasHidden.value = nasToggle.checked ? 'true' : 'false';
nasLabel.textContent = nasToggle.checked ? 'Enabled' : 'Disabled';
});
// ── GPU toggle ────────────────────────────────────────────────
const gpuToggle = document.getElementById('gpuEnabledInput');
const gpuHidden = document.getElementById('gpuEnabledHidden');