From c61f1b0288ceb5aa9c44b3a0f03d802f04da1b82 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Mon, 2 Feb 2026 21:58:32 +0300 Subject: [PATCH] =?UTF-8?q?feat(rclone):=20ak=C4=B1ll=C4=B1=20cache=20y?= =?UTF-8?q?=C3=B6netimi=20ve=20streaming=20performans=20ayarlar=C4=B1=20ek?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disk doluluk oranını izleyen ve otomatik temizleme yapan akıllı cache sistemi eklendi. Streaming performansı için buffer size, VFS read ahead ve chunk size ayarları yapılandırılabilir hale getirildi. Rclone crash durumunda otomatik yeniden başlatma mekanizması eklendi. UI'da disk kullanım bilgileri ve VFS cache modu görüntülenmeye başlandı. --- .env.example | 24 ++++ client/src/routes/Settings.svelte | 53 +++++++- docker-compose.yml | 15 ++ server/server.js | 218 +++++++++++++++++++++++++++--- 4 files changed, 290 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index 1689db7..3d49add 100644 --- a/.env.example +++ b/.env.example @@ -60,6 +60,8 @@ RCLONE_POLL_INTERVAL=1m # Rclone dizin cache süresi RCLONE_DIR_CACHE_TIME=1m # Rclone VFS cache modu (off, minimal, writes, full) +# full: Hızlı streaming için okumalar ve yazmalar cache'lenir +# Disk doluluğu threshold'ı geçince otomatik temizlenir RCLONE_VFS_CACHE_MODE=full # Rclone VFS cache dizini RCLONE_VFS_CACHE_DIR=/app/server/cache/rclone-vfs @@ -73,3 +75,25 @@ RCLONE_RC_ADDR=127.0.0.1:5572 RCLONE_DEBUG_MODE_LOG=0 # Media stream debug log (akış kaynağını loglamak için kullanılır) MEDIA_DEBUG_LOG=0 + +# --- Rclone Streaming Performans Ayarları --- +# Buffer size - streaming performansı için (varsayılan: 16M, VPS için 8M yeterli) +RCLONE_BUFFER_SIZE=8M +# VFS read ahead - streaming için önbellek (varsayılan: off) +RCLONE_VFS_READ_AHEAD=128M +# VFS read chunk size - büyük dosyalar için (varsayılan: 128M) +RCLONE_VFS_READ_CHUNK_SIZE=32M +# VFS read chunk size limit - seek performansı için (varsayılan: off) +RCLONE_VFS_READ_CHUNK_SIZE_LIMIT=64M + +# --- Rclone Akıllı Cache Yönetimi --- +# Disk doluluk oranı eşik değeri (百分比) - Bu oran aşıldığında otomatik cache temizlenir +RCLONE_CACHE_CLEAN_THRESHOLD=85 +# Cache temizleme sırasında korunacak minimum boş alan (GB) +RCLONE_MIN_FREE_SPACE_GB=5 +# Rclone crash olursa otomatik yeniden başlatma (1 = aç, 0 = kapa) +RCLONE_AUTO_RESTART=1 +# Maksimum yeniden başlatma deneme sayısı +RCLONE_AUTO_RESTART_MAX_RETRIES=5 +# Yeniden başlatma arasındaki bekleme süresi (milisaniye) +RCLONE_AUTO_RESTART_DELAY_MS=5000 diff --git a/client/src/routes/Settings.svelte b/client/src/routes/Settings.svelte index 61eb4c5..d17c183 100644 --- a/client/src/routes/Settings.svelte +++ b/client/src/routes/Settings.svelte @@ -236,6 +236,25 @@ } } + async function checkAndCleanCache() { + error = null; + success = null; + try { + const resp = await apiFetch("/api/rclone/cache/check-and-clean", { method: "POST" }); + const data = await resp.json().catch(() => ({})); + if (!resp.ok) { + throw new Error(data?.error || data?.message || `HTTP ${resp.status}`); + } + if (data.cleaned) { + success = data.message || "Cache temizlendi."; + } else { + success = data.message || "Disk durumu iyi, temizleme gerekmedi."; + } + } catch (err) { + error = err?.message || "Cache kontrolü başarısız."; + } + } + onMount(() => { loadCookies(); loadYoutubeSettings(); @@ -404,7 +423,10 @@ /> + @@ -444,11 +466,30 @@ {#if rcloneStatus}
+
Durum:
Enabled: {rcloneStatus.enabled ? "Evet" : "Hayır"}
Mounted: {rcloneStatus.mounted ? "Evet" : "Hayır"}
Remote: {rcloneStatus.remoteConfigured ? "Hazır" : "Eksik"}
+ {#if rcloneStatus.vfsCacheMode} +
VFS Cache Mode: {rcloneStatus.vfsCacheMode}
+ {/if} + {#if rcloneStatus.diskUsage} +
+
Disk Kullanımı:
+
+ Kullanım: %{rcloneStatus.diskUsage.usedPercent} | + Boş: {rcloneStatus.diskUsage.availableGB.toFixed(1)}GB / + {rcloneStatus.diskUsage.totalGB.toFixed(1)}GB +
+ {#if rcloneStatus.cacheCleanThreshold} +
+ Temizleme eşiği: %{rcloneStatus.cacheCleanThreshold} +
+ {/if} +
+ {/if} {#if rcloneStatus.lastError} -
Son hata: {rcloneStatus.lastError}
+
Son hata: {rcloneStatus.lastError}
{/if}
{/if} @@ -668,6 +709,14 @@ color: #0f7a1f; } + :global(code) { + background: #f0f0f0; + padding: 2px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + } + .password-field { position: relative; } diff --git a/docker-compose.yml b/docker-compose.yml index c22ab04..48fa9ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,3 +41,18 @@ services: RCLONE_DIR_CACHE_TIME: ${RCLONE_DIR_CACHE_TIME} RCLONE_VFS_CACHE_MODE: ${RCLONE_VFS_CACHE_MODE} RCLONE_VFS_CACHE_DIR: ${RCLONE_VFS_CACHE_DIR} + RCLONE_VFS_CACHE_MAX_SIZE: ${RCLONE_VFS_CACHE_MAX_SIZE} + RCLONE_VFS_CACHE_MAX_AGE: ${RCLONE_VFS_CACHE_MAX_AGE} + RCLONE_RC_ENABLED: ${RCLONE_RC_ENABLED} + RCLONE_RC_ADDR: ${RCLONE_RC_ADDR} + RCLONE_BUFFER_SIZE: ${RCLONE_BUFFER_SIZE} + RCLONE_VFS_READ_AHEAD: ${RCLONE_VFS_READ_AHEAD} + RCLONE_VFS_READ_CHUNK_SIZE: ${RCLONE_VFS_READ_CHUNK_SIZE} + RCLONE_VFS_READ_CHUNK_SIZE_LIMIT: ${RCLONE_VFS_READ_CHUNK_SIZE_LIMIT} + RCLONE_DEBUG_MODE_LOG: ${RCLONE_DEBUG_MODE_LOG} + MEDIA_DEBUG_LOG: ${MEDIA_DEBUG_LOG} + RCLONE_CACHE_CLEAN_THRESHOLD: ${RCLONE_CACHE_CLEAN_THRESHOLD} + RCLONE_MIN_FREE_SPACE_GB: ${RCLONE_MIN_FREE_SPACE_GB} + RCLONE_AUTO_RESTART: ${RCLONE_AUTO_RESTART} + RCLONE_AUTO_RESTART_MAX_RETRIES: ${RCLONE_AUTO_RESTART_MAX_RETRIES} + RCLONE_AUTO_RESTART_DELAY_MS: ${RCLONE_AUTO_RESTART_DELAY_MS} diff --git a/server/server.js b/server/server.js index 58954c1..1da9dd4 100644 --- a/server/server.js +++ b/server/server.js @@ -64,6 +64,26 @@ const RCLONE_VFS_CACHE_MAX_SIZE = process.env.RCLONE_VFS_CACHE_MAX_SIZE || "20G"; const RCLONE_VFS_CACHE_MAX_AGE = process.env.RCLONE_VFS_CACHE_MAX_AGE || "24h"; +// --- Streaming performans ayarları --- +const RCLONE_BUFFER_SIZE = process.env.RCLONE_BUFFER_SIZE || "8M"; +const RCLONE_VFS_READ_AHEAD = process.env.RCLONE_VFS_READ_AHEAD || "128M"; +const RCLONE_VFS_READ_CHUNK_SIZE = process.env.RCLONE_VFS_READ_CHUNK_SIZE || "32M"; +const RCLONE_VFS_READ_CHUNK_SIZE_LIMIT = process.env.RCLONE_VFS_READ_CHUNK_SIZE_LIMIT || "64M"; +// Disk doluluk oranı eşik değeri (百分比) - Bu oran aşıldığında cache temizlenir +const RCLONE_CACHE_CLEAN_THRESHOLD = + Number(process.env.RCLONE_CACHE_CLEAN_THRESHOLD) || 85; +// Cache temizleme sırasında korunacak minimum boş alan (GB) +const RCLONE_MIN_FREE_SPACE_GB = + Number(process.env.RCLONE_MIN_FREE_SPACE_GB) || 5; +// Auto-restart enable/disable +const RCLONE_AUTO_RESTART = ["1", "true", "yes", "on"].includes( + String(process.env.RCLONE_AUTO_RESTART || "1").toLowerCase() +); +// Auto-restart için retry sayısı ve delay +const RCLONE_AUTO_RESTART_MAX_RETRIES = + Number(process.env.RCLONE_AUTO_RESTART_MAX_RETRIES) || 5; +const RCLONE_AUTO_RESTART_DELAY_MS = + Number(process.env.RCLONE_AUTO_RESTART_DELAY_MS) || 5000; const MEDIA_DEBUG_LOG = ["1", "true", "yes", "on"].includes( String(process.env.MEDIA_DEBUG_LOG || "").toLowerCase() ); @@ -762,6 +782,9 @@ let rcloneProcess = null; let rcloneLastError = null; const rcloneAuthSessions = new Map(); let rcloneCacheCleanTimer = null; +// Auto-restart sayaçları +let rcloneRestartCount = 0; +let rcloneRestartInProgress = false; function logRcloneMoveError(context, error) { if (!error) return; @@ -1029,17 +1052,17 @@ function startRcloneCacheCleanSchedule(minutes) { if (!interval || interval <= 0) return; rcloneCacheCleanTimer = setInterval(() => { if (!RCLONE_RC_ENABLED) return; - fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ recursive: true }) + // Query string format kullan + const params = new URLSearchParams(); + params.append("recursive", "true"); + + fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh?${params.toString()}`, { + method: "POST" }) .then((resp) => { if (resp.status === 404) { - return fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ recursive: true }) + return fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh?${params.toString()}`, { + method: "POST" }); } return resp; @@ -1057,16 +1080,21 @@ async function runRcloneCacheClean() { const wasRunning = Boolean(rcloneProcess && !rcloneProcess.killed); try { if (wasRunning && RCLONE_RC_ENABLED) { - const resp = await fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ recursive: true }) + // Rclone RC API doğru format: Query string kullanılır + // POST /vfs/refresh with form data: recursive=true + const params = new URLSearchParams(); + params.append("recursive", "true"); + + const resp = await fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh?${params.toString()}`, { + method: "POST" }); + if (resp.status === 404) { - const fallback = await fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ recursive: true }) + // Fallback for older rclone versions + const fallbackParams = new URLSearchParams(); + fallbackParams.append("recursive", "true"); + const fallback = await fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh?${fallbackParams.toString()}`, { + method: "POST" }); if (!fallback.ok) { const body = await fallback.text(); @@ -1083,6 +1111,7 @@ async function runRcloneCacheClean() { return { ok: false, error: "Rclone RC kapalıyken mount durdurulmadan cache temizlenemez." }; } + // RC kapalıysa dosya sisteminden temizle fs.rmSync(RCLONE_VFS_CACHE_DIR, { recursive: true, force: true }); fs.mkdirSync(RCLONE_VFS_CACHE_DIR, { recursive: true }); return { ok: true, method: "fs", restarted: false }; @@ -1091,6 +1120,64 @@ async function runRcloneCacheClean() { } } +// --- Akıllı cache yönetimi --- + +/** + * Disk alanını kontrol eder ve gerekirse cache temizler + * @returns {Promise<{ok: boolean, cleaned: boolean, diskUsage: object, message: string}>} + */ +async function checkAndCleanCacheIfNeeded() { + try { + const diskInfo = await getDiskSpace(RCLONE_VFS_CACHE_DIR); + const usedPercent = diskInfo.usedPercent || 0; + const availableGB = parseFloat(diskInfo.availableGB) || 0; + + const shouldClean = usedPercent >= RCLONE_CACHE_CLEAN_THRESHOLD || availableGB < RCLONE_MIN_FREE_SPACE_GB; + + if (!shouldClean) { + return { + ok: true, + cleaned: false, + diskUsage: { usedPercent, availableGB, threshold: RCLONE_CACHE_CLEAN_THRESHOLD, minFreeGB: RCLONE_MIN_FREE_SPACE_GB }, + message: `Disk durumu iyi (${usedPercent}% kullanılıyor, ${availableGB}GB boş)` + }; + } + + console.warn(`⚠️ Disk doluluk oranı yüksek (${usedPercent}%) veya boş alan az (${availableGB}GB). Cache temizleniyor...`); + + const result = await runRcloneCacheClean(); + + if (result.ok) { + // Temizleme sonrası disk durumunu tekrar kontrol et + const newDiskInfo = await getDiskSpace(RCLONE_VFS_CACHE_DIR); + return { + ok: true, + cleaned: true, + diskUsage: { + before: { usedPercent, availableGB }, + after: { usedPercent: newDiskInfo.usedPercent, availableGB: parseFloat(newDiskInfo.availableGB) } + }, + message: `Cache temizlendi. Öncesi: ${usedPercent}%, Sonrası: ${newDiskInfo.usedPercent}%`, + method: result.method + }; + } else { + return { + ok: false, + cleaned: false, + error: result.error, + message: `Cache temizleme başarısız: ${result.error}` + }; + } + } catch (err) { + return { + ok: false, + cleaned: false, + error: err?.message || String(err), + message: `Cache kontrolü başarısız: ${err?.message}` + }; + } +} + function isRcloneMounted(mountDir) { if (!mountDir) return false; try { @@ -1157,6 +1244,14 @@ function startRcloneMount(settings) { RCLONE_VFS_CACHE_MAX_SIZE, "--vfs-cache-max-age", RCLONE_VFS_CACHE_MAX_AGE, + "--buffer-size", + RCLONE_BUFFER_SIZE, + "--vfs-read-ahead", + RCLONE_VFS_READ_AHEAD, + "--vfs-read-chunk-size", + RCLONE_VFS_READ_CHUNK_SIZE, + "--vfs-read-chunk-size-limit", + RCLONE_VFS_READ_CHUNK_SIZE_LIMIT, "--dir-cache-time", RCLONE_DIR_CACHE_TIME, "--poll-interval", @@ -1190,10 +1285,39 @@ function startRcloneMount(settings) { console.warn(`⚠️ rclone: ${msg}`); } }); - rcloneProcess.on("exit", (code) => { + rcloneProcess.on("exit", async (code) => { if (code !== 0) { rcloneLastError = `rclone exit: ${code}`; console.warn(`⚠️ rclone mount durdu (code ${code})`); + + // Auto-restart mekanizması + if (RCLONE_AUTO_RESTART && !rcloneRestartInProgress) { + const settings = loadRcloneSettings(); + + if (settings.autoMount && rcloneRestartCount < RCLONE_AUTO_RESTART_MAX_RETRIES) { + rcloneRestartInProgress = true; + rcloneRestartCount++; + + console.warn(`🔄 Rclone otomatik yeniden başlatılıyor (${rcloneRestartCount}/${RCLONE_AUTO_RESTART_MAX_RETRIES})...`); + + // Bekle ve yeniden başlat + setTimeout(async () => { + const result = startRcloneMount(settings); + if (result.ok) { + console.log(`✅ Rclone başarıyla yeniden başlatıldı.`); + rcloneRestartCount = 0; // Başarılı olunca sayacı sıfırla + } else { + console.error(`❌ Rclone yeniden başlatılamadı: ${result.error}`); + } + rcloneRestartInProgress = false; + }, RCLONE_AUTO_RESTART_DELAY_MS); + } else if (rcloneRestartCount >= RCLONE_AUTO_RESTART_MAX_RETRIES) { + console.error(`❌ Rclone yeniden başlatma sayısı aşıldı (${RCLONE_AUTO_RESTART_MAX_RETRIES}). Otomatik yeniden başlatma devre dışı.`); + } + } + } else { + // Normal exit (code 0) - sayacı sıfırla + rcloneRestartCount = 0; } rcloneProcess = null; }); @@ -9068,6 +9192,20 @@ app.post("/api/youtube/settings", requireAuth, (req, res) => { app.get("/api/rclone/status", requireAuth, async (req, res) => { const settings = loadRcloneSettings(); const mounted = isRcloneMounted(settings.mountDir); + + // Disk durumunu da ekle + let diskUsage = null; + try { + const diskInfo = await getDiskSpace(RCLONE_VFS_CACHE_DIR); + diskUsage = { + usedPercent: diskInfo.usedPercent || 0, + availableGB: parseFloat(diskInfo.availableGB) || 0, + totalGB: parseFloat(diskInfo.totalGB) || 0 + }; + } catch (err) { + // Disk bilgisi alınamazsa null kalsın + } + res.json({ enabled: RCLONE_ENABLED, mounted, @@ -9081,7 +9219,18 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => { cacheCleanMinutes: settings.cacheCleanMinutes || 0, configExists: fs.existsSync(settings.configPath), remoteConfigured: rcloneConfigHasRemote(settings.remoteName), - lastError: rcloneLastError || null + lastError: rcloneLastError || null, + // Performans ayarları + vfsCacheMode: RCLONE_VFS_CACHE_MODE, + bufferSize: RCLONE_BUFFER_SIZE, + vfsReadAhead: RCLONE_VFS_READ_AHEAD, + vfsReadChunkSize: RCLONE_VFS_READ_CHUNK_SIZE, + vfsReadChunkSizeLimit: RCLONE_VFS_READ_CHUNK_SIZE_LIMIT, + // Disk kullanımı + diskUsage, + // Cache temizleme threshold + cacheCleanThreshold: RCLONE_CACHE_CLEAN_THRESHOLD, + minFreeSpaceGB: RCLONE_MIN_FREE_SPACE_GB }); }); @@ -9223,6 +9372,15 @@ app.post("/api/rclone/cache/clean", requireAuth, async (req, res) => { return res.json({ ok: true, ...result }); }); +// Akıllı cache kontrolü - disk durumunu kontrol eder ve gerekirse temizler +app.post("/api/rclone/cache/check-and-clean", requireAuth, async (req, res) => { + const result = await checkAndCleanCacheIfNeeded(); + if (!result.ok) { + return res.status(500).json({ ok: false, error: result.error, message: result.message }); + } + return res.json({ ok: true, ...result }); +}); + app.get("/api/rclone/conf", requireAuth, (req, res) => { try { if (!fs.existsSync(RCLONE_CONFIG_PATH)) { @@ -10526,6 +10684,20 @@ if (WEBDAV_ENABLED) { // --- ☁️ Rclone auto mount --- const initialRcloneSettings = loadRcloneSettings(); + +// Başlangıçta disk kontrolü yap - cache temizleme gerekirse yap +if (RCLONE_ENABLED) { + checkAndCleanCacheIfNeeded().then(result => { + if (result.cleaned) { + console.log(`🧹 Başlangıç cache temizlemesi: ${result.message}`); + } else { + console.log(`✅ Disk durumu: ${result.message}`); + } + }).catch(err => { + console.warn(`⚠️ Başlangıç cache kontrolü başarısız: ${err.message}`); + }); +} + if (RCLONE_ENABLED && initialRcloneSettings.autoMount) { const result = startRcloneMount(initialRcloneSettings); if (!result.ok) { @@ -10534,6 +10706,16 @@ if (RCLONE_ENABLED && initialRcloneSettings.autoMount) { } startRcloneCacheCleanSchedule(initialRcloneSettings.cacheCleanMinutes || 0); +// --- Disk alanı izleme - periyodik kontrol (her 5 dakikada bir) --- +setInterval(async () => { + if (RCLONE_ENABLED) { + const result = await checkAndCleanCacheIfNeeded(); + if (result.cleaned) { + console.log(`🧹 Otomatik cache temizlemesi: ${result.message}`); + } + } +}, 5 * 60 * 1000); + // --- ✅ Client build (frontend) dosyalarını sun --- const publicDir = path.join(__dirname, "public"); if (fs.existsSync(publicDir)) {