Use forked frontend builds for test admin UI
This commit is contained in:
+30
-1
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user