add webdav support

This commit is contained in:
朱潮 2026-03-09 12:31:07 +08:00
parent b277c9bbff
commit 49034bc571
2 changed files with 37 additions and 10 deletions

View File

@ -4,20 +4,43 @@ WebDAV 文件管理路由
""" """
import os import os
import secrets
import shutil import shutil
import mimetypes import mimetypes
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone from datetime import datetime, timezone
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
from fastapi import APIRouter, Request, HTTPException, Response from fastapi import APIRouter, Request, HTTPException, Response, Depends
from fastapi.responses import FileResponse, Response as FastAPIResponse from fastapi.responses import FileResponse, Response as FastAPIResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import logging import logging
from utils.settings import WEBDAV_USERNAME, WEBDAV_PASSWORD
logger = logging.getLogger('app') logger = logging.getLogger('app')
security = HTTPBasic(realm="WebDAV")
def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)):
"""验证 WebDAV Basic Auth 账号密码"""
username_correct = secrets.compare_digest(credentials.username, WEBDAV_USERNAME)
password_correct = secrets.compare_digest(credentials.password, WEBDAV_PASSWORD)
if not (username_correct and password_correct):
raise HTTPException(
status_code=401,
detail="认证失败",
headers={"WWW-Authenticate": 'Basic realm="WebDAV"'},
)
return credentials
# 不在 router 级别加认证OPTIONS 需要免认证macOS Finder 等客户端预检不带凭据)
router = APIRouter(prefix="/webdav", tags=["webdav"]) router = APIRouter(prefix="/webdav", tags=["webdav"])
# 需要认证的依赖,用于非 OPTIONS 的端点
auth_dep = [Depends(verify_credentials)]
# WebDAV XML 命名空间 # WebDAV XML 命名空间
DAV_NS = "DAV:" DAV_NS = "DAV:"
DAV_PREFIX = "{DAV:}" DAV_PREFIX = "{DAV:}"
@ -107,8 +130,8 @@ def build_multistatus(responses: list[ET.Element]) -> str:
# ===== PROPFIND ===== # ===== PROPFIND =====
@router.api_route("/{bot_id}/{path:path}", methods=["PROPFIND"]) @router.api_route("/{bot_id}/{path:path}", methods=["PROPFIND"], dependencies=auth_dep)
@router.api_route("/{bot_id}", methods=["PROPFIND"]) @router.api_route("/{bot_id}", methods=["PROPFIND"], dependencies=auth_dep)
async def propfind(bot_id: str, request: Request, path: str = ""): async def propfind(bot_id: str, request: Request, path: str = ""):
"""PROPFIND - 获取资源属性(列出目录)""" """PROPFIND - 获取资源属性(列出目录)"""
bot_root = get_bot_root(bot_id) bot_root = get_bot_root(bot_id)
@ -166,7 +189,7 @@ async def options(bot_id: str, path: str = ""):
# ===== GET ===== # ===== GET =====
@router.get("/{bot_id}/{path:path}") @router.get("/{bot_id}/{path:path}", dependencies=auth_dep)
async def get_file(bot_id: str, path: str): async def get_file(bot_id: str, path: str):
"""GET - 下载文件""" """GET - 下载文件"""
bot_root = get_bot_root(bot_id) bot_root = get_bot_root(bot_id)
@ -192,7 +215,7 @@ async def get_file(bot_id: str, path: str):
# ===== HEAD ===== # ===== HEAD =====
@router.head("/{bot_id}/{path:path}") @router.head("/{bot_id}/{path:path}", dependencies=auth_dep)
async def head_file(bot_id: str, path: str): async def head_file(bot_id: str, path: str):
"""HEAD - 获取文件元数据""" """HEAD - 获取文件元数据"""
bot_root = get_bot_root(bot_id) bot_root = get_bot_root(bot_id)
@ -214,7 +237,7 @@ async def head_file(bot_id: str, path: str):
# ===== PUT ===== # ===== PUT =====
@router.put("/{bot_id}/{path:path}") @router.put("/{bot_id}/{path:path}", dependencies=auth_dep)
async def put_file(bot_id: str, path: str, request: Request): async def put_file(bot_id: str, path: str, request: Request):
"""PUT - 上传/覆盖文件""" """PUT - 上传/覆盖文件"""
bot_root = get_bot_root(bot_id) bot_root = get_bot_root(bot_id)
@ -234,7 +257,7 @@ async def put_file(bot_id: str, path: str, request: Request):
# ===== DELETE ===== # ===== DELETE =====
@router.delete("/{bot_id}/{path:path}") @router.delete("/{bot_id}/{path:path}", dependencies=auth_dep)
async def delete_resource(bot_id: str, path: str): async def delete_resource(bot_id: str, path: str):
"""DELETE - 删除文件或目录""" """DELETE - 删除文件或目录"""
if not path.strip("/"): if not path.strip("/"):
@ -256,7 +279,7 @@ async def delete_resource(bot_id: str, path: str):
# ===== MKCOL ===== # ===== MKCOL =====
@router.api_route("/{bot_id}/{path:path}", methods=["MKCOL"]) @router.api_route("/{bot_id}/{path:path}", methods=["MKCOL"], dependencies=auth_dep)
async def mkcol(bot_id: str, path: str): async def mkcol(bot_id: str, path: str):
"""MKCOL - 创建目录""" """MKCOL - 创建目录"""
bot_root = get_bot_root(bot_id) bot_root = get_bot_root(bot_id)
@ -274,7 +297,7 @@ async def mkcol(bot_id: str, path: str):
# ===== COPY ===== # ===== COPY =====
@router.api_route("/{bot_id}/{path:path}", methods=["COPY"]) @router.api_route("/{bot_id}/{path:path}", methods=["COPY"], dependencies=auth_dep)
async def copy_resource(bot_id: str, path: str, request: Request): async def copy_resource(bot_id: str, path: str, request: Request):
"""COPY - 复制文件或目录""" """COPY - 复制文件或目录"""
bot_root = get_bot_root(bot_id) bot_root = get_bot_root(bot_id)
@ -315,7 +338,7 @@ async def copy_resource(bot_id: str, path: str, request: Request):
# ===== MOVE ===== # ===== MOVE =====
@router.api_route("/{bot_id}/{path:path}", methods=["MOVE"]) @router.api_route("/{bot_id}/{path:path}", methods=["MOVE"], dependencies=auth_dep)
async def move_resource(bot_id: str, path: str, request: Request): async def move_resource(bot_id: str, path: str, request: Request):
"""MOVE - 移动/重命名文件或目录""" """MOVE - 移动/重命名文件或目录"""
bot_root = get_bot_root(bot_id) bot_root = get_bot_root(bot_id)

View File

@ -39,6 +39,10 @@ TOOL_OUTPUT_TRUNCATION_STRATEGY = os.getenv("TOOL_OUTPUT_TRUNCATION_STRATEGY", "
DEFAULT_THINKING_ENABLE = os.getenv("DEFAULT_THINKING_ENABLE", "true") == "true" DEFAULT_THINKING_ENABLE = os.getenv("DEFAULT_THINKING_ENABLE", "true") == "true"
# WebDAV Authentication
WEBDAV_USERNAME = os.getenv("WEBDAV_USERNAME", "admin")
WEBDAV_PASSWORD = os.getenv("WEBDAV_PASSWORD", "MmL85TjjxZC97hk9rsYfhQ")
# MCP Tool Timeout Settings # MCP Tool Timeout Settings
MCP_HTTP_TIMEOUT = int(os.getenv("MCP_HTTP_TIMEOUT", 60)) # HTTP 请求超时(秒) MCP_HTTP_TIMEOUT = int(os.getenv("MCP_HTTP_TIMEOUT", 60)) # HTTP 请求超时(秒)
MCP_SSE_READ_TIMEOUT = int(os.getenv("MCP_SSE_READ_TIMEOUT", 300)) # SSE 读取超时(秒) MCP_SSE_READ_TIMEOUT = int(os.getenv("MCP_SSE_READ_TIMEOUT", 300)) # SSE 读取超时(秒)