Use forked frontend builds for test admin UI

This commit is contained in:
Orion
2026-06-05 11:14:13 +08:00
parent 1460b51163
commit 9d978ca68e
32 changed files with 307 additions and 1303 deletions
+30 -1
View File
@@ -1,4 +1,5 @@
import hashlib
import json
import os
import time
from datetime import datetime, timedelta
@@ -8,7 +9,7 @@ from core.response import APIResponse
from core.storage import FileStorageInterface, storages
from core.settings import settings
from core.config import refresh_settings
from apps.base.models import FileCodes, KeyValue, file_codes_pydantic
from apps.base.models import FileCodes, KeyValue, TransferStats, file_codes_pydantic
from apps.base.utils import get_expire_info, get_file_path_name
from fastapi import HTTPException
from core.settings import data_root
@@ -82,6 +83,34 @@ class FileService:
return f"{self.FILE_METADATA_KEY_PREFIX}{file_id}"
async def _delete_file_code(self, file_code: FileCodes):
try:
name = (
f"{file_code.prefix or ''}{file_code.suffix or ''}"
or file_code.uuid_file_name
or file_code.code
)
await TransferStats.create(
action="delete",
file_code_id=file_code.id,
code=file_code.code,
name=name,
size=int(file_code.size or 0),
ip="admin-delete",
file_path=file_code.file_path or "",
expired_at=file_code.expired_at,
deleted_at=datetime.now(),
meta=json.dumps(
{
"prefix": file_code.prefix or "",
"suffix": file_code.suffix or "",
"uuid_file_name": file_code.uuid_file_name or "",
"created_at": str(file_code.created_at),
},
ensure_ascii=False,
),
)
except Exception:
pass
if file_code.text is None:
await self.file_storage.delete_file(file_code)
await KeyValue.filter(key=self._file_metadata_key(file_code.id)).delete()
+176 -1
View File
@@ -30,7 +30,7 @@ from apps.admin.schemas import (
UpdateFileData,
)
from core.response import APIResponse
from apps.base.models import FileCodes, KeyValue
from apps.base.models import FileCodes, KeyValue, TransferStats
from apps.admin.dependencies import create_token
from core.settings import settings
from core.utils import get_now, verify_password
@@ -174,6 +174,181 @@ async def admin_activities(
return APIResponse(detail=result)
@admin_api.get("/analytics")
async def analytics(start: str = "", end: str = ""):
now = await get_now()
def parse_day(value: str, fallback: datetime.datetime) -> datetime.datetime:
if not value:
return fallback
try:
return datetime.datetime.strptime(value[:10], "%Y-%m-%d")
except Exception:
return fallback
end_day = parse_day(end, now.replace(hour=0, minute=0, second=0, microsecond=0))
start_day = parse_day(start, end_day - datetime.timedelta(days=29))
end_exclusive = end_day + datetime.timedelta(days=1)
stats = await TransferStats.filter(
created_at__gte=start_day,
created_at__lt=end_exclusive,
)
files = await FileCodes.all()
file_map = {item.code: item for item in files}
day_map = {}
cursor = start_day
while cursor < end_exclusive:
key = cursor.strftime("%Y-%m-%d")
day_map[key] = {
"date": key,
"downloads": 0,
"uploads": 0,
"downloadTraffic": 0,
"uploadTraffic": 0,
}
cursor += datetime.timedelta(days=1)
file_stats = {}
for item in stats:
key = item.created_at.strftime("%Y-%m-%d")
row = day_map.get(key)
if row and item.action == "download":
row["downloads"] += 1
row["downloadTraffic"] += item.size or 0
elif row and item.action == "upload":
row["uploads"] += 1
row["uploadTraffic"] += item.size or 0
if not item.code:
continue
bucket = file_stats.setdefault(
item.code,
{
"downloads": 0,
"downloadTraffic": 0,
"uploads": 0,
"uploadTraffic": 0,
"name": item.name or item.code,
"size": int(item.size or 0),
"uploaded_at": None,
"expired_at": item.expired_at,
"deleted_at": None,
"deleted": False,
},
)
bucket["name"] = bucket["name"] or item.name or item.code
bucket["size"] = bucket["size"] or int(item.size or 0)
bucket["expired_at"] = bucket["expired_at"] or item.expired_at
if item.action == "upload":
bucket["uploads"] += 1
bucket["uploadTraffic"] += item.size or 0
bucket["uploaded_at"] = bucket["uploaded_at"] or item.created_at
elif item.action == "download":
bucket["downloads"] += 1
bucket["downloadTraffic"] += item.size or 0
elif item.action == "delete":
bucket["deleted"] = True
bucket["deleted_at"] = item.deleted_at or item.created_at
for file_item in files:
current_name = (
f"{file_item.prefix or ''}{file_item.suffix or ''}"
or file_item.uuid_file_name
or file_item.code
)
bucket = file_stats.setdefault(
file_item.code,
{
"downloads": 0,
"downloadTraffic": 0,
"uploads": 0,
"uploadTraffic": 0,
"name": current_name,
"size": int(file_item.size or 0),
"uploaded_at": file_item.created_at,
"expired_at": file_item.expired_at,
"deleted_at": None,
"deleted": False,
},
)
bucket["name"] = bucket["name"] or current_name
bucket["size"] = bucket["size"] or int(file_item.size or 0)
bucket["uploaded_at"] = bucket["uploaded_at"] or file_item.created_at
bucket["expired_at"] = bucket["expired_at"] or file_item.expired_at
def iso(value):
return value.isoformat() if value else ""
def history_row(code: str, data: dict) -> dict:
file_item = file_map.get(code)
uploaded_at = data.get("uploaded_at") or (
file_item.created_at if file_item else None
)
expired_at = data.get("expired_at") or (
file_item.expired_at if file_item else None
)
return {
"code": code,
"name": data.get("name") or code,
"size": str(data.get("size") or 0),
"download_count": data.get("downloads") or 0,
"download_traffic": str(data.get("downloadTraffic") or 0),
"upload_count": data.get("uploads") or 0,
"upload_traffic": str(data.get("uploadTraffic") or 0),
"current": bool(file_item),
"deleted": bool(data.get("deleted")) and not bool(file_item),
"created_at": iso(uploaded_at),
"uploaded_at": iso(uploaded_at),
"expired_at": iso(expired_at),
"deleted_at": iso(data.get("deleted_at")),
}
def sort_time(value) -> float:
return value.timestamp() if value else 0
history = sorted(
file_stats.items(),
key=lambda kv: (
sort_time(kv[1].get("deleted_at") or kv[1].get("uploaded_at")),
kv[1].get("downloads") or 0,
kv[1].get("downloadTraffic") or 0,
),
reverse=True,
)
top_files = sorted(
[item for item in history if (item[1].get("downloads") or 0) > 0],
key=lambda kv: (kv[1].get("downloads") or 0, kv[1].get("downloadTraffic") or 0),
reverse=True,
)[:5]
return APIResponse(
detail={
"range": {
"start": start_day.strftime("%Y-%m-%d"),
"end": end_day.strftime("%Y-%m-%d"),
},
"totals": {
"totalDownloads": sum(1 for item in stats if item.action == "download"),
"downloadedFiles": len(
{item.code for item in stats if item.action == "download" and item.code}
),
"downloadTraffic": str(
sum(item.size or 0 for item in stats if item.action == "download")
),
"historicalFiles": len(file_stats),
"currentFiles": len(files),
"totalUploads": sum(1 for item in stats if item.action == "upload"),
"uploadTraffic": str(
sum(item.size or 0 for item in stats if item.action == "upload")
),
},
"daily": list(day_map.values()),
"topFiles": [history_row(code, data) for code, data in top_files],
"historyFiles": [history_row(code, data) for code, data in history],
}
)
@admin_api.delete("/file/delete")
async def file_delete(
data: IDData,