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.responses import StreamingResponse, HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware 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 qwen_agent.llm.schema import ASSISTANT, FUNCTION
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -1029,163 +1031,18 @@ def build_directory_tree(path: str, relative_path: str = "") -> dict:
return tree 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__": 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) uvicorn.run(app, host="0.0.0.0", port=8001)

35
poetry.lock generated
View File

@ -620,6 +620,18 @@ websocket-client = "*"
[package.extras] [package.extras]
tokenizer = ["tiktoken"] 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]] [[package]]
name = "distro" name = "distro"
version = "1.9.0" 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"] optional = ["python-socks", "wsaccel"]
test = ["pytest", "websockets"] 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]] [[package]]
name = "xlrd" name = "xlrd"
version = "2.0.2" version = "2.0.2"
@ -4072,4 +4105,4 @@ propcache = ">=0.2.1"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "3.12.0" python-versions = "3.12.0"
content-hash = "e9161f4f141b9dbc6037c5cfe89a044c33e0e556b420cc2df66528a1d3328f46" content-hash = "8220c65ac40a6be66d2b743dc99682edb1528430873a8776c9b1d0af5ef13d0b"

View File

@ -25,6 +25,7 @@ dependencies = [
"openpyxl>=3.0.0", "openpyxl>=3.0.0",
"xlrd>=2.0.0", "xlrd>=2.0.0",
"chardet>=5.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 click==8.3.0
cryptography==46.0.3 cryptography==46.0.3
dashscope==1.24.6 dashscope==1.24.6
defusedxml==0.7.1
distro==1.9.0 distro==1.9.0
dotenv==0.9.9 dotenv==0.9.9
et_xmlfile==2.0.0 et_xmlfile==2.0.0
@ -94,6 +95,7 @@ tzdata==2025.2
urllib3==2.5.0 urllib3==2.5.0
uvicorn==0.35.0 uvicorn==0.35.0
websocket-client==1.9.0 websocket-client==1.9.0
WsgiDAV==4.3.3
xlrd==2.0.2 xlrd==2.0.2
xlsxwriter==3.2.9 xlsxwriter==3.2.9
yarl==1.22.0 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()