feat: add admin file metadata
This commit is contained in:
@@ -55,3 +55,9 @@ class BatchFilePolicyActionData(BaseModel):
|
|||||||
action: str
|
action: str
|
||||||
downloadLimit: Optional[int] = None
|
downloadLimit: Optional[int] = None
|
||||||
download_limit: Optional[int] = None
|
download_limit: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FileMetadataData(BaseModel):
|
||||||
|
id: int
|
||||||
|
note: Optional[str] = None
|
||||||
|
tags: Optional[list[str]] = None
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ from core.utils import get_now, hash_password, is_password_hashed
|
|||||||
|
|
||||||
|
|
||||||
class FileService:
|
class FileService:
|
||||||
|
FILE_METADATA_KEY_PREFIX = "admin_file_metadata:"
|
||||||
|
MAX_METADATA_NOTE_LENGTH = 2000
|
||||||
|
MAX_METADATA_TAGS = 12
|
||||||
|
MAX_METADATA_TAG_LENGTH = 24
|
||||||
|
|
||||||
POLICY_ACTIONS = {
|
POLICY_ACTIONS = {
|
||||||
"extend_24h",
|
"extend_24h",
|
||||||
"extend_7d",
|
"extend_7d",
|
||||||
@@ -37,9 +42,13 @@ class FileService:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.file_storage: FileStorageInterface = storages[settings.file_storage]()
|
self.file_storage: FileStorageInterface = storages[settings.file_storage]()
|
||||||
|
|
||||||
|
def _file_metadata_key(self, file_id: int) -> str:
|
||||||
|
return f"{self.FILE_METADATA_KEY_PREFIX}{file_id}"
|
||||||
|
|
||||||
async def _delete_file_code(self, file_code: FileCodes):
|
async def _delete_file_code(self, file_code: FileCodes):
|
||||||
if file_code.text is None:
|
if file_code.text is None:
|
||||||
await self.file_storage.delete_file(file_code)
|
await self.file_storage.delete_file(file_code)
|
||||||
|
await KeyValue.filter(key=self._file_metadata_key(file_code.id)).delete()
|
||||||
await file_code.delete()
|
await file_code.delete()
|
||||||
|
|
||||||
async def delete_file(self, file_id: int):
|
async def delete_file(self, file_id: int):
|
||||||
@@ -136,6 +145,39 @@ class FileService:
|
|||||||
await file_code.update_from_dict(update_data).save()
|
await file_code.update_from_dict(update_data).save()
|
||||||
return await self.get_file_detail(file_id)
|
return await self.get_file_detail(file_id)
|
||||||
|
|
||||||
|
async def get_file_metadata(self, file_id: int) -> dict[str, Any]:
|
||||||
|
record = await KeyValue.filter(key=self._file_metadata_key(file_id)).first()
|
||||||
|
return self._normalize_file_metadata(record.value if record else None)
|
||||||
|
|
||||||
|
async def update_file_metadata(
|
||||||
|
self,
|
||||||
|
file_id: int,
|
||||||
|
note: Optional[str],
|
||||||
|
tags: Optional[list[str]],
|
||||||
|
update_note: bool,
|
||||||
|
update_tags: bool,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
file_code = await FileCodes.filter(id=file_id).first()
|
||||||
|
if not file_code:
|
||||||
|
raise HTTPException(status_code=404, detail="文件不存在")
|
||||||
|
|
||||||
|
current_metadata = await self.get_file_metadata(file_id)
|
||||||
|
next_metadata = dict(current_metadata)
|
||||||
|
if update_note:
|
||||||
|
next_metadata["note"] = self._normalize_metadata_note(note)
|
||||||
|
if update_tags:
|
||||||
|
next_metadata["tags"] = self._normalize_metadata_tags(tags)
|
||||||
|
|
||||||
|
now = await get_now()
|
||||||
|
updated_at = now.isoformat()
|
||||||
|
next_metadata["updatedAt"] = updated_at
|
||||||
|
next_metadata["updated_at"] = updated_at
|
||||||
|
await KeyValue.update_or_create(
|
||||||
|
key=self._file_metadata_key(file_id),
|
||||||
|
defaults={"value": next_metadata},
|
||||||
|
)
|
||||||
|
return await self.get_file_detail(file_id)
|
||||||
|
|
||||||
async def apply_files_policy_action(
|
async def apply_files_policy_action(
|
||||||
self,
|
self,
|
||||||
file_ids: list[int],
|
file_ids: list[int],
|
||||||
@@ -437,8 +479,60 @@ class FileService:
|
|||||||
"timeline": timeline,
|
"timeline": timeline,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
metadata = await self.get_file_metadata(file_id)
|
||||||
|
detail.update(
|
||||||
|
{
|
||||||
|
"metadata": metadata,
|
||||||
|
"meta": metadata,
|
||||||
|
"note": metadata["note"],
|
||||||
|
"tags": metadata["tags"],
|
||||||
|
"metadataUpdatedAt": metadata["updatedAt"],
|
||||||
|
"metadata_updated_at": metadata["updated_at"],
|
||||||
|
}
|
||||||
|
)
|
||||||
return detail
|
return detail
|
||||||
|
|
||||||
|
def _normalize_metadata_note(self, note: Optional[str]) -> str:
|
||||||
|
if note is None:
|
||||||
|
return ""
|
||||||
|
return str(note).strip()[: self.MAX_METADATA_NOTE_LENGTH]
|
||||||
|
|
||||||
|
def _normalize_metadata_tags(self, tags: Any) -> list[str]:
|
||||||
|
if not tags:
|
||||||
|
return []
|
||||||
|
if isinstance(tags, str):
|
||||||
|
tags = [tags]
|
||||||
|
elif not isinstance(tags, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
normalized_tags = []
|
||||||
|
seen_tags = set()
|
||||||
|
for raw_tag in tags:
|
||||||
|
tag = str(raw_tag).strip()
|
||||||
|
if not tag:
|
||||||
|
continue
|
||||||
|
tag = tag[: self.MAX_METADATA_TAG_LENGTH]
|
||||||
|
dedupe_key = tag.lower()
|
||||||
|
if dedupe_key in seen_tags:
|
||||||
|
continue
|
||||||
|
seen_tags.add(dedupe_key)
|
||||||
|
normalized_tags.append(tag)
|
||||||
|
if len(normalized_tags) >= self.MAX_METADATA_TAGS:
|
||||||
|
break
|
||||||
|
return normalized_tags
|
||||||
|
|
||||||
|
def _normalize_file_metadata(self, metadata: Any) -> dict[str, Any]:
|
||||||
|
if not isinstance(metadata, dict):
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
updated_at = metadata.get("updatedAt") or metadata.get("updated_at")
|
||||||
|
return {
|
||||||
|
"note": self._normalize_metadata_note(metadata.get("note")),
|
||||||
|
"tags": self._normalize_metadata_tags(metadata.get("tags")),
|
||||||
|
"updatedAt": updated_at,
|
||||||
|
"updated_at": updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
def _build_file_status_insights(
|
def _build_file_status_insights(
|
||||||
self,
|
self,
|
||||||
file_code: FileCodes,
|
file_code: FileCodes,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from apps.admin.schemas import (
|
|||||||
BatchUpdateFileData,
|
BatchUpdateFileData,
|
||||||
BatchFilePolicyActionData,
|
BatchFilePolicyActionData,
|
||||||
FilePolicyActionData,
|
FilePolicyActionData,
|
||||||
|
FileMetadataData,
|
||||||
ShareItem,
|
ShareItem,
|
||||||
DeleteItem,
|
DeleteItem,
|
||||||
LoginData,
|
LoginData,
|
||||||
@@ -343,6 +344,42 @@ async def file_detail_post(
|
|||||||
return APIResponse(detail=detail)
|
return APIResponse(detail=detail)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_file_metadata(
|
||||||
|
data: FileMetadataData,
|
||||||
|
file_service: FileService,
|
||||||
|
):
|
||||||
|
fields_set = data.model_fields_set
|
||||||
|
update_note = "note" in fields_set
|
||||||
|
update_tags = "tags" in fields_set
|
||||||
|
if not update_note and not update_tags:
|
||||||
|
raise HTTPException(status_code=400, detail="请选择要更新的元数据")
|
||||||
|
|
||||||
|
detail = await file_service.update_file_metadata(
|
||||||
|
file_id=data.id,
|
||||||
|
note=data.note,
|
||||||
|
tags=data.tags,
|
||||||
|
update_note=update_note,
|
||||||
|
update_tags=update_tags,
|
||||||
|
)
|
||||||
|
return APIResponse(detail=detail)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.patch("/file/metadata")
|
||||||
|
async def file_metadata(
|
||||||
|
data: FileMetadataData,
|
||||||
|
file_service: FileService = Depends(get_file_service),
|
||||||
|
):
|
||||||
|
return await update_file_metadata(data, file_service)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.post("/file/metadata")
|
||||||
|
async def file_metadata_post(
|
||||||
|
data: FileMetadataData,
|
||||||
|
file_service: FileService = Depends(get_file_service),
|
||||||
|
):
|
||||||
|
return await update_file_metadata(data, file_service)
|
||||||
|
|
||||||
|
|
||||||
@admin_api.get("/config/get")
|
@admin_api.get("/config/get")
|
||||||
async def get_config(
|
async def get_config(
|
||||||
config_service: ConfigService = Depends(get_config_service),
|
config_service: ConfigService = Depends(get_config_service),
|
||||||
|
|||||||
Reference in New Issue
Block a user