diff --git a/DysonNetwork.Drive/Billing/UsageController.cs b/DysonNetwork.Drive/Billing/UsageController.cs index 4f043e7..e270118 100644 --- a/DysonNetwork.Drive/Billing/UsageController.cs +++ b/DysonNetwork.Drive/Billing/UsageController.cs @@ -1,6 +1,25 @@ +using Microsoft.AspNetCore.Mvc; + namespace DysonNetwork.Drive.Billing; -public class UsageController +[ApiController] +[Route("api/billing/usage")] +public class UsageController(UsageService usageService) : ControllerBase { - -} \ No newline at end of file + [HttpGet] + public async Task> GetTotalUsage() + { + return await usageService.GetTotalUsage(); + } + + [HttpGet("{poolId:guid}")] + public async Task> GetPoolUsage(Guid poolId) + { + var usageDetails = await usageService.GetPoolUsage(poolId); + if (usageDetails == null) + { + return NotFound(); + } + return usageDetails; + } +} diff --git a/DysonNetwork.Drive/Billing/UsageService.cs b/DysonNetwork.Drive/Billing/UsageService.cs index 87ac4b1..9e2a5a3 100644 --- a/DysonNetwork.Drive/Billing/UsageService.cs +++ b/DysonNetwork.Drive/Billing/UsageService.cs @@ -1,6 +1,99 @@ +using Microsoft.EntityFrameworkCore; + namespace DysonNetwork.Drive.Billing; -public class UsageService +public class UsageDetails { - + public required Guid PoolId { get; set; } + public required string PoolName { get; set; } + public required long UsageBytes { get; set; } + public required double Cost { get; set; } + public required long FileCount { get; set; } +} + +public class UsageDetailsWithPercentage : UsageDetails +{ + public required double Percentage { get; set; } +} + +public class TotalUsageDetails +{ + public required List PoolUsages { get; set; } + public required long TotalUsageBytes { get; set; } + public required double TotalCost { get; set; } + public required long TotalFileCount { get; set; } +} + +public class UsageService(AppDatabase db) +{ + public async Task GetTotalUsage() + { + var poolUsages = await db.Pools + .Select(p => new UsageDetails + { + PoolId = p.Id, + PoolName = p.Name, + UsageBytes = db.Files + .Where(f => f.PoolId == p.Id) + .Sum(f => f.Size), + Cost = db.Files + .Where(f => f.PoolId == p.Id) + .Sum(f => f.Size) / 1024.0 / 1024.0 * + (p.BillingConfig.CostMultiplier ?? 1.0), + FileCount = db.Files + .Count(f => f.PoolId == p.Id) + }) + .ToListAsync(); + + var totalUsage = poolUsages.Sum(p => p.UsageBytes); + var totalCost = poolUsages.Sum(p => p.Cost); + var totalFileCount = poolUsages.Sum(p => p.FileCount); + + var poolUsagesWithPercentage = poolUsages.Select(p => new UsageDetailsWithPercentage + { + PoolId = p.PoolId, + PoolName = p.PoolName, + UsageBytes = p.UsageBytes, + Cost = p.Cost, + FileCount = p.FileCount, + Percentage = totalUsage > 0 ? (double)p.UsageBytes / totalUsage : 0 + }).ToList(); + + return new TotalUsageDetails + { + PoolUsages = poolUsagesWithPercentage, + TotalUsageBytes = totalUsage, + TotalCost = totalCost, + TotalFileCount = totalFileCount + }; + } + + public async Task GetPoolUsage(Guid poolId) + { + var pool = await db.Pools.FindAsync(poolId); + if (pool == null) + { + return null; + } + + var usageBytes = await db.Files + .Where(f => f.PoolId == poolId) + .SumAsync(f => f.Size); + + var fileCount = await db.Files + .Where(f => f.PoolId == poolId) + .CountAsync(); + + var cost = usageBytes / 1024.0 / 1024.0 * + (pool.BillingConfig.CostMultiplier ?? 1.0); + + return new UsageDetails + { + PoolId = pool.Id, + PoolName = pool.Name, + UsageBytes = usageBytes, + Cost = cost, + FileCount = fileCount + }; + } } \ No newline at end of file diff --git a/DysonNetwork.Drive/Client/bun.lock b/DysonNetwork.Drive/Client/bun.lock index ff5412a..528c761 100644 --- a/DysonNetwork.Drive/Client/bun.lock +++ b/DysonNetwork.Drive/Client/bun.lock @@ -11,10 +11,12 @@ "@vueuse/core": "^13.5.0", "aspnet-prerendering": "^3.0.1", "cfturnstile-vue3": "^2.0.0", + "chart.js": "^4.5.0", "pinia": "^3.0.3", "tailwindcss": "^4.1.11", "tus-js-client": "^4.3.1", "vue": "^3.5.17", + "vue-chartjs": "^5.3.2", "vue-router": "^4.5.1", }, "devDependencies": { @@ -162,6 +164,8 @@ "@juggle/resize-observer": ["@juggle/resize-observer@3.4.0", "", {}, "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -400,6 +404,8 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chart.js": ["chart.js@4.5.0", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -854,6 +860,8 @@ "vue": ["vue@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/compiler-sfc": "3.5.17", "@vue/runtime-dom": "3.5.17", "@vue/server-renderer": "3.5.17", "@vue/shared": "3.5.17" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g=="], + "vue-chartjs": ["vue-chartjs@5.3.2", "", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw=="], + "vue-eslint-parser": ["vue-eslint-parser@10.2.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw=="], "vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="], diff --git a/DysonNetwork.Drive/Client/package.json b/DysonNetwork.Drive/Client/package.json index a67af57..dbb65f2 100644 --- a/DysonNetwork.Drive/Client/package.json +++ b/DysonNetwork.Drive/Client/package.json @@ -22,10 +22,12 @@ "@vueuse/core": "^13.5.0", "aspnet-prerendering": "^3.0.1", "cfturnstile-vue3": "^2.0.0", + "chart.js": "^4.5.0", "pinia": "^3.0.3", "tailwindcss": "^4.1.11", "tus-js-client": "^4.3.1", "vue": "^3.5.17", + "vue-chartjs": "^5.3.2", "vue-router": "^4.5.1" }, "devDependencies": { diff --git a/DysonNetwork.Drive/Client/src/views/dashboard/usage.vue b/DysonNetwork.Drive/Client/src/views/dashboard/usage.vue index 34f5c8f..d4b136e 100644 --- a/DysonNetwork.Drive/Client/src/views/dashboard/usage.vue +++ b/DysonNetwork.Drive/Client/src/views/dashboard/usage.vue @@ -1,9 +1,103 @@ + + diff --git a/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs index 0dc0459..4f64952 100644 --- a/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs @@ -139,6 +139,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/DysonNetwork.Drive/Storage/FilePool.cs b/DysonNetwork.Drive/Storage/FilePool.cs index 186553c..1f77fe4 100644 --- a/DysonNetwork.Drive/Storage/FilePool.cs +++ b/DysonNetwork.Drive/Storage/FilePool.cs @@ -21,7 +21,7 @@ public class RemoteStorageConfig public class BillingConfig { - public double CostMultiplier { get; set; } = 1.0; + public double? CostMultiplier { get; set; } = 1.0; } public class PolicyConfig