feat(rclone): akıllı cache yönetimi ve streaming performans ayarları ekle

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ı.
This commit is contained in:
2026-02-02 21:58:32 +03:00
parent e34b8fc024
commit c61f1b0288
4 changed files with 290 additions and 20 deletions

View File

@@ -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

View File

@@ -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 @@
/>
</div>
<button class="btn" on:click={cleanRcloneCache}>
<i class="fa-solid fa-broom"></i> Clean Cache
<i class="fa-solid fa-broom"></i> Temizle
</button>
<button class="btn primary" on:click={checkAndCleanCache}>
<i class="fa-solid fa-wand-magic-sparkles"></i> Akıllı Temizle
</button>
</div>
@@ -444,11 +466,30 @@
{#if rcloneStatus}
<div class="card muted" style="margin-top:10px;">
<div><strong>Durum:</strong></div>
<div>Enabled: {rcloneStatus.enabled ? "Evet" : "Hayır"}</div>
<div>Mounted: {rcloneStatus.mounted ? "Evet" : "Hayır"}</div>
<div>Remote: {rcloneStatus.remoteConfigured ? "Hazır" : "Eksik"}</div>
{#if rcloneStatus.vfsCacheMode}
<div>VFS Cache Mode: <code>{rcloneStatus.vfsCacheMode}</code></div>
{/if}
{#if rcloneStatus.diskUsage}
<div style="margin-top: 8px;">
<div style="font-size: 12px; color: #666;">Disk Kullanımı:</div>
<div style="font-size: 13px;">
Kullanım: %{rcloneStatus.diskUsage.usedPercent} |
Boş: {rcloneStatus.diskUsage.availableGB.toFixed(1)}GB /
{rcloneStatus.diskUsage.totalGB.toFixed(1)}GB
</div>
{#if rcloneStatus.cacheCleanThreshold}
<div style="font-size: 11px; color: #888;">
Temizleme eşiği: %{rcloneStatus.cacheCleanThreshold}
</div>
{/if}
</div>
{/if}
{#if rcloneStatus.lastError}
<div>Son hata: {rcloneStatus.lastError}</div>
<div style="margin-top: 8px; color: #d32f2f;">Son hata: {rcloneStatus.lastError}</div>
{/if}
</div>
{/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;
}

View File

@@ -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}

View File

@@ -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ıı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)) {