From cd4769b3c1aab98570f63e35dac7d609b4bdf255 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Mon, 2 Feb 2026 15:26:16 +0300 Subject: [PATCH] =?UTF-8?q?feat(rclone):=20RC=20API=20ilerleme=20takibi=20?= =?UTF-8?q?ve=20conf=20edit=C3=B6r=C3=BC=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rclone RC API kullanılarak dosya yüklemelerinde anlık ilerleme çubuğu eklendi. - Arayüz üzerinden `rclone.conf` dosyası düzenlenebilir hale getirildi. - VFS cache boyutu/yaş sınırları ve otomatik temizleme ayarı eklendi. - Manuel yetkilendirme alanları kaldırıldı. --- .env.example | 6 + client/src/routes/Settings.svelte | 206 +++++++++---------- client/src/routes/Transfers.svelte | 14 +- server/server.js | 310 +++++++++++++++++++++++++++-- 4 files changed, 404 insertions(+), 132 deletions(-) diff --git a/.env.example b/.env.example index 1d844f8..257ada9 100644 --- a/.env.example +++ b/.env.example @@ -63,6 +63,12 @@ RCLONE_DIR_CACHE_TIME=1m RCLONE_VFS_CACHE_MODE=full # Rclone VFS cache dizini RCLONE_VFS_CACHE_DIR=/app/server/cache/rclone-vfs +# Rclone VFS cache sınırları +RCLONE_VFS_CACHE_MAX_SIZE=20G +RCLONE_VFS_CACHE_MAX_AGE=24h +# Rclone RC (progress) API +RCLONE_RC_ENABLED=1 +RCLONE_RC_ADDR=127.0.0.1:5572 # Rclone debug log (taşıma hatalarını detaylı loglamak için) RCLONE_DEBUG_MODE_LOG=0 # Media stream debug log (akış kaynağını loglamak için) diff --git a/client/src/routes/Settings.svelte b/client/src/routes/Settings.svelte index b93cf27..61eb4c5 100644 --- a/client/src/routes/Settings.svelte +++ b/client/src/routes/Settings.svelte @@ -25,15 +25,11 @@ let rcloneStatus = null; let rcloneLoading = false; let rcloneSaving = false; - let rcloneAuthUrl = ""; - let rcloneToken = ""; - let rcloneClientId = ""; - let rcloneClientSecret = ""; let rcloneAutoMove = false; let rcloneAutoMount = false; - let rcloneRemoteName = ""; - let rcloneRemotePath = ""; - let rcloneMountDir = ""; + let rcloneCacheCleanMinutes = 0; + let rcloneConfText = ""; + let rcloneConfVisible = false; async function loadCookies() { loadingCookies = true; @@ -135,9 +131,7 @@ rcloneStatus = data; rcloneAutoMove = Boolean(data?.autoMove); rcloneAutoMount = Boolean(data?.autoMount); - rcloneRemoteName = data?.remoteName || ""; - rcloneRemotePath = data?.remotePath || ""; - rcloneMountDir = data?.mountDir || ""; + rcloneCacheCleanMinutes = Number(data?.cacheCleanMinutes) || 0; } catch (err) { error = err?.message || "Rclone durumu alınamadı."; } finally { @@ -145,6 +139,19 @@ } } + async function loadRcloneConf() { + try { + const resp = await apiFetch("/api/rclone/conf"); + const data = await resp.json().catch(() => ({})); + if (!resp.ok || !data?.ok) { + throw new Error(data?.error || `HTTP ${resp.status}`); + } + rcloneConfText = data.content || ""; + } catch (err) { + error = err?.message || "rclone.conf okunamadı."; + } + } + async function saveRcloneSettings() { if (rcloneSaving) return; rcloneSaving = true; @@ -157,15 +164,22 @@ body: JSON.stringify({ autoMove: rcloneAutoMove, autoMount: rcloneAutoMount, - remoteName: rcloneRemoteName, - remotePath: rcloneRemotePath, - mountDir: rcloneMountDir + cacheCleanMinutes: rcloneCacheCleanMinutes }) }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data?.ok) { throw new Error(data?.error || `HTTP ${resp.status}`); } + const confResp = await apiFetch("/api/rclone/conf", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: rcloneConfText }) + }); + const confData = await confResp.json().catch(() => ({})); + if (!confResp.ok || !confData?.ok) { + throw new Error(confData?.error || `HTTP ${confResp.status}`); + } success = "Rclone ayarları kaydedildi."; await loadRcloneStatus(); } catch (err) { @@ -175,50 +189,6 @@ } } - async function requestRcloneAuthUrl() { - error = null; - success = null; - try { - const resp = await apiFetch("/api/rclone/auth-url"); - const data = await resp.json().catch(() => ({})); - if (!resp.ok || !data?.ok) { - throw new Error(data?.error || `HTTP ${resp.status}`); - } - rcloneAuthUrl = data.url || ""; - } catch (err) { - error = err?.message || "Auth URL alınamadı."; - } - } - - async function applyRcloneToken() { - if (!rcloneToken) { - error = "Token zorunlu."; - return; - } - error = null; - success = null; - try { - const resp = await apiFetch("/api/rclone/token", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - token: rcloneToken, - clientId: rcloneClientId, - clientSecret: rcloneClientSecret, - remoteName: rcloneRemoteName - }) - }); - const data = await resp.json().catch(() => ({})); - if (!resp.ok || !data?.ok) { - throw new Error(data?.error || `HTTP ${resp.status}`); - } - success = "Token kaydedildi."; - await loadRcloneStatus(); - } catch (err) { - error = err?.message || "Token kaydedilemedi."; - } - } - async function startRcloneMount() { error = null; success = null; @@ -251,10 +221,26 @@ } } + async function cleanRcloneCache() { + error = null; + success = null; + try { + const resp = await apiFetch("/api/rclone/cache/clean", { method: "POST" }); + const data = await resp.json().catch(() => ({})); + if (!resp.ok || !data?.ok) { + throw new Error(data?.error || `HTTP ${resp.status}`); + } + success = "Rclone cache temizlendi."; + } catch (err) { + error = err?.message || "Rclone cache temizlenemedi."; + } + } + onMount(() => { loadCookies(); loadYoutubeSettings(); loadRcloneStatus(); + loadRcloneConf(); }); function formatDate(ts) { @@ -318,7 +304,7 @@ -
+
@@ -407,73 +393,47 @@
- + -
-
- -
+
- - + +
+ + +
-
-
- -
- -
- {#if rcloneAuthUrl} - - {/if} -
- -
- - -
-
- - -
-
- - -
-
- @@ -707,4 +667,32 @@ background: #e5ffe7; color: #0f7a1f; } + + .password-field { + position: relative; + } + + .conf-textarea { + width: 100%; + min-height: 180px; + resize: vertical; + font-family: "Courier New", monospace; + letter-spacing: 0.2px; + } + + .conf-textarea.masked { + -webkit-text-security: disc; + text-security: disc; + } + + .eye-btn { + position: absolute; + right: 20px; + top: 8px; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 6px; + padding: 6px 8px; + cursor: pointer; + } diff --git a/client/src/routes/Transfers.svelte b/client/src/routes/Transfers.svelte index 10e4d50..c92104f 100644 --- a/client/src/routes/Transfers.svelte +++ b/client/src/routes/Transfers.svelte @@ -855,18 +855,20 @@
{/if} -
+
{#if t.type === "mailru" && t.status === "awaiting_match"} Eşleştirme bekleniyor - {:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "moving"} - GDrive'a Taşınıyor.. • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB + {:else if t.moveToGdrive && t.moveStatus === "queued"} + GDrive kuyruğunda • {(t.downloaded / 1e6).toFixed(1)} MB + {:else if t.moveToGdrive && t.moveStatus === "uploading"} + GDrive Upload.. • {((t.moveProgress || 0) * 100).toFixed(1)}% • {(t.downloaded / 1e6).toFixed(1)} MB {:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "done"} GDrive ✓ • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB {:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "error"} @@ -1235,6 +1237,10 @@ transition: width 0.3s; } + .progress-bar.uploading .progress { + background: linear-gradient(90deg, #ef4444, #b91c1c); + } + .torrent-error { color: #e74c3c; font-size: 12px; diff --git a/server/server.js b/server/server.js index 13dec99..00ee887 100644 --- a/server/server.js +++ b/server/server.js @@ -56,6 +56,14 @@ const RCLONE_VFS_CACHE_MODE = const RCLONE_DEBUG_MODE_LOG = ["1", "true", "yes", "on"].includes( String(process.env.RCLONE_DEBUG_MODE_LOG || "").toLowerCase() ); +const RCLONE_RC_ENABLED = ["1", "true", "yes", "on"].includes( + String(process.env.RCLONE_RC_ENABLED || "1").toLowerCase() +); +const RCLONE_RC_ADDR = process.env.RCLONE_RC_ADDR || "127.0.0.1:5572"; +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"; const MEDIA_DEBUG_LOG = ["1", "true", "yes", "on"].includes( String(process.env.MEDIA_DEBUG_LOG || "").toLowerCase() ); @@ -753,6 +761,7 @@ function resolveRootDir(rootFolder) { let rcloneProcess = null; let rcloneLastError = null; const rcloneAuthSessions = new Map(); +let rcloneCacheCleanTimer = null; function logRcloneMoveError(context, error) { if (!error) return; @@ -764,10 +773,11 @@ function logRcloneMoveError(context, error) { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -async function waitForFileStable(filePath, attempts = 5, intervalMs = 2000) { +async function waitForFileStable(filePath, attempts = 8, intervalMs = 3000) { if (!filePath || !fs.existsSync(filePath)) return false; let prevSize = null; let prevMtime = null; + let stableCount = 0; for (let i = 0; i < attempts; i += 1) { let stat; try { @@ -778,7 +788,12 @@ async function waitForFileStable(filePath, attempts = 5, intervalMs = 2000) { const size = stat.size; const mtime = stat.mtimeMs; if (prevSize !== null && prevMtime !== null) { - if (size === prevSize && mtime === prevMtime) return true; + if (size === prevSize && mtime === prevMtime) { + stableCount += 1; + if (stableCount >= 2) return true; + } else { + stableCount = 0; + } } prevSize = size; prevMtime = mtime; @@ -787,6 +802,127 @@ async function waitForFileStable(filePath, attempts = 5, intervalMs = 2000) { return false; } +function computeRootVideoBytes(rootFolder) { + try { + const entries = enumerateVideoFiles(rootFolder) || []; + const total = entries.reduce((sum, item) => sum + (Number(item.size) || 0), 0); + return total || null; + } catch { + return null; + } +} + +let rcloneStatsTimer = null; +async function fetchRcloneStats() { + if (!RCLONE_RC_ENABLED) return null; + try { + let resp = await fetch(`http://${RCLONE_RC_ADDR}/core/stats`, { + method: "POST" + }); + if (resp.status === 404) { + resp = await fetch(`http://${RCLONE_RC_ADDR}/rc/core/stats`, { + method: "POST" + }); + } + if (!resp.ok) return null; + return await resp.json(); + } catch { + return null; + } +} + +function updateMoveProgressFromStats(stats) { + if (!stats) return false; + const transfers = Array.isArray(stats.transferring) ? stats.transferring : []; + let updated = false; + const applyProgress = (entry, prefix) => { + if (!entry) return; + const matched = transfers.filter((t) => String(t.name || "").includes(prefix)); + if (matched.length) { + const bytes = matched.reduce((sum, t) => sum + (Number(t.bytes) || 0), 0); + const pct = matched.reduce((sum, t) => sum + (Number(t.percentage) || 0), 0) / matched.length; + if (!entry.moveTotalBytes) { + const totalFromStats = matched.reduce((sum, t) => sum + (Number(t.size) || 0), 0); + entry.moveTotalBytes = totalFromStats || entry.moveTotalBytes || null; + } + const progress = Number.isFinite(pct) + ? Math.min(Math.max(pct / 100, 0), 0.99) + : entry.moveTotalBytes + ? Math.min(Math.max(bytes / entry.moveTotalBytes, 0), 0.99) + : 0; + if (Number.isFinite(progress) && progress !== entry.moveProgress) { + entry.moveProgress = progress; + updated = true; + } + if (entry.moveStatus !== "uploading") { + entry.moveStatus = "uploading"; + updated = true; + } + } else { + // Transfer görünmüyorsa queued kalır; done kararı aşağıda verilecek. + if (entry.moveStatus === "uploading") { + entry.moveStatus = "queued"; + updated = true; + } + } + }; + + for (const entry of torrents.values()) { + applyProgress(entry, `${RCLONE_REMOTE_PATH}/${entry.rootFolder || ""}`); + } + for (const job of youtubeJobs.values()) { + applyProgress(job, `${RCLONE_REMOTE_PATH}/${job.folderId || ""}`); + } + for (const job of mailruJobs.values()) { + const folderPrefix = job.folderId ? `${RCLONE_REMOTE_PATH}/${job.folderId}` : null; + if (folderPrefix) { + applyProgress(job, folderPrefix); + } + } + + const hasTransfers = transfers.length > 0; + if (!hasTransfers) { + const markDoneIfReady = (entry) => { + if (!entry || !entry.moveTotalBytes) return; + if (entry.moveStatus === "queued" || entry.moveStatus === "uploading") { + if ((entry.moveProgress || 0) >= 0.99) { + entry.moveStatus = "done"; + entry.moveProgress = 1; + updated = true; + } + } + }; + for (const entry of torrents.values()) markDoneIfReady(entry); + for (const job of youtubeJobs.values()) markDoneIfReady(job); + for (const job of mailruJobs.values()) markDoneIfReady(job); + } + return updated; +} + +function startRcloneStatsPolling() { + if (rcloneStatsTimer) return; + rcloneStatsTimer = setInterval(async () => { + const hasActive = Array.from(torrents.values()).some((e) => + ["queued", "uploading"].includes(e.moveStatus) + ) || + Array.from(youtubeJobs.values()).some((e) => + ["queued", "uploading"].includes(e.moveStatus) + ) || + Array.from(mailruJobs.values()).some((e) => + ["queued", "uploading"].includes(e.moveStatus) + ); + if (!hasActive) { + clearInterval(rcloneStatsTimer); + rcloneStatsTimer = null; + return; + } + const stats = await fetchRcloneStats(); + if (updateMoveProgressFromStats(stats)) { + scheduleSnapshotBroadcast(); + } + }, 2000); +} + function parseRcloneTokenFromText(text) { if (!text || !text.includes("access_token")) return null; const start = text.indexOf("{"); @@ -816,6 +952,7 @@ function loadRcloneSettings() { return { autoMove: false, autoMount: false, + cacheCleanMinutes: 0, remoteName: RCLONE_REMOTE_NAME, remotePath: RCLONE_REMOTE_PATH, mountDir: RCLONE_MOUNT_DIR, @@ -827,6 +964,7 @@ function loadRcloneSettings() { return { autoMove: Boolean(data.autoMove), autoMount: Boolean(data.autoMount), + cacheCleanMinutes: Number(data.cacheCleanMinutes) || 0, remoteName: data.remoteName || RCLONE_REMOTE_NAME, remotePath: data.remotePath || RCLONE_REMOTE_PATH, mountDir: data.mountDir || RCLONE_MOUNT_DIR, @@ -837,6 +975,7 @@ function loadRcloneSettings() { return { autoMove: false, autoMount: false, + cacheCleanMinutes: 0, remoteName: RCLONE_REMOTE_NAME, remotePath: RCLONE_REMOTE_PATH, mountDir: RCLONE_MOUNT_DIR, @@ -856,6 +995,55 @@ function saveRcloneSettings(partial) { return next; } +function startRcloneCacheCleanSchedule(minutes) { + if (rcloneCacheCleanTimer) { + clearInterval(rcloneCacheCleanTimer); + rcloneCacheCleanTimer = null; + } + const interval = Number(minutes); + if (!interval || interval <= 0) return; + rcloneCacheCleanTimer = setInterval(() => { + if (!RCLONE_RC_ENABLED) return; + fetch(`http://${RCLONE_RC_ADDR}/vfs/refresh`, { method: "POST" }) + .then((resp) => { + if (resp.status === 404) { + return fetch(`http://${RCLONE_RC_ADDR}/rc/vfs/refresh`, { + method: "POST" + }); + } + return resp; + }) + .then(() => { + console.log("🧹 Rclone cache temizleme tetiklendi."); + }) + .catch(() => { + console.warn("⚠️ Rclone cache temizleme başarısız."); + }); + }, interval * 60 * 1000); +} + +async function runRcloneCacheClean() { + const settings = loadRcloneSettings(); + const wasRunning = Boolean(rcloneProcess && !rcloneProcess.killed); + if (wasRunning) { + stopRcloneMount(); + } + try { + fs.rmSync(RCLONE_VFS_CACHE_DIR, { recursive: true, force: true }); + fs.mkdirSync(RCLONE_VFS_CACHE_DIR, { recursive: true }); + if (wasRunning) { + const result = startRcloneMount(settings); + if (!result.ok) { + return { ok: false, error: result.error || "Rclone yeniden başlatılamadı" }; + } + return { ok: true, method: "fs", restarted: true }; + } + return { ok: true, method: "fs", restarted: false }; + } catch (err) { + return { ok: false, error: err?.message || String(err) }; + } +} + function isRcloneMounted(mountDir) { if (!mountDir) return false; try { @@ -903,6 +1091,10 @@ function startRcloneMount(settings) { RCLONE_VFS_CACHE_MODE, "--cache-dir", RCLONE_VFS_CACHE_DIR, + "--vfs-cache-max-size", + RCLONE_VFS_CACHE_MAX_SIZE, + "--vfs-cache-max-age", + RCLONE_VFS_CACHE_MAX_AGE, "--dir-cache-time", RCLONE_DIR_CACHE_TIME, "--poll-interval", @@ -910,6 +1102,11 @@ function startRcloneMount(settings) { "--log-level", "INFO" ]; + if (RCLONE_RC_ENABLED) { + args.push("--rc"); + args.push("--rc-addr", RCLONE_RC_ADDR); + args.push("--rc-no-auth"); + } try { rcloneProcess = spawn("rclone", args, { @@ -1297,6 +1494,8 @@ function startYoutubeDownload(url, { moveToGdrive = false } = {}) { moveToGdrive: Boolean(moveToGdrive), moveStatus: "idle", moveError: null, + moveProgress: null, + moveTotalBytes: null, progress: 0, downloaded: 0, totalBytes: 0, @@ -1652,17 +1851,20 @@ async function finalizeYoutubeJob(job, exitCode) { console.log(`✅ YouTube indirmesi tamamlandı: ${job.title}`); if (job.moveToGdrive) { - job.moveStatus = "moving"; + job.moveStatus = "queued"; job.moveError = null; + job.moveProgress = 0; + job.moveTotalBytes = job.totalBytes || computeRootVideoBytes(job.folderId) || null; scheduleSnapshotBroadcast(); + startRcloneStatsPolling(); const moveResult = await moveRootFolderToGdrive(job.folderId); - if (moveResult.ok) { - job.moveStatus = "done"; - } else { - job.moveStatus = "error"; - job.moveError = moveResult.error || "GDrive taşıma hatası"; - logRcloneMoveError(`youtube:${job.id}`, job.moveError); - } + if (moveResult.ok) { + // Upload tamamlanma durumu RC stats ile belirlenecek + } else { + job.moveStatus = "error"; + job.moveError = moveResult.error || "GDrive taşıma hatası"; + logRcloneMoveError(`youtube:${job.id}`, job.moveError); + } broadcastFileUpdate("downloads"); scheduleSnapshotBroadcast(); } @@ -1818,7 +2020,9 @@ function mailruSnapshot(job) { status: job.state, moveToGdrive: job.moveToGdrive || false, moveStatus: job.moveStatus || "idle", - moveError: job.moveError || null + moveError: job.moveError || null, + moveProgress: job.moveProgress ?? null, + moveTotalBytes: job.moveTotalBytes ?? null }; } @@ -1993,12 +2197,15 @@ async function finalizeMailRuJob(job, exitCode) { console.log(`✅ Mail.ru indirmesi tamamlandı: ${job.title}`); if (job.moveToGdrive) { - job.moveStatus = "moving"; + job.moveStatus = "queued"; job.moveError = null; + job.moveProgress = 0; + job.moveTotalBytes = job.totalBytes || null; scheduleSnapshotBroadcast(); + startRcloneStatsPolling(); const moveResult = await movePathToGdrive(relPath); if (moveResult.ok) { - job.moveStatus = "done"; + // Upload tamamlanma durumu RC stats ile belirlenecek } else { job.moveStatus = "error"; job.moveError = moveResult.error || "GDrive taşıma hatası"; @@ -2101,6 +2308,8 @@ async function startMailRuDownload(url, { moveToGdrive = false } = {}) { moveToGdrive: Boolean(moveToGdrive), moveStatus: "idle", moveError: null, + moveProgress: null, + moveTotalBytes: null, state: "awaiting_match", progress: 0, downloaded: 0, @@ -2390,7 +2599,9 @@ function youtubeSnapshot(job) { status: job.state, moveToGdrive: job.moveToGdrive || false, moveStatus: job.moveStatus || "idle", - moveError: job.moveError || null + moveError: job.moveError || null, + moveProgress: job.moveProgress ?? null, + moveTotalBytes: job.moveTotalBytes ?? null }; } @@ -6471,7 +6682,9 @@ function snapshot() { thumbnail: entry?.thumbnail || null, moveToGdrive: entry?.moveToGdrive || false, moveStatus: entry?.moveStatus || "idle", - moveError: entry?.moveError || null + moveError: entry?.moveError || null, + moveProgress: entry?.moveProgress ?? null, + moveTotalBytes: entry?.moveTotalBytes ?? null }; } ); @@ -6507,7 +6720,9 @@ function wireTorrent( rootFolder: savePath ? path.basename(savePath) : null, moveToGdrive: Boolean(moveToGdrive), moveStatus: "idle", - moveError: null + moveError: null, + moveProgress: null, + moveTotalBytes: null }); const scheduleTorrentSnapshot = () => scheduleSnapshotBroadcast(); @@ -6820,12 +7035,23 @@ async function onTorrentDone({ torrent }) { } if (entry.moveToGdrive) { - entry.moveStatus = "moving"; + const paused = pauseTorrentEntry(entry); + if (paused) { + console.log(`⏸️ GDrive taşıma için torrent durduruldu: ${entry.infoHash}`); + } + entry.moveStatus = "queued"; entry.moveError = null; + entry.moveProgress = 0; + entry.moveTotalBytes = + entry.totalBytes || + torrent?.length || + computeRootVideoBytes(rootFolder) || + null; scheduleSnapshotBroadcast(); + startRcloneStatsPolling(); const moveResult = await moveRootFolderToGdrive(rootFolder); if (moveResult.ok) { - entry.moveStatus = "done"; + // Upload tamamlanma durumu RC stats ile belirlenecek } else { entry.moveStatus = "error"; entry.moveError = moveResult.error || "GDrive taşıma hatası"; @@ -8771,6 +8997,7 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => { configPath: settings.configPath, autoMove: settings.autoMove, autoMount: settings.autoMount, + cacheCleanMinutes: settings.cacheCleanMinutes || 0, configExists: fs.existsSync(settings.configPath), remoteConfigured: rcloneConfigHasRemote(settings.remoteName), lastError: rcloneLastError || null @@ -8779,15 +9006,17 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => { app.post("/api/rclone/settings", requireAuth, (req, res) => { try { - const { autoMove, autoMount, remoteName, remotePath, mountDir } = req.body || {}; + const { autoMove, autoMount, remoteName, remotePath, mountDir, cacheCleanMinutes } = req.body || {}; const next = saveRcloneSettings({ autoMove: Boolean(autoMove), autoMount: Boolean(autoMount), + cacheCleanMinutes: Number(cacheCleanMinutes) || 0, remoteName: remoteName || RCLONE_REMOTE_NAME, remotePath: remotePath || RCLONE_REMOTE_PATH, mountDir: mountDir || RCLONE_MOUNT_DIR, configPath: RCLONE_CONFIG_PATH }); + startRcloneCacheCleanSchedule(next.cacheCleanMinutes); res.json({ ok: true, settings: next }); } catch (err) { res.status(500).json({ ok: false, error: err?.message || String(err) }); @@ -8905,6 +9134,48 @@ app.post("/api/rclone/mount", requireAuth, (req, res) => { return res.json({ ok: true, ...result }); }); +app.post("/api/rclone/cache/clean", requireAuth, async (req, res) => { + const result = await runRcloneCacheClean(); + if (!result.ok) { + return res.status(500).json({ ok: false, error: result.error }); + } + return res.json({ ok: true, ...result }); +}); + +app.get("/api/rclone/conf", requireAuth, (req, res) => { + try { + if (!fs.existsSync(RCLONE_CONFIG_PATH)) { + return res.json({ ok: true, content: "" }); + } + const content = fs.readFileSync(RCLONE_CONFIG_PATH, "utf-8"); + res.json({ ok: true, content }); + } catch (err) { + res.status(500).json({ ok: false, error: err?.message || String(err) }); + } +}); + +app.post("/api/rclone/conf", requireAuth, (req, res) => { + try { + const content = String(req.body?.content || ""); + if (!content.trim()) { + return res.status(400).json({ ok: false, error: "Boş içerik gönderilemez." }); + } + ensureDirForFile(RCLONE_CONFIG_PATH); + fs.writeFileSync(RCLONE_CONFIG_PATH, content, "utf-8"); + const settings = loadRcloneSettings(); + if (rcloneProcess && !rcloneProcess.killed) { + stopRcloneMount(); + const restart = startRcloneMount(settings); + if (!restart.ok) { + return res.status(500).json({ ok: false, error: restart.error || "Rclone yeniden başlatılamadı." }); + } + } + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ ok: false, error: err?.message || String(err) }); + } +}); + app.post("/api/rclone/unmount", requireAuth, (req, res) => { const result = stopRcloneMount(); if (!result.ok) { @@ -10180,6 +10451,7 @@ if (RCLONE_ENABLED && initialRcloneSettings.autoMount) { console.warn(`⚠️ Rclone mount başlatılamadı: ${result.error}`); } } +startRcloneCacheCleanSchedule(initialRcloneSettings.cacheCleanMinutes || 0); // --- ✅ Client build (frontend) dosyalarını sun --- const publicDir = path.join(__dirname, "public");