From 83b88c0eb7e625751f287ee69fdd11b894ec3c96 Mon Sep 17 00:00:00 2001 From: szbk Date: Wed, 29 Oct 2025 00:39:06 +0300 Subject: [PATCH] =?UTF-8?q?Disk=20Space=20alan=C4=B1=20eklendi.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Sidebar.svelte | 127 +++++++++++++- client/src/styles/main.css | 91 +++++++++- server/server.js | 53 ++++++ server/utils/diskSpace.js | 243 +++++++++++++++++++++++++++ 4 files changed, 506 insertions(+), 8 deletions(-) create mode 100644 server/utils/diskSpace.js diff --git a/client/src/components/Sidebar.svelte b/client/src/components/Sidebar.svelte index f41c026..9b5bd24 100644 --- a/client/src/components/Sidebar.svelte +++ b/client/src/components/Sidebar.svelte @@ -1,31 +1,119 @@ + + +
+
+ + Disk Space +
+
+
+ + {#if diskSpace.usedFormatted && diskSpace.totalFormatted} + {diskSpace.usedFormatted} / {diskSpace.totalFormatted} + {:else} + {diskSpace.usedGB} GB / {diskSpace.totalGB} GB + {/if} + +
+
+
+
+
+
+
+
diff --git a/client/src/styles/main.css b/client/src/styles/main.css index 223b2fc..4be9eb3 100644 --- a/client/src/styles/main.css +++ b/client/src/styles/main.css @@ -31,13 +31,14 @@ body, .app { display: grid; grid-template-columns: 220px 1fr; - height: 100%; + height: 100vh; } .content { display: flex; flex-direction: column; - height: 100%; + height: 100vh; + overflow-y: auto; } /* ======================================================= @@ -48,6 +49,9 @@ body, border-right: 1px solid var(--border); display: flex; flex-direction: column; + height: 100vh; + position: sticky; + top: 0; } .sidebar .logo { @@ -59,6 +63,7 @@ body, .sidebar .menu { padding-top: 6px; + flex: 1; } .sidebar .menu .item { @@ -603,3 +608,85 @@ img.thumb.loaded { gap: 6px; } } + +/* ======================================================= + 💾 DISK SPACE + ======================================================= */ +.sidebar .disk-space { + margin-top: auto; + padding: 16px; + border-top: 1px solid var(--border); +} + +.sidebar .disk-space-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; + color: #222; + font-weight: 600; + font-size: 14px; +} + +.sidebar .disk-space-header .icon { + width: 16px; + text-align: center; + color: #333; +} + +.sidebar .disk-space-title { + flex: 1; +} + +.sidebar .disk-space-info { + display: flex; + flex-direction: column; + gap: 8px; +} + +.sidebar .disk-space-text { + font-size: 12px; + color: #666; + text-align: left; + display: flex; + align-items: center; +} + +.sidebar .disk-space-values { + font-weight: 500; + color: #333; +} + +.sidebar .disk-space-bar-container { + width: 100%; + height: 8px; + background: #e5e5e5; + border-radius: 99px; + overflow: hidden; +} + +.sidebar .disk-space-bar { + width: 100%; + height: 100%; + position: relative; +} + +.sidebar .disk-space-bar-fill { + height: 100%; + background: linear-gradient(90deg, #f5b333 0%, #e2a62f 100%); + border-radius: 99px; + transition: width 0.3s ease; +} + +/* Disk space renkleri - kullanım oranına göre */ +.sidebar .disk-space-bar-fill.low { + background: linear-gradient(90deg, #4caf50 0%, #388e3c 100%); +} + +.sidebar .disk-space-bar-fill.medium { + background: linear-gradient(90deg, #ff9800 0%, #f57c00 100%); +} + +.sidebar .disk-space-bar-fill.high { + background: linear-gradient(90deg, #f44336 0%, #d32f2f 100%); +} diff --git a/server/server.js b/server/server.js index adb57d2..62b9acb 100644 --- a/server/server.js +++ b/server/server.js @@ -9,6 +9,7 @@ import { WebSocketServer } from "ws"; import { fileURLToPath } from "url"; import { exec } from "child_process"; import crypto from "crypto"; // 🔒 basit token üretimi için +import { getSystemDiskInfo } from "./utils/diskSpace.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -2082,6 +2083,20 @@ function broadcastFileUpdate(rootFolder) { wss.clients.forEach((c) => c.readyState === 1 && c.send(data)); } +function broadcastDiskSpace() { + if (!wss) return; + getSystemDiskInfo(DOWNLOAD_DIR).then(diskInfo => { + console.log("🔄 Broadcasting disk space:", diskInfo); + const data = JSON.stringify({ + type: "diskSpace", + data: diskInfo + }); + wss.clients.forEach((c) => c.readyState === 1 && c.send(data)); + }).catch(err => { + console.error("❌ Disk space broadcast error:", err.message); + }); +} + function broadcastSnapshot() { if (!wss) return; const data = JSON.stringify({ type: "progress", torrents: snapshot() }); @@ -2342,6 +2357,9 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => { upsertInfoFile(entry.savePath, infoUpdate); broadcastFileUpdate(rootFolder); + + // Torrent tamamlandığında disk space bilgisini güncelle + broadcastDiskSpace(); broadcastSnapshot(); }); @@ -2755,6 +2773,9 @@ app.delete("/api/file", requireAuth, (req, res) => { torrents.delete(matchedInfoHash); console.log(`🧹 Torrent kaydı da temizlendi: ${matchedInfoHash}`); broadcastSnapshot(); + + // Torrent silindiğinde disk space bilgisini güncelle + broadcastDiskSpace(); }); } else { broadcastSnapshot(); @@ -3501,7 +3522,18 @@ const server = app.listen(PORT, () => wss = new WebSocketServer({ server }); wss.on("connection", (ws) => { + console.log("🔌 New WebSocket connection established"); ws.send(JSON.stringify({ type: "progress", torrents: snapshot() })); + // Bağlantı kurulduğunda disk space bilgisi gönder + broadcastDiskSpace(); + + ws.on("close", () => { + console.log("🔌 WebSocket connection closed"); + }); + + ws.on("error", (error) => { + console.error("🔌 WebSocket error:", error); + }); }); // --- ⏱️ Her 2 saniyede bir aktif torrent durumu yayınla --- @@ -3511,6 +3543,27 @@ setInterval(() => { } }, 2000); +// --- ⏱️ Her 30 saniyede bir disk space bilgisi yayınla --- +setInterval(() => { + broadcastDiskSpace(); +}, 30000); + +// --- Disk space bilgisi --- +app.get("/api/disk-space", requireAuth, async (req, res) => { + try { + // Downloads klasörü yoksa oluştur + if (!fs.existsSync(DOWNLOAD_DIR)) { + fs.mkdirSync(DOWNLOAD_DIR, { recursive: true }); + } + + const diskInfo = await getSystemDiskInfo(DOWNLOAD_DIR); + res.json(diskInfo); + } catch (err) { + console.error("❌ Disk space error:", err.message); + res.status(500).json({ error: err.message }); + } +}); + client.on("error", (err) => { if (!String(err).includes("uTP")) console.error("WebTorrent error:", err.message); diff --git a/server/utils/diskSpace.js b/server/utils/diskSpace.js new file mode 100644 index 0000000..9d128a9 --- /dev/null +++ b/server/utils/diskSpace.js @@ -0,0 +1,243 @@ +import fs from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +/** + * Disk alanı bilgilerini hesaplar + * @param {string} path - Kontrol edilecek dizin yolu + * @returns {Promise} Disk alanı bilgileri + */ +export async function getDiskSpace(path = '/') { + console.log("🔍 Getting disk space for path:", path); + try { + // Linux/macOS için df komutu kullan + if (process.platform !== 'win32') { + const escapedPath = path.replace(/(["\\$`])/g, '\\$1'); + const blockFlag = process.platform === 'darwin' ? '-k' : '-k'; + const { stdout } = await execAsync(`df ${blockFlag} "${escapedPath}"`); + console.log("📊 df command output:", stdout); + const lines = stdout.trim().split('\n'); + + if (lines.length >= 2) { + // Çoğu dağıtımda veriler ikinci satırda + const data = lines[lines.length - 1].trim().split(/\s+/); + console.log("📋 Parsed df data:", data); + if (data.length < 5) { + throw new Error("df output could not be parsed"); + } + + // df -k çıktısı 1K-blocks döndürür + const totalBlocks = parseFloat(data[1]) || 0; + const usedBlocks = parseFloat(data[2]) || 0; + const availableBlocks = parseFloat(data[3]) || 0; + const usedPercent = parseFloat((data[4] || '').replace('%', '')) || 0; + + const total = totalBlocks * 1024; + const used = usedBlocks * 1024; + const available = availableBlocks * 1024; + + console.log("📏 Calculated sizes (bytes):", { total, used, available, usedPercent }); + + const totalFormatted = formatBytes(total); + const usedFormatted = formatBytes(used); + const availableFormatted = formatBytes(available); + + const result = { + total, + used, + available, + usedPercent, + totalGB: (total / (1024 * 1024 * 1024)).toFixed(2), + usedGB: (used / (1024 * 1024 * 1024)).toFixed(2), + availableGB: (available / (1024 * 1024 * 1024)).toFixed(2), + // Dinamik formatlanmış değerler + totalFormatted: totalFormatted.formatted, + usedFormatted: usedFormatted.formatted, + availableFormatted: availableFormatted.formatted, + totalValue: totalFormatted.value, + totalUnit: totalFormatted.unit, + usedValue: usedFormatted.value, + usedUnit: usedFormatted.unit + }; + + console.log("✅ Final disk space result:", result); + return result; + } + } else { + // Windows için wmic komutu kullan + const { stdout } = await execAsync('wmic logicaldisk get size,freespace,caption'); + const lines = stdout.trim().split('\n'); + + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line) { + const parts = line.split(/\s+/); + if (parts.length >= 3) { + const caption = parts[0]; + const freeSpace = parseInt(parts[1]); + const size = parseInt(parts[parts.length - 1]); + + if (size > 0) { + const used = size - freeSpace; + const usedPercent = (used / size) * 100; + + const totalFormatted = formatBytes(size); + const usedFormatted = formatBytes(used); + const availableFormatted = formatBytes(freeSpace); + + return { + total: size, + used, + available: freeSpace, + usedPercent, + totalGB: (size / (1024 * 1024 * 1024)).toFixed(2), + usedGB: (used / (1024 * 1024 * 1024)).toFixed(2), + availableGB: (freeSpace / (1024 * 1024 * 1024)).toFixed(2), + // Dinamik formatlanmış değerler + totalFormatted: totalFormatted.formatted, + usedFormatted: usedFormatted.formatted, + availableFormatted: availableFormatted.formatted, + totalValue: totalFormatted.value, + totalUnit: totalFormatted.unit, + usedValue: usedFormatted.value, + usedUnit: usedFormatted.unit, + drive: caption + }; + } + } + } + } + } + + // Fallback olarak Node.js fs.statSync kullan + const stats = fs.statSync(path); + return { + total: 0, + used: 0, + available: 0, + usedPercent: 0, + totalGB: '0', + usedGB: '0', + availableGB: '0', + error: 'Disk bilgileri alınamadı' + }; + } catch (error) { + console.error('Disk space error:', error.message); + return { + total: 0, + used: 0, + available: 0, + usedPercent: 0, + totalGB: '0', + usedGB: '0', + availableGB: '0', + error: error.message + }; + } +} + +/** + * Boyut string'ini byte'a çevirir (örn: "10G" -> 10737418240) + * @param {string} sizeStr - Boyut string'i + * @returns {number} Byte cinsinden boyut + */ +function parseSize(sizeStr) { + const units = { + 'K': 1024, + 'M': 1024 * 1024, + 'G': 1024 * 1024 * 1024, + 'T': 1024 * 1024 * 1024 * 1024 + }; + + const match = sizeStr.match(/^(\d+(?:\.\d+)?)(K|M|G|T)?$/i); + if (!match) return 0; + + const value = parseFloat(match[1]); + const unit = match[2] ? match[2].toUpperCase() : ''; + + const result = value * (units[unit] || 1); + console.log(`🔍 parseSize: ${sizeStr} -> ${result} bytes`); + return result; +} + +/** + * Byte cinsinden boyutu insan tarafından okunabilir formata çevirir + * @param {number} bytes - Byte cinsinden boyut + * @returns {Object} Formatlanmış boyut bilgisi + */ +function formatBytes(bytes) { + if (bytes === 0) return { value: 0, unit: 'GB', formatted: '0 GB' }; + + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const threshold = 1024; + const unitIndex = Math.floor(Math.log(bytes) / Math.log(threshold)); + const unit = units[unitIndex]; + const value = bytes / Math.pow(threshold, unitIndex); + + // Eğer GB veya daha büyükse, 2 ondalık basamak kullan + // Aksi takdirde tam sayı olarak göster + const decimals = unitIndex >= 3 ? 2 : 0; + const formattedValue = value.toFixed(decimals); + + return { + value: parseFloat(formattedValue), + unit, + formatted: `${formattedValue} ${unit}` + }; +} + +/** + * Downloads klasörünün boyutunu hesaplar + * @param {string} downloadsPath - Downloads klasörü yolu + * @returns {Promise} Klasör boyut bilgileri + */ +export async function getDownloadsSize(downloadsPath) { + try { + if (!downloadsPath || !fs.existsSync(downloadsPath)) { + return { + bytes: 0, + GB: '0', + MB: '0' + }; + } + const escapedDownloadsPath = downloadsPath.replace(/(["\\$`])/g, '\\$1'); + const { stdout } = await execAsync(`du -sb "${escapedDownloadsPath}"`); + const sizeInBytes = parseInt(stdout.trim().split('\t')[0]); + + return { + bytes: sizeInBytes, + GB: (sizeInBytes / (1024 * 1024 * 1024)).toFixed(2), + MB: (sizeInBytes / (1024 * 1024)).toFixed(2) + }; + } catch (error) { + console.error('Downloads size error:', error.message); + return { + bytes: 0, + GB: '0', + MB: '0', + error: error.message + }; + } +} + +/** + * Sistem genelinde disk kullanım özetini döndürür + * @param {string} downloadsPath - Downloads klasörü yolu + * @returns {Promise} Kapsamlı disk bilgileri + */ +export async function getSystemDiskInfo(downloadsPath) { + console.log("🔍 Getting system disk info for:", downloadsPath); + const diskSpace = await getDiskSpace(downloadsPath); + const downloadsSize = await getDownloadsSize(downloadsPath); + + console.log("📊 Disk space result:", diskSpace); + console.log("📁 Downloads size result:", downloadsSize); + + return { + ...diskSpace, + downloads: downloadsSize, + timestamp: new Date().toISOString() + }; +}