feat: add admin file view presets

This commit is contained in:
Lan
2026-06-03 07:01:38 +08:00
parent fc6046a663
commit b6aa28c345
3 changed files with 287 additions and 1 deletions
+12 -1
View File
@@ -1,5 +1,5 @@
import datetime
from typing import Optional, Union
from typing import Any, Optional, Union
from pydantic import BaseModel
@@ -61,3 +61,14 @@ class FileMetadataData(BaseModel):
id: int
note: Optional[str] = None
tags: Optional[list[str]] = None
class FileViewPresetData(BaseModel):
id: Optional[str] = None
name: str
filters: Optional[dict[str, Any]] = None
params: Optional[dict[str, Any]] = None
class FileViewPresetDeleteData(BaseModel):
id: str
+212
View File
@@ -1,3 +1,4 @@
import hashlib
import os
import time
from datetime import datetime, timedelta
@@ -16,9 +17,13 @@ from core.utils import get_now, hash_password, is_password_hashed
class FileService:
FILE_METADATA_KEY_PREFIX = "admin_file_metadata:"
FILE_VIEW_PRESETS_KEY = "admin_file_view_presets"
MAX_METADATA_NOTE_LENGTH = 2000
MAX_METADATA_TAGS = 12
MAX_METADATA_TAG_LENGTH = 24
MAX_VIEW_PRESETS = 24
MAX_VIEW_PRESET_NAME_LENGTH = 32
MAX_VIEW_PRESET_KEYWORD_LENGTH = 80
POLICY_ACTIONS = {
"extend_24h",
@@ -38,6 +43,28 @@ class FileService:
"usedcount",
"code",
}
VIEW_PRESET_STATUS_VALUES = {"all", "active", "expired"}
VIEW_PRESET_TYPE_VALUES = {"all", "file", "text", "chunked"}
VIEW_PRESET_HEALTH_VALUES = {
"all",
"attention",
"danger",
"warning",
"healthy",
"expired",
"expiring_soon",
"storage_issue",
"never_retrieved",
"permanent",
}
VIEW_PRESET_SORT_FIELDS = {
"created_at",
"expired_at",
"name",
"size",
"used_count",
"code",
}
def __init__(self):
self.file_storage: FileStorageInterface = storages[settings.file_storage]()
@@ -178,6 +205,77 @@ class FileService:
)
return await self.get_file_detail(file_id)
async def list_file_view_presets(self) -> dict[str, Any]:
presets = await self._get_file_view_presets()
return {
"presets": presets,
"items": presets,
"total": len(presets),
}
async def save_file_view_preset(
self,
preset_id: Optional[str],
name: str,
filters: dict[str, Any],
) -> dict[str, Any]:
presets = await self._get_file_view_presets()
normalized_name = self._normalize_file_view_preset_name(name)
normalized_filters = self._normalize_file_view_preset_filters(filters)
now = await get_now()
updated_at = now.isoformat()
target_index = next(
(index for index, preset in enumerate(presets) if preset["id"] == preset_id),
-1,
)
if target_index >= 0:
preset = presets[target_index]
next_preset = {
**preset,
"name": normalized_name,
"filters": normalized_filters,
"params": normalized_filters,
"updatedAt": updated_at,
"updated_at": updated_at,
}
presets[target_index] = next_preset
else:
if len(presets) >= self.MAX_VIEW_PRESETS:
raise HTTPException(status_code=400, detail="视图预设数量已达上限")
next_preset = {
"id": preset_id or self._build_file_view_preset_id(normalized_name, now),
"name": normalized_name,
"filters": normalized_filters,
"params": normalized_filters,
"createdAt": updated_at,
"created_at": updated_at,
"updatedAt": updated_at,
"updated_at": updated_at,
}
presets.append(next_preset)
await self._save_file_view_presets(presets)
return next_preset
async def delete_file_view_preset(self, preset_id: str) -> dict[str, Any]:
preset_id = str(preset_id).strip()
if not preset_id:
raise HTTPException(status_code=400, detail="请选择要删除的视图预设")
presets = await self._get_file_view_presets()
next_presets = [preset for preset in presets if preset["id"] != preset_id]
if len(next_presets) == len(presets):
raise HTTPException(status_code=404, detail="视图预设不存在")
await self._save_file_view_presets(next_presets)
return {
"deleted": preset_id,
"deletedPresetId": preset_id,
"deleted_preset_id": preset_id,
"total": len(next_presets),
}
async def apply_files_policy_action(
self,
file_ids: list[int],
@@ -533,6 +631,120 @@ class FileService:
"updated_at": updated_at,
}
async def _get_file_view_presets(self) -> list[dict[str, Any]]:
record = await KeyValue.filter(key=self.FILE_VIEW_PRESETS_KEY).first()
raw_presets = record.value if record else []
if isinstance(raw_presets, dict):
raw_presets = raw_presets.get("presets") or raw_presets.get("items") or []
if not isinstance(raw_presets, list):
return []
presets = []
seen_ids = set()
for raw_preset in raw_presets:
try:
preset = self._normalize_file_view_preset(raw_preset)
except HTTPException:
continue
if not preset or preset["id"] in seen_ids:
continue
seen_ids.add(preset["id"])
presets.append(preset)
if len(presets) >= self.MAX_VIEW_PRESETS:
break
return presets
async def _save_file_view_presets(self, presets: list[dict[str, Any]]) -> None:
await KeyValue.update_or_create(
key=self.FILE_VIEW_PRESETS_KEY,
defaults={"value": {"presets": presets}},
)
def _normalize_file_view_preset(self, preset: Any) -> Optional[dict[str, Any]]:
if not isinstance(preset, dict):
return None
preset_id = str(preset.get("id") or "").strip()
raw_name = str(preset.get("name") or "").strip()
if not raw_name:
return None
name = self._normalize_file_view_preset_name(raw_name)
if not preset_id:
preset_id = self._build_file_view_preset_id(name)
filters = preset.get("filters") or preset.get("params") or {}
normalized_filters = self._normalize_file_view_preset_filters(filters)
created_at = preset.get("createdAt") or preset.get("created_at")
updated_at = preset.get("updatedAt") or preset.get("updated_at")
return {
"id": preset_id,
"name": name,
"filters": normalized_filters,
"params": normalized_filters,
"createdAt": created_at,
"created_at": created_at,
"updatedAt": updated_at,
"updated_at": updated_at,
}
def _normalize_file_view_preset_name(self, name: Any) -> str:
normalized_name = str(name or "").strip()
if not normalized_name:
raise HTTPException(status_code=400, detail="请输入视图名称")
return normalized_name[: self.MAX_VIEW_PRESET_NAME_LENGTH]
def _normalize_file_view_preset_filters(self, filters: Any) -> dict[str, Any]:
if not isinstance(filters, dict):
filters = {}
sort_by = str(filters.get("sortBy") or filters.get("sort_by") or "created_at")
sort_by = sort_by.replace("-", "_").strip().lower()
if sort_by not in self.VIEW_PRESET_SORT_FIELDS:
sort_by = "created_at"
sort_order = str(filters.get("sortOrder") or filters.get("sort_order") or "desc")
sort_order = sort_order.strip().lower()
if sort_order not in {"asc", "desc"}:
sort_order = "desc"
size = filters.get("size", 10)
try:
size = int(size)
except (TypeError, ValueError):
size = 10
return {
"keyword": str(filters.get("keyword") or "").strip()[
: self.MAX_VIEW_PRESET_KEYWORD_LENGTH
],
"status": self._normalize_file_view_preset_choice(
filters.get("status"), self.VIEW_PRESET_STATUS_VALUES
),
"type": self._normalize_file_view_preset_choice(
filters.get("type"), self.VIEW_PRESET_TYPE_VALUES
),
"health": self._normalize_file_view_preset_choice(
filters.get("health"), self.VIEW_PRESET_HEALTH_VALUES
),
"sortBy": sort_by,
"sortOrder": sort_order,
"size": min(max(size, 1), 100),
}
def _normalize_file_view_preset_choice(self, value: Any, allowed_values: set[str]) -> str:
normalized_value = str(value or "all").strip().lower()
if normalized_value not in allowed_values:
return "all"
return normalized_value
def _build_file_view_preset_id(
self, name: str, timestamp: Optional[datetime] = None
) -> str:
seed = int(timestamp.timestamp() * 1000) if timestamp else "saved"
digest = hashlib.sha1(name.encode("utf-8")).hexdigest()[:8]
return f"view_{seed}_{digest}"
def _build_file_status_insights(
self,
file_code: FileCodes,
+63
View File
@@ -21,6 +21,8 @@ from apps.admin.schemas import (
BatchFilePolicyActionData,
FilePolicyActionData,
FileMetadataData,
FileViewPresetData,
FileViewPresetDeleteData,
ShareItem,
DeleteItem,
LoginData,
@@ -380,6 +382,67 @@ async def file_metadata_post(
return await update_file_metadata(data, file_service)
@admin_api.get("/file/view-presets")
async def file_view_presets(
file_service: FileService = Depends(get_file_service),
):
result = await file_service.list_file_view_presets()
return APIResponse(detail=result)
async def save_file_view_preset(
data: FileViewPresetData,
file_service: FileService,
):
filters = data.filters if data.filters is not None else data.params
preset = await file_service.save_file_view_preset(
preset_id=data.id,
name=data.name,
filters=filters or {},
)
return APIResponse(detail=preset)
@admin_api.post("/file/view-presets")
async def file_view_presets_save(
data: FileViewPresetData,
file_service: FileService = Depends(get_file_service),
):
return await save_file_view_preset(data, file_service)
@admin_api.patch("/file/view-presets")
async def file_view_presets_patch(
data: FileViewPresetData,
file_service: FileService = Depends(get_file_service),
):
return await save_file_view_preset(data, file_service)
async def delete_file_view_preset(
data: FileViewPresetDeleteData,
file_service: FileService,
):
result = await file_service.delete_file_view_preset(data.id)
return APIResponse(detail=result)
@admin_api.delete("/file/view-presets")
async def file_view_presets_delete(
data: FileViewPresetDeleteData,
file_service: FileService = Depends(get_file_service),
):
return await delete_file_view_preset(data, file_service)
@admin_api.post("/file/view-presets/delete")
async def file_view_presets_delete_post(
data: FileViewPresetDeleteData,
file_service: FileService = Depends(get_file_service),
):
return await delete_file_view_preset(data, file_service)
@admin_api.get("/config/get")
async def get_config(
config_service: ConfigService = Depends(get_config_service),