test/custom-admin-ui #3

Merged
orion merged 673 commits from test/custom-admin-ui into master 2026-06-05 17:20:58 +08:00
8 changed files with 2138 additions and 85 deletions
Showing only changes of commit 8e4d21f02f - Show all commits
+12
View File
@@ -85,6 +85,18 @@ def _require_admin_payload(authorization: str) -> dict:
return payload
def get_admin_session(authorization: str = Header(default=None)) -> dict:
token = _extract_bearer_token(authorization)
payload = _require_admin_payload(authorization)
return {
"id": "admin",
"username": "admin",
"token": token,
"token_type": "Bearer",
"expires_at": payload.get("exp"),
}
ADMIN_PUBLIC_ENDPOINTS = {("POST", "/admin/login")}
+44 -1
View File
@@ -1,5 +1,5 @@
import datetime
from typing import Optional, Union
from typing import Any, Optional, Union
from pydantic import BaseModel
@@ -8,6 +8,10 @@ class IDData(BaseModel):
id: int
class IDsData(BaseModel):
ids: list[int]
class ShareItem(BaseModel):
expire_value: int
expire_style: str = "day"
@@ -29,3 +33,42 @@ class UpdateFileData(BaseModel):
suffix: Optional[str] = None
expired_at: Optional[Union[datetime.datetime, str]] = None
expired_count: Optional[int] = None
class BatchUpdateFileData(BaseModel):
ids: list[int]
expired_at: Optional[Union[datetime.datetime, str]] = None
expired_count: Optional[int] = None
clearExpiredAt: Optional[bool] = None
clear_expired_at: Optional[bool] = None
class FilePolicyActionData(BaseModel):
id: int
action: str
downloadLimit: Optional[int] = None
download_limit: Optional[int] = None
class BatchFilePolicyActionData(BaseModel):
ids: list[int]
action: str
downloadLimit: Optional[int] = None
download_limit: Optional[int] = None
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
+1460 -37
View File
File diff suppressed because it is too large Load Diff
+446 -11
View File
@@ -3,16 +3,32 @@
# @File : views.py
# @Software: PyCharm
import datetime
from collections import Counter
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from apps.admin.services import FileService, ConfigService, LocalFileService
from apps.admin.dependencies import (
admin_required,
get_admin_session,
get_file_service,
get_config_service,
get_local_file_service,
)
from apps.admin.schemas import IDData, ShareItem, DeleteItem, LoginData, UpdateFileData
from apps.admin.schemas import (
IDData,
IDsData,
BatchUpdateFileData,
BatchFilePolicyActionData,
FilePolicyActionData,
FileMetadataData,
FileViewPresetData,
FileViewPresetDeleteData,
ShareItem,
DeleteItem,
LoginData,
UpdateFileData,
)
from core.response import APIResponse
from apps.base.models import FileCodes, KeyValue
from apps.admin.dependencies import create_token
@@ -24,6 +40,14 @@ admin_api = APIRouter(
)
def _pick_query_text(*values: Optional[str]) -> Optional[str]:
for value in values:
normalized_value = str(value or "").strip()
if normalized_value:
return normalized_value
return None
@admin_api.post("/login")
async def login(data: LoginData):
if not verify_password(data.password, settings.admin_token):
@@ -33,10 +57,37 @@ async def login(data: LoginData):
return APIResponse(detail={"token": token, "token_type": "Bearer"})
@admin_api.get("/verify")
async def verify_admin(session: dict = Depends(get_admin_session)):
return APIResponse(detail=session)
@admin_api.post("/logout")
async def logout_admin():
return APIResponse(detail={"ok": True})
async def build_dashboard_recent_file(file_code: FileCodes) -> dict:
is_expired = await file_code.is_expired()
return {
"id": file_code.id,
"code": file_code.code,
"name": file_code.prefix + file_code.suffix,
"suffix": file_code.suffix,
"size": file_code.size,
"text": file_code.text is not None,
"expiredAt": file_code.expired_at,
"expiredCount": file_code.expired_count,
"usedCount": file_code.used_count,
"createdAt": file_code.created_at,
"isExpired": is_expired,
}
@admin_api.get("/dashboard")
async def dashboard():
async def dashboard(file_service: FileService = Depends(get_file_service)):
all_codes = await FileCodes.all()
all_size = str(sum([code.size for code in all_codes]))
all_size = sum([code.size for code in all_codes])
sys_start = await KeyValue.filter(key="sys_start").first()
now = await get_now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
@@ -46,19 +97,83 @@ async def dashboard():
created_at__gte=yesterday_start, created_at__lte=yesterday_end
)
today_codes = FileCodes.filter(created_at__gte=today_start)
yesterday_file_codes = await yesterday_codes
today_file_codes = await today_codes
expired_count = 0
for file_code in all_codes:
if await file_code.is_expired():
expired_count += 1
health_summary = await file_service.build_file_health_summary(all_codes, now=now)
text_count = sum(1 for file_code in all_codes if file_code.text is not None)
chunked_count = sum(1 for file_code in all_codes if file_code.is_chunked)
used_count = sum([file_code.used_count for file_code in all_codes])
suffix_counter = Counter(
"Text" if file_code.text is not None else (file_code.suffix or "file")
for file_code in all_codes
)
recent_file_codes = sorted(
all_codes,
key=lambda file_code: file_code.created_at.timestamp()
if file_code.created_at
else 0,
reverse=True,
)[:8]
recent_activities = await file_service.list_admin_activities(limit=8)
return APIResponse(
detail={
"totalFiles": len(all_codes),
"storageUsed": all_size,
"sysUptime": sys_start.value,
"yesterdayCount": await yesterday_codes.count(),
"yesterdaySize": str(sum([code.size for code in await yesterday_codes])),
"todayCount": await today_codes.count(),
"todaySize": str(sum([code.size for code in await today_codes])),
"storageUsed": str(all_size),
"sysUptime": sys_start.value if sys_start else None,
"yesterdayCount": len(yesterday_file_codes),
"yesterdaySize": str(sum([code.size for code in yesterday_file_codes])),
"todayCount": len(today_file_codes),
"todaySize": str(sum([code.size for code in today_file_codes])),
"activeCount": len(all_codes) - expired_count,
"expiredCount": expired_count,
"textCount": text_count,
"fileCount": len(all_codes) - text_count,
"chunkedCount": chunked_count,
"usedCount": used_count,
"storageBackend": settings.file_storage,
"uploadSizeLimit": settings.uploadSize,
"openUpload": settings.openUpload,
"enableChunk": settings.enableChunk,
"maxSaveSeconds": settings.max_save_seconds,
**health_summary,
"healthSummary": health_summary,
"topSuffixes": [
{"suffix": suffix, "count": count}
for suffix, count in suffix_counter.most_common(8)
],
"recentFiles": [
await build_dashboard_recent_file(file_code)
for file_code in recent_file_codes
],
"recentActivities": recent_activities["activities"],
"recent_activities": recent_activities["activities"],
}
)
@admin_api.get("/activities")
async def admin_activities(
limit: int = 20,
action: Optional[str] = None,
targetType: Optional[str] = None,
target_type: Optional[str] = None,
keyword: Optional[str] = None,
file_service: FileService = Depends(get_file_service),
):
result = await file_service.list_admin_activities(
limit=limit,
action=action,
target_type=_pick_query_text(targetType, target_type),
keyword=keyword,
)
return APIResponse(detail=result)
@admin_api.delete("/file/delete")
async def file_delete(
data: IDData,
@@ -68,24 +183,296 @@ async def file_delete(
return APIResponse()
async def batch_delete_files(
data: IDsData,
file_service: FileService,
):
if not data.ids:
raise HTTPException(status_code=400, detail="请选择要删除的文件")
result = await file_service.delete_files(data.ids)
return APIResponse(detail=result)
@admin_api.delete("/file/batch-delete")
async def file_batch_delete(
data: IDsData,
file_service: FileService = Depends(get_file_service),
):
return await batch_delete_files(data, file_service)
@admin_api.post("/file/batch-delete")
async def file_batch_delete_post(
data: IDsData,
file_service: FileService = Depends(get_file_service),
):
return await batch_delete_files(data, file_service)
async def batch_update_files(
data: BatchUpdateFileData,
file_service: FileService,
):
if not data.ids:
raise HTTPException(status_code=400, detail="请选择要更新的文件")
update_data = {}
fields_set = data.model_fields_set
should_clear_expired_at = bool(data.clearExpiredAt or data.clear_expired_at)
if should_clear_expired_at:
update_data["expired_at"] = None
update_data["expired_count"] = -1
elif "expired_at" in fields_set and data.expired_at != "":
update_data["expired_at"] = data.expired_at
if (
not should_clear_expired_at
and "expired_count" in fields_set
and data.expired_count is not None
):
update_data["expired_count"] = data.expired_count
if not update_data:
raise HTTPException(status_code=400, detail="请选择要更新的字段")
result = await file_service.update_files(data.ids, update_data)
return APIResponse(detail=result)
@admin_api.patch("/file/batch-update")
async def file_batch_update(
data: BatchUpdateFileData,
file_service: FileService = Depends(get_file_service),
):
return await batch_update_files(data, file_service)
@admin_api.post("/file/batch-update")
async def file_batch_update_post(
data: BatchUpdateFileData,
file_service: FileService = Depends(get_file_service),
):
return await batch_update_files(data, file_service)
async def apply_file_policy_action(
data: FilePolicyActionData,
file_service: FileService,
):
download_limit = data.downloadLimit
if download_limit is None:
download_limit = data.download_limit
detail = await file_service.apply_file_policy_action(
file_id=data.id,
action=data.action,
download_limit=download_limit,
)
return APIResponse(detail=detail)
@admin_api.patch("/file/policy-action")
async def file_policy_action(
data: FilePolicyActionData,
file_service: FileService = Depends(get_file_service),
):
return await apply_file_policy_action(data, file_service)
@admin_api.post("/file/policy-action")
async def file_policy_action_post(
data: FilePolicyActionData,
file_service: FileService = Depends(get_file_service),
):
return await apply_file_policy_action(data, file_service)
async def apply_batch_file_policy_action(
data: BatchFilePolicyActionData,
file_service: FileService,
):
if not data.ids:
raise HTTPException(status_code=400, detail="请选择要更新的文件")
download_limit = data.downloadLimit
if download_limit is None:
download_limit = data.download_limit
result = await file_service.apply_files_policy_action(
file_ids=data.ids,
action=data.action,
download_limit=download_limit,
)
return APIResponse(detail=result)
@admin_api.patch("/file/batch-policy-action")
async def file_batch_policy_action(
data: BatchFilePolicyActionData,
file_service: FileService = Depends(get_file_service),
):
return await apply_batch_file_policy_action(data, file_service)
@admin_api.post("/file/batch-policy-action")
async def file_batch_policy_action_post(
data: BatchFilePolicyActionData,
file_service: FileService = Depends(get_file_service),
):
return await apply_batch_file_policy_action(data, file_service)
@admin_api.get("/file/list")
async def file_list(
page: int = 1,
size: int = 10,
keyword: str = "",
status: str = "",
type: str = "",
health: str = "",
sortBy: str = "created_at",
sortOrder: str = "desc",
file_service: FileService = Depends(get_file_service),
):
files, total = await file_service.list_files(page, size, keyword)
page = max(page, 1)
size = min(max(size, 1), 100)
files, total, summary = await file_service.list_files(
page,
size,
keyword,
status=status,
file_type=type,
health=health,
sort_by=sortBy,
sort_order=sortOrder,
)
return APIResponse(
detail={
"page": page,
"size": size,
"data": files,
"total": total,
"summary": summary,
}
)
@admin_api.get("/file/detail")
async def file_detail(
id: int,
file_service: FileService = Depends(get_file_service),
):
detail = await file_service.get_file_detail(id)
return APIResponse(detail=detail)
@admin_api.post("/file/detail")
async def file_detail_post(
data: IDData,
file_service: FileService = Depends(get_file_service),
):
detail = await file_service.get_file_detail(data.id)
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("/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),
@@ -97,9 +484,17 @@ async def get_config(
async def update_config(
data: dict,
config_service: ConfigService = Depends(get_config_service),
file_service: FileService = Depends(get_file_service),
):
data.pop("themesChoices")
data.pop("themesChoices", None)
await config_service.update_config(data)
await file_service.record_admin_activity(
action="config.update",
target_type="config",
target_name="system",
count=1,
meta={"fields": sorted(data.keys())},
)
return APIResponse()
@@ -112,6 +507,16 @@ async def file_download(
return file_content
@admin_api.get("/file/preview")
async def file_preview(
id: int,
maxChars: int = 4000,
file_service: FileService = Depends(get_file_service),
):
preview = await file_service.preview_file(id, maxChars)
return APIResponse(detail=preview)
@admin_api.get("/local/lists")
async def get_local_lists(
local_file_service: LocalFileService = Depends(get_local_file_service),
@@ -124,8 +529,16 @@ async def get_local_lists(
async def delete_local_file(
item: DeleteItem,
local_file_service: LocalFileService = Depends(get_local_file_service),
file_service: FileService = Depends(get_file_service),
):
result = await local_file_service.delete_file(item.filename)
await file_service.record_admin_activity(
action="local_file.delete",
target_type="local_file",
target_name=item.filename,
count=1,
meta={"success": bool(result)},
)
return APIResponse(detail=result)
@@ -135,16 +548,29 @@ async def share_local_file(
file_service: FileService = Depends(get_file_service),
):
share_info = await file_service.share_local_file(item)
await file_service.record_admin_activity(
action="local_file.share",
target_type="file",
target_id=share_info.get("id") if isinstance(share_info, dict) else None,
target_name=item.filename,
count=1,
meta={
"expireValue": item.expire_value,
"expireStyle": item.expire_style,
},
)
return APIResponse(detail=share_info)
@admin_api.patch("/file/update")
async def update_file(
data: UpdateFileData,
file_service: FileService = Depends(get_file_service),
):
file_code = await FileCodes.filter(id=data.id).first()
if not file_code:
raise HTTPException(status_code=404, detail="文件不存在")
target_name = file_service._build_file_activity_name(file_code)
update_data = {}
if data.code is not None and data.code != file_code.code:
@@ -166,4 +592,13 @@ async def update_file(
update_data["expired_count"] = data.expired_count
await file_code.update_from_dict(update_data).save()
if update_data:
await file_service.record_admin_activity(
action="file.update",
target_type="file",
target_id=data.id,
target_name=target_name,
count=1,
meta={"fields": sorted(update_data.keys())},
)
return APIResponse(detail="更新成功")
+102 -17
View File
@@ -7,7 +7,8 @@ from urllib.parse import unquote
from typing import Optional, Tuple, Union
from fastapi import APIRouter, Form, UploadFile, File, Depends, HTTPException
from fastapi import APIRouter, Form, Request, UploadFile, File, Depends, HTTPException
from pydantic import BaseModel, ValidationError
from starlette import status
from apps.admin.dependencies import share_required_login
@@ -176,6 +177,64 @@ async def update_file_usage(file_code: FileCodes) -> None:
await file_code.save()
def build_file_metadata(file_code: FileCodes) -> dict:
is_text = file_code.text is not None
remaining_downloads = (
file_code.expired_count if file_code.expired_count > 0 else None
)
return {
"code": file_code.code,
"name": file_code.prefix + file_code.suffix,
"size": file_code.size,
"type": "text" if is_text else "file",
"is_text": is_text,
"created_at": file_code.created_at,
"expired_at": file_code.expired_at,
"expires_at": file_code.expired_at,
"expired_count": file_code.expired_count,
"used_count": file_code.used_count,
"remaining_downloads": remaining_downloads,
}
async def build_select_detail(
file_code: FileCodes, file_storage: FileStorageInterface
) -> dict:
metadata = build_file_metadata(file_code)
download_url = (
None if file_code.text is not None else await file_storage.get_file_url(file_code)
)
content = file_code.text if file_code.text is not None else None
return {
**metadata,
"text": content if content is not None else download_url,
"content": content,
"download_url": download_url,
}
@share_api.get("/metadata/")
async def get_file_metadata(code: str, ip: str = Depends(ip_limit["error"])):
has, file_code = await get_code_file_by_code(code)
if not has:
ip_limit["error"].add_ip(ip)
return APIResponse(code=404, detail=file_code)
assert isinstance(file_code, FileCodes)
return APIResponse(detail=build_file_metadata(file_code))
@share_api.post("/metadata/")
async def post_file_metadata(data: SelectFileModel, ip: str = Depends(ip_limit["error"])):
has, file_code = await get_code_file_by_code(data.code)
if not has:
ip_limit["error"].add_ip(ip)
return APIResponse(code=404, detail=file_code)
assert isinstance(file_code, FileCodes)
return APIResponse(detail=build_file_metadata(file_code))
@share_api.get("/select/")
async def get_code_file(code: str, ip: str = Depends(ip_limit["error"])):
file_storage: FileStorageInterface = storages[settings.file_storage]()
@@ -199,18 +258,7 @@ async def select_file(data: SelectFileModel, ip: str = Depends(ip_limit["error"]
assert isinstance(file_code, FileCodes)
await update_file_usage(file_code)
return APIResponse(
detail={
"code": file_code.code,
"name": file_code.prefix + file_code.suffix,
"size": file_code.size,
"text": (
file_code.text
if file_code.text is not None
else await file_storage.get_file_url(file_code)
),
}
)
return APIResponse(detail=await build_select_detail(file_code, file_storage))
@share_api.get("/download")
@@ -233,8 +281,30 @@ async def download_file(key: str, code: str, ip: str = Depends(ip_limit["error"]
chunk_api = APIRouter(prefix="/chunk", tags=["切片"])
async def parse_body_model(request: Request, model_class: type[BaseModel]):
content_type = request.headers.get("content-type", "").lower()
try:
if "application/x-www-form-urlencoded" in content_type or "multipart/form-data" in content_type:
payload = dict(await request.form())
else:
payload = await request.json()
return model_class.model_validate(payload)
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors())
except Exception:
raise HTTPException(status_code=400, detail="请求体格式错误")
async def parse_init_chunk_upload(request: Request) -> InitChunkUploadModel:
return await parse_body_model(request, InitChunkUploadModel)
async def parse_complete_upload(request: Request) -> CompleteUploadModel:
return await parse_body_model(request, CompleteUploadModel)
@chunk_api.post("/upload/init/", dependencies=[Depends(share_required_login)])
async def init_chunk_upload(data: InitChunkUploadModel):
async def init_chunk_upload(data: InitChunkUploadModel = Depends(parse_init_chunk_upload)):
# 服务端校验:根据 total_chunks * chunk_size 计算理论最大上传量
total_chunks = (data.file_size + data.chunk_size - 1) // data.chunk_size
max_possible_size = total_chunks * data.chunk_size
@@ -444,7 +514,9 @@ async def get_upload_status(upload_id: str):
"/upload/complete/{upload_id}", dependencies=[Depends(share_required_login)]
)
async def complete_upload(
upload_id: str, data: CompleteUploadModel, ip: str = Depends(ip_limit["upload"])
upload_id: str,
data: CompleteUploadModel = Depends(parse_complete_upload),
ip: str = Depends(ip_limit["upload"]),
):
# 获取上传基本信息
chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
@@ -524,6 +596,14 @@ presign_api = APIRouter(prefix="/presign", tags=["预签名上传"])
PRESIGN_SESSION_EXPIRES = 900 # 15分钟
def build_proxy_upload_urls(upload_id: str) -> dict:
proxy_upload_url = f"/presign/upload/proxy/{upload_id}"
return {
"proxy_upload_url": proxy_upload_url,
"legacy_proxy_upload_url": f"/api{proxy_upload_url}",
}
async def _get_valid_session(
upload_id: str, expected_mode: Optional[str] = None
) -> PresignUploadSession:
@@ -563,7 +643,8 @@ async def presign_upload_init(
)
mode = "direct" if presigned_url else "proxy"
upload_url = presigned_url or f"/api/presign/upload/proxy/{upload_id}"
proxy_urls = build_proxy_upload_urls(upload_id)
upload_url = presigned_url or proxy_urls["proxy_upload_url"]
await PresignUploadSession.create(
upload_id=upload_id,
@@ -577,13 +658,17 @@ async def presign_upload_init(
)
ip_limit["upload"].add_ip(ip)
return APIResponse(
detail = {
"upload_id": upload_id,
"upload_url": upload_url,
"mode": mode,
"expires_in": PRESIGN_SESSION_EXPIRES,
}
if mode == "proxy":
detail.update(proxy_urls)
return APIResponse(
detail=detail
)
+5
View File
@@ -12,4 +12,9 @@ T = TypeVar("T")
class APIResponse(BaseModel, Generic[T]):
code: int = 200
message: str = "ok"
msg: Optional[str] = None
detail: Optional[T] = None
def model_post_init(self, __context) -> None:
if self.msg is None:
self.msg = self.message
+1
View File
@@ -0,0 +1 @@
APP_VERSION = "2.0.0-dev"
+60 -11
View File
@@ -24,6 +24,46 @@ from core.response import APIResponse
from core.settings import settings, BASE_DIR, DEFAULT_CONFIG
from core.tasks import delete_expire_files, clean_incomplete_uploads
from core.utils import hash_password, is_password_hashed
from core.version import APP_VERSION
def build_public_config() -> dict:
return {
"name": settings.name,
"description": settings.description,
"explain": settings.page_explain,
"uploadSize": settings.uploadSize,
"expireStyle": settings.expireStyle,
"enableChunk": settings.enableChunk,
"openUpload": settings.openUpload,
"notify_title": settings.notify_title,
"notify_content": settings.notify_content,
"show_admin_address": settings.showAdminAddr,
"max_save_seconds": settings.max_save_seconds,
}
def build_public_meta() -> dict:
return {
"version": APP_VERSION,
"api": {
"legacyConfig": "/",
"publicConfig": "/api/v1/config",
"health": "/health",
},
"features": {
"chunkUpload": bool(settings.enableChunk),
"guestUpload": bool(settings.openUpload),
"adminAddressVisible": bool(settings.showAdminAddr),
"expirationModes": settings.expireStyle,
},
"limits": {
"uploadSize": settings.uploadSize,
"maxSaveSeconds": settings.max_save_seconds,
"uploadWindowMinutes": settings.uploadMinute,
"uploadWindowCount": settings.uploadCount,
},
}
@asynccontextmanager
@@ -110,6 +150,7 @@ register_tortoise(
app.include_router(share_api)
app.include_router(chunk_api)
app.include_router(presign_api)
app.include_router(presign_api, prefix="/api")
app.include_router(admin_api)
@@ -139,19 +180,27 @@ async def robots():
@app.post("/")
async def get_config():
return APIResponse(detail=build_public_config())
@app.get("/api/v1/config")
async def get_public_config():
return APIResponse(
detail={
"name": settings.name,
"description": settings.description,
"explain": settings.page_explain,
"uploadSize": settings.uploadSize,
"expireStyle": settings.expireStyle,
"enableChunk": settings.enableChunk,
"openUpload": settings.openUpload,
"notify_title": settings.notify_title,
"notify_content": settings.notify_content,
"show_admin_address": settings.showAdminAddr,
"max_save_seconds": settings.max_save_seconds,
"config": build_public_config(),
"meta": build_public_meta(),
}
)
@app.get("/health")
async def health_check():
return APIResponse(
detail={
"status": "ok",
"version": APP_VERSION,
"storage": settings.file_storage,
"theme": settings.themesSelect,
}
)