Usage drive

This commit is contained in:
2025-07-26 23:21:57 +08:00
parent f40d1dc1b2
commit 02af78ca99
7 changed files with 226 additions and 9 deletions

View File

@@ -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
{
}
[HttpGet]
public async Task<ActionResult<TotalUsageDetails>> GetTotalUsage()
{
return await usageService.GetTotalUsage();
}
[HttpGet("{poolId:guid}")]
public async Task<ActionResult<UsageDetails>> GetPoolUsage(Guid poolId)
{
var usageDetails = await usageService.GetPoolUsage(poolId);
if (usageDetails == null)
{
return NotFound();
}
return usageDetails;
}
}

View File

@@ -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<UsageDetailsWithPercentage> 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<TotalUsageDetails> 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<UsageDetails?> 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
};
}
}

View File

@@ -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=="],

View File

@@ -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": {

View File

@@ -1,9 +1,103 @@
<template>
<section class="h-full"></section>
<section class="h-full container-fluid mx-auto py-4 px-5">
<div class="h-full flex justify-center items-center" v-if="!usage">
<n-spin />
</div>
<n-grid cols="1 s:2 m:3 l:4" responsive="screen" :x-gap="16" :y-gap="16" v-else>
<n-gi>
<n-card class="h-stats">
<n-statistic label="All Uploads" tabular-nums>
<n-number-animation
:from="0"
:to="toGigabytes(usage.total_usage_bytes)"
:precision="3"
/>
<template #suffix>GiB</template>
</n-statistic>
</n-card>
</n-gi>
<n-gi>
<n-card class="h-stats">
<n-statistic label="All Files" tabular-nums>
<n-number-animation :from="0" :to="usage.total_file_count" />
</n-statistic>
</n-card>
</n-gi>
<n-gi>
<n-card class="h-stats">
<n-statistic label="Cost" tabular-nums>
<n-number-animation :from="0" :to="usage.total_cost" :precision="2" />
<template #suffix>NSD</template>
</n-statistic>
</n-card>
</n-gi>
<n-gi>
<n-card class="h-stats">
<n-statistic label="Pools" tabular-nums>
<n-number-animation :from="0" :to="usage.pool_usages.length" />
</n-statistic>
</n-card>
</n-gi>
<n-gi span="2">
<n-card class="ratio-video" title="Pool Usage">
<pie
:data="chartData"
:options="{
maintainAspectRatio: false,
responsive: true,
plugins: { legend: { position: isDesktop ? 'right' : 'bottom' } },
}"
/>
</n-card>
</n-gi>
</n-grid>
</section>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user';
import { NSpin, NCard, NStatistic, NGrid, NGi, NNumberAnimation } from 'naive-ui'
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement } from 'chart.js'
import { Pie } from 'vue-chartjs'
import { computed, onMounted, ref } from 'vue'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
const userStore = useUserStore()
ChartJS.register(Title, Tooltip, Legend, ArcElement)
const breakpoints = useBreakpoints(breakpointsTailwind)
const isDesktop = breakpoints.greaterOrEqual('md')
const chartData = computed(() => ({
labels: usage.value.pool_usages.map((pool: any) => pool.pool_name),
datasets: [
{
label: 'Pool Usage',
backgroundColor: '#7D80BAFF',
data: usage.value.pool_usages.map((pool: any) => pool.usage_bytes),
},
]
}))
const usage = ref<any>()
async function fetchUsage() {
try {
const response = await fetch('/api/billing/usage')
if (!response.ok) {
throw new Error('Network response was not ok')
}
usage.value = await response.json()
} catch (error) {
console.error('Failed to fetch usage data:', error)
}
}
onMounted(() => fetchUsage())
function toGigabytes(bytes: number): number {
return bytes / (1024 * 1024 * 1024)
}
</script>
<style scoped>
.h-stats {
height: 105px;
}
</style>

View File

@@ -139,6 +139,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<Storage.FileService>();
services.AddScoped<Storage.FileReferenceService>();
services.AddScoped<Billing.UsageService>();
services.AddScoped<Billing.UsageService>();
return services;
}

View File

@@ -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