mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 15:47:00 +08:00
feat: implement caching for adapter usage and display summary in AdaptersPage
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -14,6 +15,28 @@ from models import StorageAdapter
|
|||||||
|
|
||||||
|
|
||||||
class AdapterService:
|
class AdapterService:
|
||||||
|
_usage_cache_ttl = 3600
|
||||||
|
_usage_cache: dict[int, tuple[float, AdapterUsage]] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_cached_usage(cls, adapter_id: int) -> AdapterUsage | None:
|
||||||
|
cached = cls._usage_cache.get(adapter_id)
|
||||||
|
if not cached:
|
||||||
|
return None
|
||||||
|
expires_at, usage = cached
|
||||||
|
if expires_at <= time.time():
|
||||||
|
cls._usage_cache.pop(adapter_id, None)
|
||||||
|
return None
|
||||||
|
return usage
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _set_cached_usage(cls, usage: AdapterUsage):
|
||||||
|
cls._usage_cache[usage.id] = (time.time() + cls._usage_cache_ttl, usage)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _clear_cached_usage(cls, adapter_id: int):
|
||||||
|
cls._usage_cache.pop(adapter_id, None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _validate_and_normalize_config(cls, adapter_type: str, cfg):
|
def _validate_and_normalize_config(cls, adapter_type: str, cfg):
|
||||||
schemas = get_config_schemas()
|
schemas = get_config_schemas()
|
||||||
@@ -106,6 +129,10 @@ class AdapterService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _get_adapter_usage_for_record(cls, rec: StorageAdapter) -> AdapterUsage:
|
async def _get_adapter_usage_for_record(cls, rec: StorageAdapter) -> AdapterUsage:
|
||||||
|
cached = cls._get_cached_usage(rec.id)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
|
||||||
if not rec.enabled:
|
if not rec.enabled:
|
||||||
return cls._unsupported_usage(rec, "adapter_disabled")
|
return cls._unsupported_usage(rec, "adapter_disabled")
|
||||||
|
|
||||||
@@ -127,7 +154,7 @@ class AdapterService:
|
|||||||
if not isinstance(raw_usage, dict):
|
if not isinstance(raw_usage, dict):
|
||||||
return cls._unsupported_usage(rec, "invalid_usage_response")
|
return cls._unsupported_usage(rec, "invalid_usage_response")
|
||||||
|
|
||||||
return AdapterUsage(
|
usage = AdapterUsage(
|
||||||
id=rec.id,
|
id=rec.id,
|
||||||
name=rec.name,
|
name=rec.name,
|
||||||
type=rec.type,
|
type=rec.type,
|
||||||
@@ -139,6 +166,8 @@ class AdapterService:
|
|||||||
source=raw_usage.get("source") or rec.type,
|
source=raw_usage.get("source") or rec.type,
|
||||||
scope=raw_usage.get("scope"),
|
scope=raw_usage.get("scope"),
|
||||||
)
|
)
|
||||||
|
cls._set_cached_usage(usage)
|
||||||
|
return usage
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def list_adapter_usages(cls):
|
async def list_adapter_usages(cls):
|
||||||
@@ -168,6 +197,7 @@ class AdapterService:
|
|||||||
await rec.save()
|
await rec.save()
|
||||||
|
|
||||||
await runtime_registry.upsert(rec)
|
await runtime_registry.upsert(rec)
|
||||||
|
cls._clear_cached_usage(adapter_id)
|
||||||
return AdapterOut.model_validate(rec)
|
return AdapterOut.model_validate(rec)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -176,4 +206,5 @@ class AdapterService:
|
|||||||
if not deleted:
|
if not deleted:
|
||||||
raise HTTPException(404, detail="Not found")
|
raise HTTPException(404, detail="Not found")
|
||||||
runtime_registry.remove(adapter_id)
|
runtime_registry.remove(adapter_id)
|
||||||
|
cls._clear_cached_usage(adapter_id)
|
||||||
return {"deleted": True}
|
return {"deleted": True}
|
||||||
|
|||||||
@@ -156,6 +156,35 @@ const AdaptersPage = memo(function AdaptersPage() {
|
|||||||
return label === key ? type : label;
|
return label === key ? type : label;
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
const usageSummary = Object.values(usageMap).reduce(
|
||||||
|
(acc, usage) => {
|
||||||
|
if (!usage.supported) return acc;
|
||||||
|
if (usage.used_bytes !== null && usage.used_bytes !== undefined) {
|
||||||
|
acc.used += usage.used_bytes;
|
||||||
|
acc.hasUsed = true;
|
||||||
|
}
|
||||||
|
if (usage.total_bytes !== null && usage.total_bytes !== undefined) {
|
||||||
|
acc.total += usage.total_bytes;
|
||||||
|
acc.hasTotal = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ used: 0, total: 0, hasUsed: false, hasTotal: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageTitle = (
|
||||||
|
<Space size={12} wrap>
|
||||||
|
<span>{t('Storage Adapters')}</span>
|
||||||
|
{(usageSummary.hasUsed || usageSummary.hasTotal) && (
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 13, fontWeight: 400 }}>
|
||||||
|
{usageSummary.hasUsed ? formatBytes(usageSummary.used) : '-'}
|
||||||
|
{' / '}
|
||||||
|
{usageSummary.hasTotal ? formatBytes(usageSummary.total) : '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: t('Name'), dataIndex: 'name' },
|
{ title: t('Name'), dataIndex: 'name' },
|
||||||
{ title: t('Type'), dataIndex: 'type', width: 140, render: (value: string) => renderTypeLabel(value) },
|
{ title: t('Type'), dataIndex: 'type', width: 140, render: (value: string) => renderTypeLabel(value) },
|
||||||
@@ -234,7 +263,7 @@ const AdaptersPage = memo(function AdaptersPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageCard
|
<PageCard
|
||||||
title={t('Storage Adapters')}
|
title={pageTitle}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={fetchList} loading={loading}>{t('Refresh')}</Button>
|
<Button onClick={fetchList} loading={loading}>{t('Refresh')}</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user