add webdav

This commit is contained in:
朱潮 2025-11-07 14:06:54 +08:00
parent 764a723023
commit 25dec10b67
6 changed files with 317 additions and 153 deletions

97
WEBDAV_GUIDE.md Normal file
View File

@ -0,0 +1,97 @@
# WebDAV 文件管理服务
您的项目现在已经成功集成了 WebDAV 文件管理服务,替代了原来功能单一的 `/api/v1/projects/tree``/api/v1/projects/subtree` 接口。
## 功能特性
- ✅ 完整的文件/文件夹操作(增删改查、移动、重命名)
- ✅ 支持大文件上传/下载
- ✅ 支持文件夹批量操作
- ✅ 可挂载为网络驱动器
- ✅ 支持任何WebDAV客户端
## 访问方式
### 1. 浏览器访问
直接在浏览器中打开:
```
http://localhost:8001/webdav
```
### 2. 系统挂载
#### Windows
1. 打开 "此电脑"
2. 右键点击 "网络位置" → "添加一个网络位置"
3. 输入:`http://localhost:8001/webdav`
4. 完成,即可在网络驱动器中访问
#### macOS
1. 打开 "访达"
2. 前往 → 连接服务器Cmd+K
3. 输入:`http://localhost:8001/webdav`
4. 连接
#### Linux
使用 davfs2 挂载:
```bash
sudo mount -t davfs http://localhost:8001/webdav /mnt/webdav
```
### 3. 移动端App
使用任何WebDAV客户端App
- iOS: Documents by Readdle, WebDAV Navigator
- Android: Solid Explorer, WebDAV Client
## API 操作示例
### 查看目录列表
```bash
curl -X PROPFIND http://localhost:8001/webdav/
```
### 上传文件
```bash
curl -X PUT http://localhost:8001/webdav/test.txt -d "Hello World"
```
### 下载文件
```bash
curl http://localhost:8001/webdav/test.txt
```
### 创建文件夹
```bash
curl -X MKCOL http://localhost:8001/webdav/new_folder
```
### 删除文件
```bash
curl -X DELETE http://localhost:8001/webdav/test.txt
```
### 移动文件
```bash
curl -X MOVE http://localhost:8001/webdav/test.txt -H "Destination: /webdav/backup/test.txt"
```
## 安全说明
当前配置为开发环境,无需认证即可访问。在生产环境中,建议:
1. 启用HTTPS
2. 配置用户认证
3. 限制访问权限
## 支持的文件操作
| 操作 | HTTP方法 | 说明 |
|------|----------|------|
| 读取文件/目录 | PROPFIND | 获取文件信息和目录列表 |
| 创建文件 | PUT | 上传或更新文件 |
| 创建目录 | MKCOL | 创建新文件夹 |
| 删除文件/目录 | DELETE | 删除文件或空目录 |
| 移动/重命名 | MOVE | 移动或重命名文件/目录 |
| 复制 | COPY | 复制文件/目录 |
现在您可以使用任何支持WebDAV的工具来管理您的 `projects` 文件夹,享受完整的文件管理功能!

View File

@ -14,6 +14,8 @@ from fastapi import FastAPI, HTTPException, Depends, Header, UploadFile, File
from fastapi.responses import StreamingResponse, HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.wsgi import WSGIMiddleware
from webdav_server import create_webdav_app
from qwen_agent.llm.schema import ASSISTANT, FUNCTION
from pydantic import BaseModel, Field
@ -1029,163 +1031,18 @@ def build_directory_tree(path: str, relative_path: str = "") -> dict:
return tree
@app.get("/api/v1/projects/tree")
async def get_projects_tree(
include_files: bool = True,
max_depth: int = 10,
filter_type: Optional[str] = None
):
"""
获取projects文件夹的目录树结构
Args:
include_files: 是否包含文件false时只显示目录
max_depth: 最大深度限制
filter_type: 过滤类型 ('data', 'robot', 'uploads')
Returns:
dict: 包含目录树结构的响应
"""
try:
projects_dir = "projects"
if not os.path.exists(projects_dir):
return {
"success": False,
"message": "projects目录不存在",
"tree": {}
}
tree = build_directory_tree(projects_dir)
# 根据filter_type过滤
if filter_type and filter_type in ['data', 'robot', 'uploads']:
filtered_children = []
for child in tree.get("children", []):
if child["name"] == filter_type:
filtered_children.append(child)
tree["children"] = filtered_children
# 如果不包含文件,移除所有文件节点
if not include_files:
tree = filter_directories_only(tree)
# 计算统计信息
stats = calculate_tree_stats(tree)
return {
"success": True,
"message": "目录树获取成功",
"tree": tree,
"stats": stats,
"filters": {
"include_files": include_files,
"max_depth": max_depth,
"filter_type": filter_type
}
}
except Exception as e:
print(f"Error getting projects tree: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取目录树失败: {str(e)}")
def filter_directories_only(tree: dict) -> dict:
"""过滤掉文件,只保留目录"""
if tree["type"] != "directory":
return tree
filtered_children = []
for child in tree.get("children", []):
if child["type"] == "directory":
filtered_children.append(filter_directories_only(child))
tree["children"] = filtered_children
return tree
def calculate_tree_stats(tree: dict) -> dict:
"""计算目录树统计信息"""
stats = {
"total_directories": 0,
"total_files": 0,
"total_size": 0
}
def traverse(node):
if node["type"] == "directory":
stats["total_directories"] += 1
for child in node.get("children", []):
traverse(child)
else:
stats["total_files"] += 1
stats["total_size"] += node.get("size", 0)
traverse(tree)
return stats
@app.get("/api/v1/projects/subtree/{sub_path:path}")
async def get_projects_subtree(
sub_path: str,
include_files: bool = True,
max_depth: int = 5
):
"""
获取projects子目录的树结构
Args:
sub_path: 子目录路径 'data/1624be71-5432-40bf-9758-f4aecffd4e9c'
include_files: 是否包含文件
max_depth: 最大深度
Returns:
dict: 包含子目录树结构的响应
"""
try:
full_path = os.path.join("projects", sub_path)
if not os.path.exists(full_path):
return {
"success": False,
"message": f"路径不存在: {sub_path}",
"tree": {}
}
if not os.path.isdir(full_path):
return {
"success": False,
"message": f"路径不是目录: {sub_path}",
"tree": {}
}
tree = build_directory_tree(full_path, sub_path)
# 如果不包含文件,移除所有文件节点
if not include_files:
tree = filter_directories_only(tree)
# 计算统计信息
stats = calculate_tree_stats(tree)
return {
"success": True,
"message": "子目录树获取成功",
"sub_path": sub_path,
"tree": tree,
"stats": stats,
"filters": {
"include_files": include_files,
"max_depth": max_depth
}
}
except Exception as e:
print(f"Error getting projects subtree: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取子目录树失败: {str(e)}")
# 创建并挂载 WebDAV 应用 (无论以何种方式启动都要挂载)
webdav_app = create_webdav_app()
app.mount("/webdav", WSGIMiddleware(webdav_app))
if __name__ == "__main__":
# 启动 FastAPI 应用 (WebDAV 已经挂载到 /webdav 路径)
print("Starting FastAPI server with WebDAV support...")
print("WebDAV available at: http://localhost:8001/webdav")
print("API available at: http://localhost:8001")
uvicorn.run(app, host="0.0.0.0", port=8001)

35
poetry.lock generated
View File

@ -620,6 +620,18 @@ websocket-client = "*"
[package.extras]
tokenizer = ["tiktoken"]
[[package]]
name = "defusedxml"
version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
groups = ["main"]
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "distro"
version = "1.9.0"
@ -3895,6 +3907,27 @@ docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"]
optional = ["python-socks", "wsaccel"]
test = ["pytest", "websockets"]
[[package]]
name = "wsgidav"
version = "4.3.3"
description = "Generic and extendable WebDAV server based on WSGI"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "WsgiDAV-4.3.3-py3-none-any.whl", hash = "sha256:8d96b0f05ad7f280572e99d1c605962a853d715f8e934298555d0c47ef275e88"},
{file = "wsgidav-4.3.3.tar.gz", hash = "sha256:5f0ad71bea72def3018b6ba52da3bcb83f61e0873c27225344582805d6e52b9e"},
]
[package.dependencies]
defusedxml = "*"
Jinja2 = "*"
json5 = "*"
PyYAML = "*"
[package.extras]
pam = ["python-pam"]
[[package]]
name = "xlrd"
version = "2.0.2"
@ -4072,4 +4105,4 @@ propcache = ">=0.2.1"
[metadata]
lock-version = "2.1"
python-versions = "3.12.0"
content-hash = "e9161f4f141b9dbc6037c5cfe89a044c33e0e556b420cc2df66528a1d3328f46"
content-hash = "8220c65ac40a6be66d2b743dc99682edb1528430873a8776c9b1d0af5ef13d0b"

View File

@ -25,6 +25,7 @@ dependencies = [
"openpyxl>=3.0.0",
"xlrd>=2.0.0",
"chardet>=5.0.0",
"wsgidav>=4.0.0",
]

View File

@ -13,6 +13,7 @@ charset-normalizer==3.4.4
click==8.3.0
cryptography==46.0.3
dashscope==1.24.6
defusedxml==0.7.1
distro==1.9.0
dotenv==0.9.9
et_xmlfile==2.0.0
@ -94,6 +95,7 @@ tzdata==2025.2
urllib3==2.5.0
uvicorn==0.35.0
websocket-client==1.9.0
WsgiDAV==4.3.3
xlrd==2.0.2
xlsxwriter==3.2.9
yarl==1.22.0

174
webdav_server.py Normal file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
WebDAV 服务器模块
提供基于 WsgiDAV 的文件管理服务
"""
import os
from pathlib import Path
from wsgidav.wsgidav_app import WsgiDAVApp
from wsgidav.fs_dav_provider import FilesystemProvider
from wsgidav.http_authenticator import HTTPAuthenticator
class NullDomainController:
"""空域控制器,跳过认证"""
def __init__(self, wsgidav_app, config):
pass
def get_domain_realm(self, path_info, environ):
return None
def require_authentication(self, realm, environ):
return False # 不需要认证
def is_authenticated_user(self, realm, user_name, environ):
return True # 总是认证成功
def auth_user(self, realm, user_name, password, environ):
return True # 总是认证成功
def supports_http_digest_auth(self):
return True
def is_share_anonymous(self, share):
return True # 允许匿名访问
def create_webdav_app():
"""
创建 WebDAV 应用
Returns:
WsgiDAVApp: 配置好的 WebDAV 应用实例
"""
# 确保projects目录存在
projects_dir = Path("projects")
projects_dir.mkdir(exist_ok=True)
# 配置 WebDAV
config = {
"host": "0.0.0.0",
"port": 8001,
"provider_mapping": {
"/": FilesystemProvider(str(projects_dir))
},
"http_authenticator": {
"domain_controller": "webdav_server.NullDomainController",
"accept_basic": False,
"accept_digest": False,
"default_to_digest": False,
},
"verbose": 1, # 日志级别
"dir_browser": {
"enable": True,
"response_trailer": True,
"davmount_path": "/webdav",
"ms_mount_path": "/webdav",
}
}
# 创建 WsgiDAV 应用
app = WsgiDAVApp(config)
return app
class ProjectWebDAVProvider(FilesystemProvider):
"""
自定义的 WebDAV 提供器专门用于管理 projects 目录
"""
def __init__(self, root_folder="projects"):
super().__init__(root_folder)
self.root_folder = Path(root_folder)
def get_file_info(self, path):
"""获取文件信息"""
full_path = self.root_folder / path.lstrip('/')
if not full_path.exists():
return None
stat = full_path.stat()
return {
"path": path,
"name": full_path.name,
"is_dir": full_path.is_dir(),
"size": stat.st_size if full_path.is_file() else 0,
"modified": stat.st_mtime,
"created": stat.st_ctime
}
def create_directory(self, path, **kwargs):
"""创建目录"""
full_path = self.root_folder / path.lstrip('/')
full_path.mkdir(parents=True, exist_ok=True)
return True
def delete_file(self, path):
"""删除文件"""
full_path = self.root_folder / path.lstrip('/')
if full_path.is_file():
full_path.unlink()
elif full_path.is_dir():
import shutil
shutil.rmtree(full_path)
return True
def move_file(self, src_path, dest_path):
"""移动文件"""
src_full = self.root_folder / src_path.lstrip('/')
dest_full = self.root_folder / dest_path.lstrip('/')
# 确保目标目录存在
dest_full.parent.mkdir(parents=True, exist_ok=True)
import shutil
shutil.move(str(src_full), str(dest_full))
return True
def copy_file(self, src_path, dest_path):
"""复制文件"""
src_full = self.root_folder / src_path.lstrip('/')
dest_full = self.root_folder / dest_path.lstrip('/')
# 确保目标目录存在
dest_full.parent.mkdir(parents=True, exist_ok=True)
import shutil
if src_full.is_file():
shutil.copy2(str(src_full), str(dest_full))
elif src_full.is_dir():
shutil.copytree(str(src_full), str(dest_full))
return True
def start_webdav_server(host="0.0.0.0", port=8090):
"""
启动独立的 WebDAV 服务器
Args:
host: 主机地址
port: 端口号
"""
app = create_webdav_app()
# 修改配置,使用不同端口避免冲突
app.config["host"] = host
app.config["port"] = port
print(f"Starting WebDAV server on http://{host}:{port}/webdav")
print(f"Projects directory: {Path.cwd()}/projects")
# 启动服务器
from wsgidav.server.run_server import run_server
run_server(app)
if __name__ == "__main__":
start_webdav_server()