add webdav
This commit is contained in:
parent
764a723023
commit
25dec10b67
97
WEBDAV_GUIDE.md
Normal file
97
WEBDAV_GUIDE.md
Normal 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` 文件夹,享受完整的文件管理功能!
|
||||
161
fastapi_app.py
161
fastapi_app.py
@ -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
35
poetry.lock
generated
@ -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"
|
||||
|
||||
@ -25,6 +25,7 @@ dependencies = [
|
||||
"openpyxl>=3.0.0",
|
||||
"xlrd>=2.0.0",
|
||||
"chardet>=5.0.0",
|
||||
"wsgidav>=4.0.0",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -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
174
webdav_server.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user