feat(rclone): RC API ilerleme takibi ve conf editörü ekle
- 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ı.
This commit is contained in:
@@ -63,6 +63,12 @@ RCLONE_DIR_CACHE_TIME=1m
|
|||||||
RCLONE_VFS_CACHE_MODE=full
|
RCLONE_VFS_CACHE_MODE=full
|
||||||
# Rclone VFS cache dizini
|
# Rclone VFS cache dizini
|
||||||
RCLONE_VFS_CACHE_DIR=/app/server/cache/rclone-vfs
|
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 log (taşıma hatalarını detaylı loglamak için)
|
||||||
RCLONE_DEBUG_MODE_LOG=0
|
RCLONE_DEBUG_MODE_LOG=0
|
||||||
# Media stream debug log (akış kaynağını loglamak için)
|
# Media stream debug log (akış kaynağını loglamak için)
|
||||||
|
|||||||
@@ -25,15 +25,11 @@
|
|||||||
let rcloneStatus = null;
|
let rcloneStatus = null;
|
||||||
let rcloneLoading = false;
|
let rcloneLoading = false;
|
||||||
let rcloneSaving = false;
|
let rcloneSaving = false;
|
||||||
let rcloneAuthUrl = "";
|
|
||||||
let rcloneToken = "";
|
|
||||||
let rcloneClientId = "";
|
|
||||||
let rcloneClientSecret = "";
|
|
||||||
let rcloneAutoMove = false;
|
let rcloneAutoMove = false;
|
||||||
let rcloneAutoMount = false;
|
let rcloneAutoMount = false;
|
||||||
let rcloneRemoteName = "";
|
let rcloneCacheCleanMinutes = 0;
|
||||||
let rcloneRemotePath = "";
|
let rcloneConfText = "";
|
||||||
let rcloneMountDir = "";
|
let rcloneConfVisible = false;
|
||||||
|
|
||||||
async function loadCookies() {
|
async function loadCookies() {
|
||||||
loadingCookies = true;
|
loadingCookies = true;
|
||||||
@@ -135,9 +131,7 @@
|
|||||||
rcloneStatus = data;
|
rcloneStatus = data;
|
||||||
rcloneAutoMove = Boolean(data?.autoMove);
|
rcloneAutoMove = Boolean(data?.autoMove);
|
||||||
rcloneAutoMount = Boolean(data?.autoMount);
|
rcloneAutoMount = Boolean(data?.autoMount);
|
||||||
rcloneRemoteName = data?.remoteName || "";
|
rcloneCacheCleanMinutes = Number(data?.cacheCleanMinutes) || 0;
|
||||||
rcloneRemotePath = data?.remotePath || "";
|
|
||||||
rcloneMountDir = data?.mountDir || "";
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err?.message || "Rclone durumu alınamadı.";
|
error = err?.message || "Rclone durumu alınamadı.";
|
||||||
} finally {
|
} 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() {
|
async function saveRcloneSettings() {
|
||||||
if (rcloneSaving) return;
|
if (rcloneSaving) return;
|
||||||
rcloneSaving = true;
|
rcloneSaving = true;
|
||||||
@@ -157,15 +164,22 @@
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
autoMove: rcloneAutoMove,
|
autoMove: rcloneAutoMove,
|
||||||
autoMount: rcloneAutoMount,
|
autoMount: rcloneAutoMount,
|
||||||
remoteName: rcloneRemoteName,
|
cacheCleanMinutes: rcloneCacheCleanMinutes
|
||||||
remotePath: rcloneRemotePath,
|
|
||||||
mountDir: rcloneMountDir
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok || !data?.ok) {
|
if (!resp.ok || !data?.ok) {
|
||||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
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.";
|
success = "Rclone ayarları kaydedildi.";
|
||||||
await loadRcloneStatus();
|
await loadRcloneStatus();
|
||||||
} catch (err) {
|
} 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() {
|
async function startRcloneMount() {
|
||||||
error = null;
|
error = null;
|
||||||
success = 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(() => {
|
onMount(() => {
|
||||||
loadCookies();
|
loadCookies();
|
||||||
loadYoutubeSettings();
|
loadYoutubeSettings();
|
||||||
loadRcloneStatus();
|
loadRcloneStatus();
|
||||||
|
loadRcloneConf();
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatDate(ts) {
|
function formatDate(ts) {
|
||||||
@@ -318,7 +304,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions left">
|
||||||
<button class="btn" on:click={loadYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}>
|
<button class="btn" on:click={loadYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}>
|
||||||
<i class="fa-solid fa-rotate"></i> Yenile
|
<i class="fa-solid fa-rotate"></i> Yenile
|
||||||
</button>
|
</button>
|
||||||
@@ -407,73 +393,47 @@
|
|||||||
|
|
||||||
<div class="field inline compact left-align">
|
<div class="field inline compact left-align">
|
||||||
<div class="inline-field">
|
<div class="inline-field">
|
||||||
<label for="rclone-remote">Remote adı</label>
|
<label for="rclone-cache-clean">Cache temizleme (dakika)</label>
|
||||||
<input
|
<input
|
||||||
id="rclone-remote"
|
id="rclone-cache-clean"
|
||||||
type="text"
|
type="number"
|
||||||
bind:value={rcloneRemoteName}
|
min="0"
|
||||||
disabled={rcloneLoading || rcloneSaving}
|
step="1"
|
||||||
/>
|
bind:value={rcloneCacheCleanMinutes}
|
||||||
</div>
|
|
||||||
<div class="inline-field">
|
|
||||||
<label for="rclone-path">Drive klasörü</label>
|
|
||||||
<input
|
|
||||||
id="rclone-path"
|
|
||||||
type="text"
|
|
||||||
bind:value={rcloneRemotePath}
|
|
||||||
disabled={rcloneLoading || rcloneSaving}
|
disabled={rcloneLoading || rcloneSaving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn" on:click={cleanRcloneCache}>
|
||||||
|
<i class="fa-solid fa-broom"></i> Clean Cache
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="rclone-mount">Mount dizini</label>
|
<label>rclone.conf</label>
|
||||||
<input
|
<div class="password-field">
|
||||||
id="rclone-mount"
|
<textarea
|
||||||
type="text"
|
class="conf-textarea {rcloneConfVisible ? '' : 'masked'}"
|
||||||
bind:value={rcloneMountDir}
|
bind:value={rcloneConfText}
|
||||||
disabled={rcloneLoading || rcloneSaving}
|
placeholder="rclone.conf içeriğini yapıştırın"
|
||||||
/>
|
></textarea>
|
||||||
|
<button
|
||||||
|
class="eye-btn"
|
||||||
|
type="button"
|
||||||
|
on:click={() => (rcloneConfVisible = !rcloneConfVisible)}
|
||||||
|
aria-label="Gizli/Görünür"
|
||||||
|
>
|
||||||
|
<i class={rcloneConfVisible ? "fa-solid fa-eye-slash" : "fa-solid fa-eye"}></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn" on:click={loadRcloneStatus} disabled={rcloneLoading || rcloneSaving}>
|
|
||||||
<i class="fa-solid fa-rotate"></i> Yenile
|
|
||||||
</button>
|
|
||||||
<button class="btn primary" on:click={saveRcloneSettings} disabled={rcloneLoading || rcloneSaving}>
|
<button class="btn primary" on:click={saveRcloneSettings} disabled={rcloneLoading || rcloneSaving}>
|
||||||
<i class="fa-solid fa-floppy-disk"></i> Kaydet
|
<i class="fa-solid fa-floppy-disk"></i> Kaydet
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label>Yetkilendirme</label>
|
|
||||||
<div class="actions">
|
|
||||||
<button class="btn" on:click={requestRcloneAuthUrl}>
|
|
||||||
<i class="fa-solid fa-link"></i> Auth URL al
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{#if rcloneAuthUrl}
|
|
||||||
<input type="text" readonly value={rcloneAuthUrl} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="rclone-client-id">Client ID (opsiyonel)</label>
|
|
||||||
<input id="rclone-client-id" type="text" bind:value={rcloneClientId} />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="rclone-client-secret">Client Secret (opsiyonel)</label>
|
|
||||||
<input id="rclone-client-secret" type="text" bind:value={rcloneClientSecret} />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="rclone-token">Token</label>
|
|
||||||
<textarea id="rclone-token" bind:value={rcloneToken} placeholder="rclone authorize çıktısındaki token JSON'u"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn" on:click={applyRcloneToken}>
|
|
||||||
<i class="fa-solid fa-key"></i> Token Kaydet
|
|
||||||
</button>
|
|
||||||
<button class="btn" on:click={startRcloneMount}>
|
<button class="btn" on:click={startRcloneMount}>
|
||||||
<i class="fa-solid fa-play"></i> Mount Başlat
|
<i class="fa-solid fa-play"></i> Mount Başlat
|
||||||
</button>
|
</button>
|
||||||
@@ -707,4 +667,32 @@
|
|||||||
background: #e5ffe7;
|
background: #e5ffe7;
|
||||||
color: #0f7a1f;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -855,18 +855,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="progress-bar">
|
<div class="progress-bar {t.moveStatus === 'uploading' ? 'uploading' : ''}">
|
||||||
<div
|
<div
|
||||||
class="progress"
|
class="progress"
|
||||||
style="width:{(t.progress || 0) * 100}%"
|
style="width:{(t.moveStatus === 'uploading' ? (t.moveProgress || 0) : (t.progress || 0)) * 100}%"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="progress-text">
|
<div class="progress-text">
|
||||||
{#if t.type === "mailru" && t.status === "awaiting_match"}
|
{#if t.type === "mailru" && t.status === "awaiting_match"}
|
||||||
Eşleştirme bekleniyor
|
Eşleştirme bekleniyor
|
||||||
{:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "moving"}
|
{:else if t.moveToGdrive && t.moveStatus === "queued"}
|
||||||
GDrive'a Taşınıyor.. • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB
|
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"}
|
{:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "done"}
|
||||||
GDrive ✓ • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB
|
GDrive ✓ • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB
|
||||||
{:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "error"}
|
{:else if (t.progress || 0) >= 1 && t.moveToGdrive && t.moveStatus === "error"}
|
||||||
@@ -1235,6 +1237,10 @@
|
|||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-bar.uploading .progress {
|
||||||
|
background: linear-gradient(90deg, #ef4444, #b91c1c);
|
||||||
|
}
|
||||||
|
|
||||||
.torrent-error {
|
.torrent-error {
|
||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
310
server/server.js
310
server/server.js
@@ -56,6 +56,14 @@ const RCLONE_VFS_CACHE_MODE =
|
|||||||
const RCLONE_DEBUG_MODE_LOG = ["1", "true", "yes", "on"].includes(
|
const RCLONE_DEBUG_MODE_LOG = ["1", "true", "yes", "on"].includes(
|
||||||
String(process.env.RCLONE_DEBUG_MODE_LOG || "").toLowerCase()
|
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(
|
const MEDIA_DEBUG_LOG = ["1", "true", "yes", "on"].includes(
|
||||||
String(process.env.MEDIA_DEBUG_LOG || "").toLowerCase()
|
String(process.env.MEDIA_DEBUG_LOG || "").toLowerCase()
|
||||||
);
|
);
|
||||||
@@ -753,6 +761,7 @@ function resolveRootDir(rootFolder) {
|
|||||||
let rcloneProcess = null;
|
let rcloneProcess = null;
|
||||||
let rcloneLastError = null;
|
let rcloneLastError = null;
|
||||||
const rcloneAuthSessions = new Map();
|
const rcloneAuthSessions = new Map();
|
||||||
|
let rcloneCacheCleanTimer = null;
|
||||||
|
|
||||||
function logRcloneMoveError(context, error) {
|
function logRcloneMoveError(context, error) {
|
||||||
if (!error) return;
|
if (!error) return;
|
||||||
@@ -764,10 +773,11 @@ function logRcloneMoveError(context, error) {
|
|||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
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;
|
if (!filePath || !fs.existsSync(filePath)) return false;
|
||||||
let prevSize = null;
|
let prevSize = null;
|
||||||
let prevMtime = null;
|
let prevMtime = null;
|
||||||
|
let stableCount = 0;
|
||||||
for (let i = 0; i < attempts; i += 1) {
|
for (let i = 0; i < attempts; i += 1) {
|
||||||
let stat;
|
let stat;
|
||||||
try {
|
try {
|
||||||
@@ -778,7 +788,12 @@ async function waitForFileStable(filePath, attempts = 5, intervalMs = 2000) {
|
|||||||
const size = stat.size;
|
const size = stat.size;
|
||||||
const mtime = stat.mtimeMs;
|
const mtime = stat.mtimeMs;
|
||||||
if (prevSize !== null && prevMtime !== null) {
|
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;
|
prevSize = size;
|
||||||
prevMtime = mtime;
|
prevMtime = mtime;
|
||||||
@@ -787,6 +802,127 @@ async function waitForFileStable(filePath, attempts = 5, intervalMs = 2000) {
|
|||||||
return false;
|
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) {
|
function parseRcloneTokenFromText(text) {
|
||||||
if (!text || !text.includes("access_token")) return null;
|
if (!text || !text.includes("access_token")) return null;
|
||||||
const start = text.indexOf("{");
|
const start = text.indexOf("{");
|
||||||
@@ -816,6 +952,7 @@ function loadRcloneSettings() {
|
|||||||
return {
|
return {
|
||||||
autoMove: false,
|
autoMove: false,
|
||||||
autoMount: false,
|
autoMount: false,
|
||||||
|
cacheCleanMinutes: 0,
|
||||||
remoteName: RCLONE_REMOTE_NAME,
|
remoteName: RCLONE_REMOTE_NAME,
|
||||||
remotePath: RCLONE_REMOTE_PATH,
|
remotePath: RCLONE_REMOTE_PATH,
|
||||||
mountDir: RCLONE_MOUNT_DIR,
|
mountDir: RCLONE_MOUNT_DIR,
|
||||||
@@ -827,6 +964,7 @@ function loadRcloneSettings() {
|
|||||||
return {
|
return {
|
||||||
autoMove: Boolean(data.autoMove),
|
autoMove: Boolean(data.autoMove),
|
||||||
autoMount: Boolean(data.autoMount),
|
autoMount: Boolean(data.autoMount),
|
||||||
|
cacheCleanMinutes: Number(data.cacheCleanMinutes) || 0,
|
||||||
remoteName: data.remoteName || RCLONE_REMOTE_NAME,
|
remoteName: data.remoteName || RCLONE_REMOTE_NAME,
|
||||||
remotePath: data.remotePath || RCLONE_REMOTE_PATH,
|
remotePath: data.remotePath || RCLONE_REMOTE_PATH,
|
||||||
mountDir: data.mountDir || RCLONE_MOUNT_DIR,
|
mountDir: data.mountDir || RCLONE_MOUNT_DIR,
|
||||||
@@ -837,6 +975,7 @@ function loadRcloneSettings() {
|
|||||||
return {
|
return {
|
||||||
autoMove: false,
|
autoMove: false,
|
||||||
autoMount: false,
|
autoMount: false,
|
||||||
|
cacheCleanMinutes: 0,
|
||||||
remoteName: RCLONE_REMOTE_NAME,
|
remoteName: RCLONE_REMOTE_NAME,
|
||||||
remotePath: RCLONE_REMOTE_PATH,
|
remotePath: RCLONE_REMOTE_PATH,
|
||||||
mountDir: RCLONE_MOUNT_DIR,
|
mountDir: RCLONE_MOUNT_DIR,
|
||||||
@@ -856,6 +995,55 @@ function saveRcloneSettings(partial) {
|
|||||||
return next;
|
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) {
|
function isRcloneMounted(mountDir) {
|
||||||
if (!mountDir) return false;
|
if (!mountDir) return false;
|
||||||
try {
|
try {
|
||||||
@@ -903,6 +1091,10 @@ function startRcloneMount(settings) {
|
|||||||
RCLONE_VFS_CACHE_MODE,
|
RCLONE_VFS_CACHE_MODE,
|
||||||
"--cache-dir",
|
"--cache-dir",
|
||||||
RCLONE_VFS_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",
|
"--dir-cache-time",
|
||||||
RCLONE_DIR_CACHE_TIME,
|
RCLONE_DIR_CACHE_TIME,
|
||||||
"--poll-interval",
|
"--poll-interval",
|
||||||
@@ -910,6 +1102,11 @@ function startRcloneMount(settings) {
|
|||||||
"--log-level",
|
"--log-level",
|
||||||
"INFO"
|
"INFO"
|
||||||
];
|
];
|
||||||
|
if (RCLONE_RC_ENABLED) {
|
||||||
|
args.push("--rc");
|
||||||
|
args.push("--rc-addr", RCLONE_RC_ADDR);
|
||||||
|
args.push("--rc-no-auth");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rcloneProcess = spawn("rclone", args, {
|
rcloneProcess = spawn("rclone", args, {
|
||||||
@@ -1297,6 +1494,8 @@ function startYoutubeDownload(url, { moveToGdrive = false } = {}) {
|
|||||||
moveToGdrive: Boolean(moveToGdrive),
|
moveToGdrive: Boolean(moveToGdrive),
|
||||||
moveStatus: "idle",
|
moveStatus: "idle",
|
||||||
moveError: null,
|
moveError: null,
|
||||||
|
moveProgress: null,
|
||||||
|
moveTotalBytes: null,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
downloaded: 0,
|
downloaded: 0,
|
||||||
totalBytes: 0,
|
totalBytes: 0,
|
||||||
@@ -1652,17 +1851,20 @@ async function finalizeYoutubeJob(job, exitCode) {
|
|||||||
console.log(`✅ YouTube indirmesi tamamlandı: ${job.title}`);
|
console.log(`✅ YouTube indirmesi tamamlandı: ${job.title}`);
|
||||||
|
|
||||||
if (job.moveToGdrive) {
|
if (job.moveToGdrive) {
|
||||||
job.moveStatus = "moving";
|
job.moveStatus = "queued";
|
||||||
job.moveError = null;
|
job.moveError = null;
|
||||||
|
job.moveProgress = 0;
|
||||||
|
job.moveTotalBytes = job.totalBytes || computeRootVideoBytes(job.folderId) || null;
|
||||||
scheduleSnapshotBroadcast();
|
scheduleSnapshotBroadcast();
|
||||||
|
startRcloneStatsPolling();
|
||||||
const moveResult = await moveRootFolderToGdrive(job.folderId);
|
const moveResult = await moveRootFolderToGdrive(job.folderId);
|
||||||
if (moveResult.ok) {
|
if (moveResult.ok) {
|
||||||
job.moveStatus = "done";
|
// Upload tamamlanma durumu RC stats ile belirlenecek
|
||||||
} else {
|
} else {
|
||||||
job.moveStatus = "error";
|
job.moveStatus = "error";
|
||||||
job.moveError = moveResult.error || "GDrive taşıma hatası";
|
job.moveError = moveResult.error || "GDrive taşıma hatası";
|
||||||
logRcloneMoveError(`youtube:${job.id}`, job.moveError);
|
logRcloneMoveError(`youtube:${job.id}`, job.moveError);
|
||||||
}
|
}
|
||||||
broadcastFileUpdate("downloads");
|
broadcastFileUpdate("downloads");
|
||||||
scheduleSnapshotBroadcast();
|
scheduleSnapshotBroadcast();
|
||||||
}
|
}
|
||||||
@@ -1818,7 +2020,9 @@ function mailruSnapshot(job) {
|
|||||||
status: job.state,
|
status: job.state,
|
||||||
moveToGdrive: job.moveToGdrive || false,
|
moveToGdrive: job.moveToGdrive || false,
|
||||||
moveStatus: job.moveStatus || "idle",
|
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}`);
|
console.log(`✅ Mail.ru indirmesi tamamlandı: ${job.title}`);
|
||||||
|
|
||||||
if (job.moveToGdrive) {
|
if (job.moveToGdrive) {
|
||||||
job.moveStatus = "moving";
|
job.moveStatus = "queued";
|
||||||
job.moveError = null;
|
job.moveError = null;
|
||||||
|
job.moveProgress = 0;
|
||||||
|
job.moveTotalBytes = job.totalBytes || null;
|
||||||
scheduleSnapshotBroadcast();
|
scheduleSnapshotBroadcast();
|
||||||
|
startRcloneStatsPolling();
|
||||||
const moveResult = await movePathToGdrive(relPath);
|
const moveResult = await movePathToGdrive(relPath);
|
||||||
if (moveResult.ok) {
|
if (moveResult.ok) {
|
||||||
job.moveStatus = "done";
|
// Upload tamamlanma durumu RC stats ile belirlenecek
|
||||||
} else {
|
} else {
|
||||||
job.moveStatus = "error";
|
job.moveStatus = "error";
|
||||||
job.moveError = moveResult.error || "GDrive taşıma hatası";
|
job.moveError = moveResult.error || "GDrive taşıma hatası";
|
||||||
@@ -2101,6 +2308,8 @@ async function startMailRuDownload(url, { moveToGdrive = false } = {}) {
|
|||||||
moveToGdrive: Boolean(moveToGdrive),
|
moveToGdrive: Boolean(moveToGdrive),
|
||||||
moveStatus: "idle",
|
moveStatus: "idle",
|
||||||
moveError: null,
|
moveError: null,
|
||||||
|
moveProgress: null,
|
||||||
|
moveTotalBytes: null,
|
||||||
state: "awaiting_match",
|
state: "awaiting_match",
|
||||||
progress: 0,
|
progress: 0,
|
||||||
downloaded: 0,
|
downloaded: 0,
|
||||||
@@ -2390,7 +2599,9 @@ function youtubeSnapshot(job) {
|
|||||||
status: job.state,
|
status: job.state,
|
||||||
moveToGdrive: job.moveToGdrive || false,
|
moveToGdrive: job.moveToGdrive || false,
|
||||||
moveStatus: job.moveStatus || "idle",
|
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,
|
thumbnail: entry?.thumbnail || null,
|
||||||
moveToGdrive: entry?.moveToGdrive || false,
|
moveToGdrive: entry?.moveToGdrive || false,
|
||||||
moveStatus: entry?.moveStatus || "idle",
|
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,
|
rootFolder: savePath ? path.basename(savePath) : null,
|
||||||
moveToGdrive: Boolean(moveToGdrive),
|
moveToGdrive: Boolean(moveToGdrive),
|
||||||
moveStatus: "idle",
|
moveStatus: "idle",
|
||||||
moveError: null
|
moveError: null,
|
||||||
|
moveProgress: null,
|
||||||
|
moveTotalBytes: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const scheduleTorrentSnapshot = () => scheduleSnapshotBroadcast();
|
const scheduleTorrentSnapshot = () => scheduleSnapshotBroadcast();
|
||||||
@@ -6820,12 +7035,23 @@ async function onTorrentDone({ torrent }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entry.moveToGdrive) {
|
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.moveError = null;
|
||||||
|
entry.moveProgress = 0;
|
||||||
|
entry.moveTotalBytes =
|
||||||
|
entry.totalBytes ||
|
||||||
|
torrent?.length ||
|
||||||
|
computeRootVideoBytes(rootFolder) ||
|
||||||
|
null;
|
||||||
scheduleSnapshotBroadcast();
|
scheduleSnapshotBroadcast();
|
||||||
|
startRcloneStatsPolling();
|
||||||
const moveResult = await moveRootFolderToGdrive(rootFolder);
|
const moveResult = await moveRootFolderToGdrive(rootFolder);
|
||||||
if (moveResult.ok) {
|
if (moveResult.ok) {
|
||||||
entry.moveStatus = "done";
|
// Upload tamamlanma durumu RC stats ile belirlenecek
|
||||||
} else {
|
} else {
|
||||||
entry.moveStatus = "error";
|
entry.moveStatus = "error";
|
||||||
entry.moveError = moveResult.error || "GDrive taşıma hatası";
|
entry.moveError = moveResult.error || "GDrive taşıma hatası";
|
||||||
@@ -8771,6 +8997,7 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => {
|
|||||||
configPath: settings.configPath,
|
configPath: settings.configPath,
|
||||||
autoMove: settings.autoMove,
|
autoMove: settings.autoMove,
|
||||||
autoMount: settings.autoMount,
|
autoMount: settings.autoMount,
|
||||||
|
cacheCleanMinutes: settings.cacheCleanMinutes || 0,
|
||||||
configExists: fs.existsSync(settings.configPath),
|
configExists: fs.existsSync(settings.configPath),
|
||||||
remoteConfigured: rcloneConfigHasRemote(settings.remoteName),
|
remoteConfigured: rcloneConfigHasRemote(settings.remoteName),
|
||||||
lastError: rcloneLastError || null
|
lastError: rcloneLastError || null
|
||||||
@@ -8779,15 +9006,17 @@ app.get("/api/rclone/status", requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
app.post("/api/rclone/settings", requireAuth, (req, res) => {
|
app.post("/api/rclone/settings", requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { autoMove, autoMount, remoteName, remotePath, mountDir } = req.body || {};
|
const { autoMove, autoMount, remoteName, remotePath, mountDir, cacheCleanMinutes } = req.body || {};
|
||||||
const next = saveRcloneSettings({
|
const next = saveRcloneSettings({
|
||||||
autoMove: Boolean(autoMove),
|
autoMove: Boolean(autoMove),
|
||||||
autoMount: Boolean(autoMount),
|
autoMount: Boolean(autoMount),
|
||||||
|
cacheCleanMinutes: Number(cacheCleanMinutes) || 0,
|
||||||
remoteName: remoteName || RCLONE_REMOTE_NAME,
|
remoteName: remoteName || RCLONE_REMOTE_NAME,
|
||||||
remotePath: remotePath || RCLONE_REMOTE_PATH,
|
remotePath: remotePath || RCLONE_REMOTE_PATH,
|
||||||
mountDir: mountDir || RCLONE_MOUNT_DIR,
|
mountDir: mountDir || RCLONE_MOUNT_DIR,
|
||||||
configPath: RCLONE_CONFIG_PATH
|
configPath: RCLONE_CONFIG_PATH
|
||||||
});
|
});
|
||||||
|
startRcloneCacheCleanSchedule(next.cacheCleanMinutes);
|
||||||
res.json({ ok: true, settings: next });
|
res.json({ ok: true, settings: next });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ ok: false, error: err?.message || String(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 });
|
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) => {
|
app.post("/api/rclone/unmount", requireAuth, (req, res) => {
|
||||||
const result = stopRcloneMount();
|
const result = stopRcloneMount();
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
@@ -10180,6 +10451,7 @@ if (RCLONE_ENABLED && initialRcloneSettings.autoMount) {
|
|||||||
console.warn(`⚠️ Rclone mount başlatılamadı: ${result.error}`);
|
console.warn(`⚠️ Rclone mount başlatılamadı: ${result.error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
startRcloneCacheCleanSchedule(initialRcloneSettings.cacheCleanMinutes || 0);
|
||||||
|
|
||||||
// --- ✅ Client build (frontend) dosyalarını sun ---
|
// --- ✅ Client build (frontend) dosyalarını sun ---
|
||||||
const publicDir = path.join(__dirname, "public");
|
const publicDir = path.join(__dirname, "public");
|
||||||
|
|||||||
Reference in New Issue
Block a user