feat(rclone): Google Drive entegrasyonu ekle
Dockerfile ve docker-compose yapılandırması Rclone ve FUSE için güncellendi. Backend API'leri Rclone durumunu, ayarlarını, yetkilendirmesini ve mount işlemlerini yönetmek için eklendi. İndirmeler tamamlandığında (Torrent, YouTube, Mail.ru) dosyaların otomatik veya manuel olarak Google Drive'a taşınması sağlandı. Dosya sistemi hem yerel hem de mount edilmiş GDrive yollarını destekleyecek şekilde güncellendi. Ayarlar ve Dosyalar arayüzüne ilgili kontroller eklendi.
This commit is contained in:
24
.env.example
24
.env.example
@@ -43,3 +43,27 @@ WEBDAV_PATH=/webdav
|
||||
WEBDAV_READONLY=1
|
||||
# WebDAV index yeniden oluşturma süresi (ms).
|
||||
WEBDAV_INDEX_TTL=60000
|
||||
|
||||
# --- Rclone / Google Drive ---
|
||||
# Rclone entegrasyonunu aç/kapat
|
||||
RCLONE_ENABLED=0
|
||||
# Rclone config dosyası konumu (container içinde)
|
||||
RCLONE_CONFIG_PATH=/config/rclone/rclone.conf
|
||||
# Google Drive mount edilecek dizin (container içinde)
|
||||
RCLONE_MOUNT_DIR=/app/server/gdrive
|
||||
# Rclone remote adı
|
||||
RCLONE_REMOTE_NAME=dupe
|
||||
# Google Drive içinde kullanılacak klasör adı
|
||||
RCLONE_REMOTE_PATH=Dupe
|
||||
# Rclone mount tazeleme/poll süresi
|
||||
RCLONE_POLL_INTERVAL=1m
|
||||
# Rclone dizin cache süresi
|
||||
RCLONE_DIR_CACHE_TIME=1m
|
||||
# Rclone VFS cache modu (off, minimal, writes, full)
|
||||
RCLONE_VFS_CACHE_MODE=full
|
||||
# Rclone VFS cache dizini
|
||||
RCLONE_VFS_CACHE_DIR=/app/server/cache/rclone-vfs
|
||||
# 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)
|
||||
MEDIA_DEBUG_LOG=0
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN npm run build
|
||||
|
||||
# Build server
|
||||
FROM node:22-slim
|
||||
RUN apt-get update && apt-get install -y ffmpeg curl aria2 && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y ffmpeg curl aria2 rclone fuse3 && rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp \
|
||||
&& chmod a+rx /usr/local/bin/yt-dlp
|
||||
WORKDIR /app/server
|
||||
|
||||
@@ -604,6 +604,27 @@
|
||||
refreshMovieCount();
|
||||
refreshTvShowCount();
|
||||
}
|
||||
|
||||
async function moveToGdrive(entry) {
|
||||
if (!entry?.name) return;
|
||||
const ok = confirm("Bu öğe GDrive'a taşınsın mı?");
|
||||
if (!ok) return;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/move", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path: entry.name })
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.ok) {
|
||||
alert(data?.error || "GDrive taşıma başarısız oldu.");
|
||||
return;
|
||||
}
|
||||
await loadFiles();
|
||||
} catch (err) {
|
||||
alert(err?.message || "GDrive taşıma başarısız oldu.");
|
||||
}
|
||||
}
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return "0 MB";
|
||||
if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
|
||||
@@ -1237,7 +1258,7 @@
|
||||
const button = event.currentTarget;
|
||||
const rect = button.getBoundingClientRect();
|
||||
const menuWidth = 160;
|
||||
const menuHeight = 140; // Yaklaşık menü yüksekliği
|
||||
const menuHeight = 180; // Yaklaşık menü yüksekliği
|
||||
|
||||
// Üç noktanın son noktası ile menünün sol kenarını hizala
|
||||
// Düğme genişliği 34px, son nokta sağ kenara yakın
|
||||
@@ -2389,6 +2410,14 @@
|
||||
</button>
|
||||
<div class="menu-divider"></div>
|
||||
{/if}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click|stopPropagation={() => moveToGdrive(activeMenu)}
|
||||
>
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>GDrive'a Taşı</span>
|
||||
</button>
|
||||
<div class="menu-divider"></div>
|
||||
<button
|
||||
class="menu-item delete"
|
||||
on:click|stopPropagation={() => deleteFile(activeMenu)}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const tabs = [
|
||||
{ id: "general", label: "General", icon: "fa-solid fa-sliders" },
|
||||
{ id: "youtube", label: "YouTube", icon: "fa-brands fa-youtube" },
|
||||
{ id: "rclone", label: "Rclone", icon: "fa-solid fa-cloud" },
|
||||
{ id: "advanced", label: "Advanced", icon: "fa-solid fa-gear" }
|
||||
];
|
||||
|
||||
@@ -21,6 +22,19 @@
|
||||
let error = null;
|
||||
let success = null;
|
||||
|
||||
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 = "";
|
||||
|
||||
async function loadCookies() {
|
||||
loadingCookies = true;
|
||||
error = null;
|
||||
@@ -111,9 +125,136 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRcloneStatus() {
|
||||
rcloneLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/status");
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
rcloneStatus = data;
|
||||
rcloneAutoMove = Boolean(data?.autoMove);
|
||||
rcloneAutoMount = Boolean(data?.autoMount);
|
||||
rcloneRemoteName = data?.remoteName || "";
|
||||
rcloneRemotePath = data?.remotePath || "";
|
||||
rcloneMountDir = data?.mountDir || "";
|
||||
} catch (err) {
|
||||
error = err?.message || "Rclone durumu alınamadı.";
|
||||
} finally {
|
||||
rcloneLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRcloneSettings() {
|
||||
if (rcloneSaving) return;
|
||||
rcloneSaving = true;
|
||||
error = null;
|
||||
success = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
autoMove: rcloneAutoMove,
|
||||
autoMount: rcloneAutoMount,
|
||||
remoteName: rcloneRemoteName,
|
||||
remotePath: rcloneRemotePath,
|
||||
mountDir: rcloneMountDir
|
||||
})
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
success = "Rclone ayarları kaydedildi.";
|
||||
await loadRcloneStatus();
|
||||
} catch (err) {
|
||||
error = err?.message || "Rclone ayarları kaydedilemedi.";
|
||||
} finally {
|
||||
rcloneSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/mount", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
success = "Rclone mount başlatıldı.";
|
||||
await loadRcloneStatus();
|
||||
} catch (err) {
|
||||
error = err?.message || "Rclone mount başlatılamadı.";
|
||||
}
|
||||
}
|
||||
|
||||
async function stopRcloneMount() {
|
||||
error = null;
|
||||
success = null;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/unmount", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
success = "Rclone mount durduruldu.";
|
||||
await loadRcloneStatus();
|
||||
} catch (err) {
|
||||
error = err?.message || "Rclone mount durdurulamadı.";
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadCookies();
|
||||
loadYoutubeSettings();
|
||||
loadRcloneStatus();
|
||||
});
|
||||
|
||||
function formatDate(ts) {
|
||||
@@ -236,6 +377,135 @@
|
||||
</div>
|
||||
{:else if activeTab === "general"}
|
||||
<div class="card muted">Genel ayarlar burada yer alacak.</div>
|
||||
{:else if activeTab === "rclone"}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-cloud"></i>
|
||||
<span>Google Drive (Rclone)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field inline compact left-align">
|
||||
<label class="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={rcloneAutoMove}
|
||||
disabled={rcloneLoading || rcloneSaving}
|
||||
/>
|
||||
<span>İndirince otomatik taşı</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={rcloneAutoMount}
|
||||
disabled={rcloneLoading || rcloneSaving}
|
||||
/>
|
||||
<span>Başlangıçta otomatik mount</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field inline compact left-align">
|
||||
<div class="inline-field">
|
||||
<label for="rclone-remote">Remote adı</label>
|
||||
<input
|
||||
id="rclone-remote"
|
||||
type="text"
|
||||
bind:value={rcloneRemoteName}
|
||||
disabled={rcloneLoading || rcloneSaving}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="rclone-mount">Mount dizini</label>
|
||||
<input
|
||||
id="rclone-mount"
|
||||
type="text"
|
||||
bind:value={rcloneMountDir}
|
||||
disabled={rcloneLoading || rcloneSaving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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}>
|
||||
<i class="fa-solid fa-floppy-disk"></i> Kaydet
|
||||
</button>
|
||||
</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">
|
||||
<button class="btn" on:click={applyRcloneToken}>
|
||||
<i class="fa-solid fa-key"></i> Token Kaydet
|
||||
</button>
|
||||
<button class="btn" on:click={startRcloneMount}>
|
||||
<i class="fa-solid fa-play"></i> Mount Başlat
|
||||
</button>
|
||||
<button class="btn" on:click={stopRcloneMount}>
|
||||
<i class="fa-solid fa-stop"></i> Mount Durdur
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if rcloneStatus}
|
||||
<div class="card muted" style="margin-top:10px;">
|
||||
<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.lastError}
|
||||
<div>Son hata: {rcloneStatus.lastError}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="alert error" style="margin-top:10px;">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if success}
|
||||
<div class="alert success" style="margin-top:10px;">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
{success}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === "advanced"}
|
||||
<div class="card muted">Gelişmiş ayarlar burada yer alacak.</div>
|
||||
{/if}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
let totalDownloaded = 0;
|
||||
let totalDownloadSpeed = 0;
|
||||
let pollTimer;
|
||||
let moveToGdriveDefault = false;
|
||||
let loadingRcloneStatus = false;
|
||||
|
||||
// Modal / player state
|
||||
let showModal = false;
|
||||
@@ -79,11 +81,28 @@
|
||||
updateTransferStats();
|
||||
}
|
||||
|
||||
async function loadRcloneStatus() {
|
||||
loadingRcloneStatus = true;
|
||||
try {
|
||||
const resp = await apiFetch("/api/rclone/status");
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (typeof data?.autoMove === "boolean") {
|
||||
moveToGdriveDefault = data.autoMove;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Rclone durumu alınamadı:", err);
|
||||
} finally {
|
||||
loadingRcloneStatus = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(e) {
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
const fd = new FormData();
|
||||
fd.append("torrent", f);
|
||||
fd.append("moveToGdrive", moveToGdriveDefault ? "1" : "0");
|
||||
await apiFetch("/api/transfer", { method: "POST", body: fd }); // ✅
|
||||
await list();
|
||||
}
|
||||
@@ -132,7 +151,7 @@
|
||||
await apiFetch("/api/transfer", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ magnet: input })
|
||||
body: JSON.stringify({ magnet: input, moveToGdrive: moveToGdriveDefault })
|
||||
});
|
||||
await list();
|
||||
return;
|
||||
@@ -142,7 +161,7 @@
|
||||
const resp = await apiFetch("/api/youtube/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: normalizedYoutube })
|
||||
body: JSON.stringify({ url: normalizedYoutube, moveToGdrive: moveToGdriveDefault })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => null);
|
||||
@@ -157,7 +176,7 @@
|
||||
const resp = await apiFetch("/api/mailru/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: normalizedMailRu })
|
||||
body: JSON.stringify({ url: normalizedMailRu, moveToGdrive: moveToGdriveDefault })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => null);
|
||||
@@ -369,6 +388,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTransferGdrive(infoHash, enabled) {
|
||||
try {
|
||||
await apiFetch(`/api/transfer/${infoHash}/gdrive`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled: enabled ? "1" : "0" })
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("GDrive toggle hatası:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function streamURL(hash, index = 0) {
|
||||
const base = `${API}/stream/${hash}?index=${index}`;
|
||||
return withToken(base);
|
||||
@@ -581,6 +612,7 @@
|
||||
for (const file of torrentsToUpload) {
|
||||
const fd = new FormData();
|
||||
fd.append("torrent", file);
|
||||
fd.append("moveToGdrive", moveToGdriveDefault ? "1" : "0");
|
||||
await apiFetch("/api/transfer", { method: "POST", body: fd });
|
||||
}
|
||||
|
||||
@@ -603,6 +635,7 @@
|
||||
onMount(() => {
|
||||
list(); // 🔒 token'lı liste çekimi
|
||||
wsConnect(); // 🔒 token'lı WebSocket
|
||||
loadRcloneStatus();
|
||||
addGlobalDragListeners();
|
||||
const slider = document.querySelector(".volume-slider");
|
||||
if (slider) {
|
||||
@@ -642,6 +675,14 @@
|
||||
<label class="btn-primary" on:click={handleUrlInput}>
|
||||
<i class="fa-solid fa-magnet btn-icon"></i> ADD URL
|
||||
</label>
|
||||
<label class="gdrive-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={moveToGdriveDefault}
|
||||
disabled={loadingRcloneStatus}
|
||||
/>
|
||||
<span>GDrive'a taşı</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px;" title="Total Transfer Speed">
|
||||
<div class="transfer-info-box">
|
||||
@@ -734,6 +775,15 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div style="display:flex; gap:5px;">
|
||||
<label class="gdrive-toggle compact" on:click|stopPropagation>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={t.moveToGdrive}
|
||||
on:change={(e) =>
|
||||
toggleTransferGdrive(t.infoHash, e.target.checked)}
|
||||
/>
|
||||
<span>GDrive</span>
|
||||
</label>
|
||||
{#if t.type === "torrent" || !t.type}
|
||||
<button
|
||||
class="toggle-btn"
|
||||
@@ -815,6 +865,12 @@
|
||||
<div class="progress-text">
|
||||
{#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.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"}
|
||||
GDrive Hata • 100.0% • {(t.downloaded / 1e6).toFixed(1)} MB
|
||||
{:else if (t.progress || 0) < 1}
|
||||
{(t.progress * 100).toFixed(1)}% •
|
||||
{t.downloaded ? (t.downloaded / 1e6).toFixed(1) : 0} MB •
|
||||
@@ -1502,4 +1558,26 @@
|
||||
.more-item:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.gdrive-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #444;
|
||||
background: #f7f7f7;
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.gdrive-toggle.compact {
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.gdrive-toggle input {
|
||||
accent-color: #4caf50;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,14 @@ services:
|
||||
volumes:
|
||||
- ./downloads:/app/server/downloads
|
||||
- ./cache:/app/server/cache
|
||||
- ./rclone:/config/rclone
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
# Login credentials for basic auth
|
||||
environment:
|
||||
USERNAME: ${USERNAME}
|
||||
@@ -25,3 +32,12 @@ services:
|
||||
WEBDAV_PATH: ${WEBDAV_PATH}
|
||||
WEBDAV_READONLY: ${WEBDAV_READONLY}
|
||||
WEBDAV_INDEX_TTL: ${WEBDAV_INDEX_TTL}
|
||||
RCLONE_ENABLED: ${RCLONE_ENABLED}
|
||||
RCLONE_CONFIG_PATH: ${RCLONE_CONFIG_PATH}
|
||||
RCLONE_MOUNT_DIR: ${RCLONE_MOUNT_DIR}
|
||||
RCLONE_REMOTE_NAME: ${RCLONE_REMOTE_NAME}
|
||||
RCLONE_REMOTE_PATH: ${RCLONE_REMOTE_PATH}
|
||||
RCLONE_POLL_INTERVAL: ${RCLONE_POLL_INTERVAL}
|
||||
RCLONE_DIR_CACHE_TIME: ${RCLONE_DIR_CACHE_TIME}
|
||||
RCLONE_VFS_CACHE_MODE: ${RCLONE_VFS_CACHE_MODE}
|
||||
RCLONE_VFS_CACHE_DIR: ${RCLONE_VFS_CACHE_DIR}
|
||||
|
||||
1036
server/server.js
1036
server/server.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user