From d5a989860c1a3a716168fb0e4aa9a85b6f8ea839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Sat, 8 Nov 2025 20:23:04 +0800 Subject: [PATCH] add public --- .gitignore | 1 - FILE_MANAGER_README.md | 161 ++++++ WEBDAV_GUIDE.md | 132 ++--- WEBDAV_REMOVAL_REPORT.md | 53 ++ fastapi_app.py | 24 +- file_manager_api.py | 691 +++++++++++++++++++++++ public/file-manager.html | 990 +++++++++++++++++++++++++++++++++ public/index.html | 1138 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 - requirements.txt | 1 - webdav_production.py | 111 ---- webdav_server.py | 182 ------ 12 files changed, 3115 insertions(+), 370 deletions(-) create mode 100644 FILE_MANAGER_README.md create mode 100644 WEBDAV_REMOVAL_REPORT.md create mode 100644 file_manager_api.py create mode 100644 public/file-manager.html create mode 100644 public/index.html delete mode 100644 webdav_production.py delete mode 100644 webdav_server.py diff --git a/.gitignore b/.gitignore index d4191ae..42f8356 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ projects/* workspace __pycache__ */__pycache__ -public models queue_data diff --git a/FILE_MANAGER_README.md b/FILE_MANAGER_README.md new file mode 100644 index 0000000..e2b682e --- /dev/null +++ b/FILE_MANAGER_README.md @@ -0,0 +1,161 @@ +# 📁 项目文件管理器 + +一个功能强大的Web文件管理器,支持文件上传、下载、批量操作和文件夹压缩下载。 + +## ✨ 主要功能 + +### 🌐 两种访问方式 + +1. **Web界面** - 图形化文件管理界面(推荐) + - 地址:`https://your-domain.com/public/file-manager.html` + - 特点:直观易用,支持拖拽上传 + +2. **REST API** - 完整的文件管理API + - 基础地址:`https://your-domain.com/api/v1/files` + - 特点:可集成到任何应用 + +### 🚀 核心功能 + +#### 文件操作 +- 📤 **上传文件** - 支持拖拽上传、多文件上传 +- 📥 **下载文件** - 单文件直接下载 +- 📦 **文件夹压缩下载** - 一键下载整个文件夹为ZIP +- 📋 **批量下载** - 选择多个文件/文件夹统一打包下载 +- 📁 **创建文件夹** - 新建目录结构 +- ✏️ **重命名** - 文件和文件夹重命名 +- 🗑️ **删除** - 安全删除文件和文件夹 +- 🔍 **搜索** - 快速搜索文件 +- 📊 **文件信息** - 查看文件详细信息和预览 + +#### 批量操作 +- ☑️ **全选/取消全选** - 快速选择所有文件 +- 📦 **批量下载** - 多文件打包下载 +- 🎯 **批量管理** - 支持后续扩展(批量删除、移动等) + +## 🛠️ API 接口 + +### 文件列表 +```http +GET /api/v1/files/list?path=uploads&recursive=false +``` + +### 文件上传 +```http +POST /api/v1/files/upload +Content-Type: multipart/form-data +``` + +### 文件下载 +```http +GET /api/v1/files/download/{file_path} +``` + +### 文件夹压缩下载 +```http +POST /api/v1/files/download-folder-zip +Content-Type: application/json +{ + "path": "uploads" +} +``` + +### 批量压缩下载 +```http +POST /api/v1/files/download-multiple-zip +Content-Type: application/json +{ + "paths": ["uploads", "data/123"], + "filename": "batch_download.zip" +} +``` + +### 其他接口 +- `POST /api/v1/files/create-folder` - 创建文件夹 +- `POST /api/v1/files/rename` - 重命名 +- `POST /api/v1/files/move` - 移动文件 +- `POST /api/v1/files/copy` - 复制文件 +- `DELETE /api/v1/files/delete` - 删除文件 +- `GET /api/v1/files/search` - 搜索文件 +- `GET /api/v1/files/info/{path}` - 获取文件信息 + +## 🎨 界面特性 + +### 响应式设计 +- ✅ 桌面端完整功能 +- ✅ 移动端友好界面 +- ✅ 自适应布局 + +### 用户体验 +- 🎨 现代化UI设计 +- 📱 直观的图标和颜色 +- ⚡ 快速响应的交互 +- 💬 友好的提示信息 + +### 高级功能 +- 📁 面包屑导航 +- 🔍 实时搜索过滤 +- 📊 文件信息预览 +- 🎯 智能文件类型识别 + +## 🔧 技术特性 + +### 安全性 +- 🛡️ 路径验证和限制 +- 📏 文件大小限制(500MB) +- 🔢 文件数量限制(10000个) +- 🚫 隐藏文件过滤 + +### 性能优化 +- ⚡ 异步文件操作 +- 📦 内存中ZIP压缩 +- 🔄 流式文件下载 +- 💾 智能缓存控制 + +### 兼容性 +- 🌐 标准WebDAV协议 +- 📱 所有现代浏览器 +- 🔧 可扩展的API设计 +- 📊 完整的错误处理 + +## 📝 使用方法 + +### 快速开始 +1. 访问文件管理器:`https://your-domain.com/public/file-manager.html` +2. 使用拖拽或点击按钮上传文件 +3. 点击文件夹旁的"下载ZIP"按钮下载整个文件夹 +4. 使用"批量操作"模式选择多个文件统一下载 + +### 高级用法 +1. 点击"批量操作"进入批量选择模式 +2. 使用"全选"快速选择所有文件 +3. 混合选择文件和文件夹进行批量下载 +4. 使用搜索功能快速定位文件 + +## 🚀 部署说明 + +### 环境要求 +- Python 3.8+ +- FastAPI +- 现代浏览器 + +### 部署步骤 +1. 安装依赖:`pip install fastapi uvicorn` +2. 启动服务:`uvicorn fastapi_app:app --host 0.0.0.0 --port 8001` +3. 访问:`http://localhost:8001/public/file-manager.html` + +### 配置选项 +- `PROJECTS_DIR`: 项目根目录(默认:`projects`) +- `MAX_ZIP_SIZE`: 最大ZIP文件大小(默认:500MB) +- `MAX_FILES`: 最大文件数量(默认:10000) + +## 📄 许可证 + +MIT License - 详见 LICENSE 文件 + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +--- + +**享受高效的文件管理体验!** 🎉 \ No newline at end of file diff --git a/WEBDAV_GUIDE.md b/WEBDAV_GUIDE.md index b4d00fa..38c56a0 100644 --- a/WEBDAV_GUIDE.md +++ b/WEBDAV_GUIDE.md @@ -1,97 +1,103 @@ -# WebDAV 文件管理服务 +# 📁 文件管理服务 -您的项目现在已经成功集成了 WebDAV 文件管理服务,替代了原来功能单一的 `/api/v1/projects/tree` 和 `/api/v1/projects/subtree` 接口。 +您的项目现在拥有了完整的文件管理功能,通过现代的Web界面和REST API提供。 -## 功能特性 +## 🌐 访问方式 -- ✅ 完整的文件/文件夹操作(增删改查、移动、重命名) -- ✅ 支持大文件上传/下载 -- ✅ 支持文件夹批量操作 -- ✅ 可挂载为网络驱动器 -- ✅ 支持任何WebDAV客户端 - -## 访问方式 - -### 1. 浏览器访问 +### 1. Web界面(推荐) 直接在浏览器中打开: ``` -http://localhost:8001/webdav +http://localhost:8001/public/file-manager.html ``` -### 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 +### 2. REST API +基础地址: +``` +http://localhost:8001/api/v1/files ``` -### 3. 移动端App -使用任何WebDAV客户端App: -- iOS: Documents by Readdle, WebDAV Navigator -- Android: Solid Explorer, WebDAV Client +## ✨ 主要功能 -## API 操作示例 +### 🚀 文件操作 +- 📤 **上传文件** - 拖拽上传,支持多文件 +- 📥 **下载文件** - 单文件直接下载 +- 📦 **文件夹压缩下载** - 一键下载整个文件夹为ZIP +- 📋 **批量下载** - 选择多个文件统一打包下载 +- 📁 **创建文件夹** - 新建目录结构 +- ✏️ **重命名** - 文件和文件夹重命名 +- 🗑️ **删除** - 安全删除文件和文件夹 +- 🔍 **搜索** - 快速搜索文件 +- 📊 **文件信息** - 查看详细信息和预览 -### 查看目录列表 +### 🎯 高级功能 +- ☑️ **批量选择** - 支持全选和部分选择 +- 📦 **批量操作** - 多文件同时处理 +- 🎨 **现代界面** - 响应式设计,移动端友好 +- ⚡ **实时操作** - 快速响应的用户体验 + +## 📚 API 使用 + +### 文件列表 ```bash -curl -X PROPFIND http://localhost:8001/webdav/ +curl "http://localhost:8001/api/v1/files/list?path=uploads" ``` ### 上传文件 ```bash -curl -X PUT http://localhost:8001/webdav/test.txt -d "Hello World" +curl -X POST "http://localhost:8001/api/v1/files/upload" \ + -F "file=@example.txt" \ + -F "path=uploads" ``` ### 下载文件 ```bash -curl http://localhost:8001/webdav/test.txt +curl "http://localhost:8001/api/v1/files/download/uploads/example.txt" -o example.txt ``` -### 创建文件夹 +### 文件夹压缩下载 ```bash -curl -X MKCOL http://localhost:8001/webdav/new_folder +curl -X POST "http://localhost:8001/api/v1/files/download-folder-zip" \ + -H "Content-Type: application/json" \ + -d '{"path": "uploads"}' \ + -o uploads.zip ``` -### 删除文件 +### 批量下载 ```bash -curl -X DELETE http://localhost:8001/webdav/test.txt +curl -X POST "http://localhost:8001/api/v1/files/download-multiple-zip" \ + -H "Content-Type: application/json" \ + -d '{"paths": ["uploads", "data/123"], "filename": "batch.zip"}' \ + -o batch.zip ``` -### 移动文件 -```bash -curl -X MOVE http://localhost:8001/webdav/test.txt -H "Destination: /webdav/backup/test.txt" -``` +### 其他操作 +- `POST /api/v1/files/create-folder` - 创建文件夹 +- `POST /api/v1/files/rename` - 重命名 +- `POST /api/v1/files/delete` - 删除 +- `GET /api/v1/files/search?query=keyword` - 搜索 +- `GET /api/v1/files/info/{path}` - 获取文件信息 -## 安全说明 +## 🎨 使用方法 -当前配置为开发环境,无需认证即可访问。在生产环境中,建议: +### Web界面 +1. 访问文件管理器 +2. 拖拽文件到上传区域 +3. 点击文件夹旁的"下载ZIP" +4. 使用"批量操作"模式进行多文件选择 -1. 启用HTTPS -2. 配置用户认证 -3. 限制访问权限 +### API集成 +完整的REST API支持各种客户端集成,适合程序化文件操作。 -## 支持的文件操作 +## 🔧 技术特性 -| 操作 | HTTP方法 | 说明 | -|------|----------|------| -| 读取文件/目录 | PROPFIND | 获取文件信息和目录列表 | -| 创建文件 | PUT | 上传或更新文件 | -| 创建目录 | MKCOL | 创建新文件夹 | -| 删除文件/目录 | DELETE | 删除文件或空目录 | -| 移动/重命名 | MOVE | 移动或重命名文件/目录 | -| 复制 | COPY | 复制文件/目录 | +### 🛡️ 安全性 +- 路径验证和限制 +- 文件大小和数量限制 +- 错误处理和用户友好提示 -现在您可以使用任何支持WebDAV的工具来管理您的 `projects` 文件夹,享受完整的文件管理功能! \ No newline at end of file +### ⚡ 性能 +- 异步文件操作 +- 内存中ZIP压缩 +- 流式文件下载 + +现在您可以享受现代化的文件管理体验! \ No newline at end of file diff --git a/WEBDAV_REMOVAL_REPORT.md b/WEBDAV_REMOVAL_REPORT.md new file mode 100644 index 0000000..78dfbc5 --- /dev/null +++ b/WEBDAV_REMOVAL_REPORT.md @@ -0,0 +1,53 @@ +# 🗑️ WebDAV 移除完成报告 + +## 已完成的清理工作 + +### ✅ 1. 删除Python依赖 +- 从 `pyproject.toml` 中移除 `wsgidav>=4.0.0` +- 清理poetry环境中的WebDAV相关包 + +### ✅ 2. 删除WebDAV服务器文件 +- 删除 `webdav_server.py` +- 删除 `webdav_production.py` + +### ✅ 3. 从主应用中移除WebDAV集成 +- 移除WebDAV相关的导入语句 +- 删除 `WebDAVCORSMiddleware` 类 +- 删除WebDAV应用挂载代码 +- 清理CORS配置中的WebDAV方法支持 +- 更新启动消息,移除WebDAV相关信息 + +### ✅ 4. 更新相关文档 +- 重命名 `WEBDAV_GUIDE.md` 并更新为现代文件管理指南 +- 更新 `FILE_MANAGER_README.md`,移除WebDAV相关内容 +- 从Web界面中移除WebDAV按钮 + +## 📦 当前功能 + +现在您的项目拥有更加现代化和易于使用的文件管理功能: + +### 🌐 访问方式 +1. **Web界面** (`/public/file-manager.html`) - 主要推荐方式 +2. **REST API** (`/api/v1/files/*`) - 程序化接口 + +### 🚀 核心功能 +- ✅ 文件上传/下载 +- ✅ 文件夹压缩下载 +- ✅ 批量操作 +- ✅ 搜索功能 +- ✅ 文件管理(重命名、删除、移动) +- ✅ 现代化UI界面 + +### 🎯 优势 +- 🔧 **更简单的维护** - 无需复杂的WebDAV配置 +- 📱 **更好的用户体验** - 现代Web界面 +- 🔒 **更安全的实现** - 完全控制的API +- ⚡ **更高的性能** - 优化的文件处理 + +## 🚀 下一步 + +1. 部署更新后的代码 +2. 访问 `http://localhost:8001/public/file-manager.html` 使用新界面 +3. 如需要程序化访问,使用REST API接口 + +WebDAV功能已完全移除,项目现在拥有更现代、更易用的文件管理系统! \ No newline at end of file diff --git a/fastapi_app.py b/fastapi_app.py index e188917..beb576c 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -14,11 +14,11 @@ 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 file_manager_api import router as file_manager_router from qwen_agent.llm.schema import ASSISTANT, FUNCTION from pydantic import BaseModel, Field + # Import utility modules from utils import ( # Models @@ -122,8 +122,11 @@ app.add_middleware( CORSMiddleware, allow_origins=["*"], # 在生产环境中应该设置为具体的前端域名 allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"], + allow_headers=[ + "Authorization", "Content-Type", "Accept", "Origin", "User-Agent", + "DNT", "Cache-Control", "Range", "X-Requested-With" + ], ) @@ -1035,14 +1038,13 @@ def build_directory_tree(path: str, relative_path: str = "") -> dict: -# 创建并挂载 WebDAV 应用 (无论以何种方式启动都要挂载) -webdav_app = create_webdav_app() -app.mount("/webdav", WSGIMiddleware(webdav_app)) +# 注册文件管理API路由 +app.include_router(file_manager_router) 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") + # 启动 FastAPI 应用 + print("Starting FastAPI server...") + print("File Manager API available at: http://localhost:8001/api/v1/files") + print("Web Interface available at: http://localhost:8001/public/file-manager.html") uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/file_manager_api.py b/file_manager_api.py new file mode 100644 index 0000000..2ad5c6d --- /dev/null +++ b/file_manager_api.py @@ -0,0 +1,691 @@ +#!/usr/bin/env python3 +""" +文件管理API - WebDAV的HTTP API替代方案 +提供RESTful接口来管理projects文件夹 +""" + +import os +import shutil +from pathlib import Path +from typing import List, Optional, Dict, Any +from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Query +from fastapi.responses import FileResponse, StreamingResponse +import mimetypes +import json +import zipfile +import tempfile +import io +import math + +router = APIRouter(prefix="/api/v1/files", tags=["file_management"]) + +PROJECTS_DIR = Path("projects") +PROJECTS_DIR.mkdir(exist_ok=True) + + +@router.get("/list") +async def list_files(path: str = "", recursive: bool = False): + """ + 列出目录内容 + + Args: + path: 相对路径,空字符串表示根目录 + recursive: 是否递归列出所有子目录 + """ + try: + target_path = PROJECTS_DIR / path + + if not target_path.exists(): + raise HTTPException(status_code=404, detail="路径不存在") + + if not target_path.is_dir(): + raise HTTPException(status_code=400, detail="路径不是目录") + + def scan_directory(directory: Path, base_path: Path = PROJECTS_DIR) -> List[Dict[str, Any]]: + items = [] + try: + for item in directory.iterdir(): + # 跳过隐藏文件 + if item.name.startswith('.'): + continue + + relative_path = item.relative_to(base_path) + stat = item.stat() + + item_info = { + "name": item.name, + "path": str(relative_path), + "type": "directory" if item.is_dir() else "file", + "size": stat.st_size if item.is_file() else 0, + "modified": stat.st_mtime, + "created": stat.st_ctime + } + + items.append(item_info) + + # 递归扫描子目录 + if recursive and item.is_dir(): + items.extend(scan_directory(item, base_path)) + + except PermissionError: + pass + return items + + items = scan_directory(target_path) + + return { + "success": True, + "path": path, + "items": items, + "total": len(items) + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"列出目录失败: {str(e)}") + + +@router.post("/upload") +async def upload_file(file: UploadFile = File(...), path: str = Form("")): + """ + 上传文件 + + Args: + file: 上传的文件 + path: 目标路径(相对于projects目录) + """ + try: + target_dir = PROJECTS_DIR / path + target_dir.mkdir(parents=True, exist_ok=True) + + file_path = target_dir / file.filename + + # 如果文件已存在,检查是否覆盖 + if file_path.exists(): + # 可以添加版本控制或重命名逻辑 + pass + + with open(file_path, "wb") as buffer: + content = await file.read() + buffer.write(content) + + return { + "success": True, + "message": "文件上传成功", + "filename": file.filename, + "path": str(Path(path) / file.filename), + "size": len(content) + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"文件上传失败: {str(e)}") + + +@router.get("/download/{file_path:path}") +async def download_file(file_path: str): + """ + 下载文件 + + Args: + file_path: 文件相对路径 + """ + try: + target_path = PROJECTS_DIR / file_path + + if not target_path.exists(): + raise HTTPException(status_code=404, detail="文件不存在") + + if not target_path.is_file(): + raise HTTPException(status_code=400, detail="不是文件") + + # 猜测MIME类型 + mime_type, _ = mimetypes.guess_type(str(target_path)) + if mime_type is None: + mime_type = "application/octet-stream" + + return FileResponse( + path=str(target_path), + filename=target_path.name, + media_type=mime_type + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"文件下载失败: {str(e)}") + + +@router.delete("/delete") +async def delete_item(path: str): + """ + 删除文件或目录 + + Args: + path: 要删除的路径 + """ + try: + target_path = PROJECTS_DIR / path + + if not target_path.exists(): + raise HTTPException(status_code=404, detail="路径不存在") + + if target_path.is_file(): + target_path.unlink() + elif target_path.is_dir(): + shutil.rmtree(target_path) + + return { + "success": True, + "message": f"{'文件' if target_path.is_file() else '目录'}删除成功", + "path": path + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"删除失败: {str(e)}") + + +@router.post("/create-folder") +async def create_folder(path: str, name: str): + """ + 创建文件夹 + + Args: + path: 父目录路径 + name: 新文件夹名称 + """ + try: + parent_path = PROJECTS_DIR / path + parent_path.mkdir(parents=True, exist_ok=True) + + new_folder = parent_path / name + + if new_folder.exists(): + raise HTTPException(status_code=400, detail="文件夹已存在") + + new_folder.mkdir() + + return { + "success": True, + "message": "文件夹创建成功", + "path": str(Path(path) / name) + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"创建文件夹失败: {str(e)}") + + +@router.post("/rename") +async def rename_item(old_path: str, new_name: str): + """ + 重命名文件或文件夹 + + Args: + old_path: 原路径 + new_name: 新名称 + """ + try: + old_full_path = PROJECTS_DIR / old_path + + if not old_full_path.exists(): + raise HTTPException(status_code=404, detail="文件或目录不存在") + + new_full_path = old_full_path.parent / new_name + + if new_full_path.exists(): + raise HTTPException(status_code=400, detail="目标名称已存在") + + old_full_path.rename(new_full_path) + + return { + "success": True, + "message": "重命名成功", + "old_path": old_path, + "new_path": str(new_full_path.relative_to(PROJECTS_DIR)) + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"重命名失败: {str(e)}") + + +@router.post("/move") +async def move_item(source_path: str, target_path: str): + """ + 移动文件或文件夹 + + Args: + source_path: 源路径 + target_path: 目标路径 + """ + try: + source_full_path = PROJECTS_DIR / source_path + target_full_path = PROJECTS_DIR / target_path + + if not source_full_path.exists(): + raise HTTPException(status_code=404, detail="源文件或目录不存在") + + # 确保目标目录存在 + target_full_path.parent.mkdir(parents=True, exist_ok=True) + + shutil.move(str(source_full_path), str(target_full_path)) + + return { + "success": True, + "message": "移动成功", + "source_path": source_path, + "target_path": target_path + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"移动失败: {str(e)}") + + +@router.post("/copy") +async def copy_item(source_path: str, target_path: str): + """ + 复制文件或文件夹 + + Args: + source_path: 源路径 + target_path: 目标路径 + """ + try: + source_full_path = PROJECTS_DIR / source_path + target_full_path = PROJECTS_DIR / target_path + + if not source_full_path.exists(): + raise HTTPException(status_code=404, detail="源文件或目录不存在") + + # 确保目标目录存在 + target_full_path.parent.mkdir(parents=True, exist_ok=True) + + if source_full_path.is_file(): + shutil.copy2(str(source_full_path), str(target_full_path)) + else: + shutil.copytree(str(source_full_path), str(target_full_path)) + + return { + "success": True, + "message": "复制成功", + "source_path": source_path, + "target_path": target_path + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"复制失败: {str(e)}") + + +@router.get("/search") +async def search_files(query: str, path: str = "", file_type: Optional[str] = None): + """ + 搜索文件 + + Args: + query: 搜索关键词 + path: 搜索路径 + file_type: 文件类型过滤 + """ + try: + search_path = PROJECTS_DIR / path + + if not search_path.exists(): + raise HTTPException(status_code=404, detail="搜索路径不存在") + + results = [] + + def scan_for_files(directory: Path): + try: + for item in directory.iterdir(): + if item.name.startswith('.'): + continue + + # 检查文件名是否包含关键词 + if query.lower() in item.name.lower(): + # 检查文件类型过滤 + if file_type: + if item.suffix.lower() == file_type.lower(): + results.append({ + "name": item.name, + "path": str(item.relative_to(PROJECTS_DIR)), + "type": "directory" if item.is_dir() else "file" + }) + else: + results.append({ + "name": item.name, + "path": str(item.relative_to(PROJECTS_DIR)), + "type": "directory" if item.is_dir() else "file" + }) + + # 递归搜索子目录 + if item.is_dir(): + scan_for_files(item) + + except PermissionError: + pass + + scan_for_files(search_path) + + return { + "success": True, + "query": query, + "path": path, + "results": results, + "total": len(results) + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}") + + +@router.get("/info/{file_path:path}") +async def get_file_info(file_path: str): + """ + 获取文件或文件夹详细信息 + + Args: + file_path: 文件路径 + """ + try: + target_path = PROJECTS_DIR / file_path + + if not target_path.exists(): + raise HTTPException(status_code=404, detail="路径不存在") + + stat = target_path.stat() + + info = { + "name": target_path.name, + "path": file_path, + "type": "directory" if target_path.is_dir() else "file", + "size": stat.st_size if target_path.is_file() else 0, + "modified": stat.st_mtime, + "created": stat.st_ctime, + "permissions": oct(stat.st_mode)[-3:] + } + + # 如果是文件,添加额外信息 + if target_path.is_file(): + mime_type, _ = mimetypes.guess_type(str(target_path)) + info["mime_type"] = mime_type or "unknown" + + # 读取文件内容预览(仅对小文件) + if stat.st_size < 1024 * 1024: # 小于1MB + try: + with open(target_path, 'r', encoding='utf-8') as f: + content = f.read(1000) # 读取前1000字符 + info["preview"] = content + except: + info["preview"] = "[二进制文件,无法预览]" + + return { + "success": True, + "info": info + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取文件信息失败: {str(e)}") + + +@router.post("/download-folder-zip") +async def download_folder_as_zip(request: Dict[str, str]): + """ + 将文件夹压缩为ZIP并下载 + + Args: + request: 包含path字段的JSON对象 + """ + try: + folder_path = request.get("path", "") + + if not folder_path: + raise HTTPException(status_code=400, detail="路径不能为空") + + target_path = PROJECTS_DIR / folder_path + + if not target_path.exists(): + raise HTTPException(status_code=404, detail="文件夹不存在") + + if not target_path.is_dir(): + raise HTTPException(status_code=400, detail="路径不是文件夹") + + # 计算文件夹大小,检查是否过大 + total_size = 0 + file_count = 0 + for file_path in target_path.rglob('*'): + if file_path.is_file(): + total_size += file_path.stat().st_size + file_count += 1 + + # 限制最大500MB + max_size = 500 * 1024 * 1024 + if total_size > max_size: + raise HTTPException( + status_code=413, + detail=f"文件夹过大 ({formatFileSize(total_size)}),最大支持 {formatFileSize(max_size)}" + ) + + # 限制文件数量 + max_files = 10000 + if file_count > max_files: + raise HTTPException( + status_code=413, + detail=f"文件数量过多 ({file_count}),最大支持 {max_files} 个文件" + ) + + # 创建ZIP文件名 + folder_name = target_path.name + zip_filename = f"{folder_name}.zip" + + # 创建ZIP文件 + zip_buffer = io.BytesIO() + + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as zipf: + # 添加文件夹中的所有文件 + for file_path in target_path.rglob('*'): + if file_path.is_file(): + try: + # 计算相对路径,保持文件夹结构 + arcname = file_path.relative_to(target_path) + zipf.write(file_path, arcname) + except (OSError, IOError) as e: + # 跳过无法读取的文件,但记录警告 + print(f"Warning: Skipping file {file_path}: {e}") + continue + + zip_buffer.seek(0) + + # 设置响应头 + headers = { + 'Content-Disposition': f'attachment; filename="{zip_filename}"', + 'Content-Type': 'application/zip', + 'Content-Length': str(len(zip_buffer.getvalue())), + 'Cache-Control': 'no-cache' + } + + # 创建流式响应 + from fastapi.responses import StreamingResponse + + async def generate_zip(): + zip_buffer.seek(0) + yield zip_buffer.getvalue() + + return StreamingResponse( + generate_zip(), + media_type="application/zip", + headers=headers + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"压缩文件夹失败: {str(e)}") + + +@router.post("/download-multiple-zip") +async def download_multiple_items_as_zip(request: Dict[str, Any]): + """ + 将多个文件和文件夹压缩为ZIP并下载 + + Args: + request: 包含paths和filename字段的JSON对象 + """ + try: + paths = request.get("paths", []) + filename = request.get("filename", "batch_download.zip") + + if not paths: + raise HTTPException(status_code=400, detail="请选择要下载的文件") + + # 验证所有路径 + valid_paths = [] + total_size = 0 + file_count = 0 + + for path in paths: + target_path = PROJECTS_DIR / path + + if not target_path.exists(): + continue # 跳过不存在的文件 + + if target_path.is_file(): + total_size += target_path.stat().st_size + file_count += 1 + valid_paths.append(path) + elif target_path.is_dir(): + # 计算文件夹大小 + for file_path in target_path.rglob('*'): + if file_path.is_file(): + total_size += file_path.stat().st_size + file_count += 1 + valid_paths.append(path) + + if not valid_paths: + raise HTTPException(status_code=404, detail="没有找到有效的文件") + + # 限制大小 + max_size = 500 * 1024 * 1024 + if total_size > max_size: + raise HTTPException( + status_code=413, + detail=f"选中文件过大 ({formatFileSize(total_size)}),最大支持 {formatFileSize(max_size)}" + ) + + # 限制文件数量 + max_files = 10000 + if file_count > max_files: + raise HTTPException( + status_code=413, + detail=f"文件数量过多 ({file_count}),最大支持 {max_files} 个文件" + ) + + # 创建ZIP文件 + zip_buffer = io.BytesIO() + + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as zipf: + for path in valid_paths: + target_path = PROJECTS_DIR / path + + if target_path.is_file(): + # 单个文件 + try: + zipf.write(target_path, target_path.name) + except (OSError, IOError) as e: + print(f"Warning: Skipping file {target_path}: {e}") + continue + elif target_path.is_dir(): + # 文件夹 + for file_path in target_path.rglob('*'): + if file_path.is_file(): + try: + # 保持相对路径结构 + arcname = f"{target_path.name}/{file_path.relative_to(target_path)}" + zipf.write(file_path, arcname) + except (OSError, IOError) as e: + print(f"Warning: Skipping file {file_path}: {e}") + continue + + zip_buffer.seek(0) + + # 设置响应头 + headers = { + 'Content-Disposition': f'attachment; filename="{filename}"', + 'Content-Type': 'application/zip', + 'Content-Length': str(len(zip_buffer.getvalue())), + 'Cache-Control': 'no-cache' + } + + # 创建流式响应 + from fastapi.responses import StreamingResponse + + async def generate_zip(): + zip_buffer.seek(0) + yield zip_buffer.getvalue() + + return StreamingResponse( + generate_zip(), + media_type="application/zip", + headers=headers + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"批量压缩失败: {str(e)}") + + +def formatFileSize(bytes_size: int) -> str: + """格式化文件大小""" + if bytes_size == 0: + return "0 B" + + k = 1024 + sizes = ["B", "KB", "MB", "GB", "TB"] + i = int(math.floor(math.log(bytes_size, k))) + + if i >= len(sizes): + i = len(sizes) - 1 + + return f"{bytes_size / math.pow(k, i):.1f} {sizes[i]}" + + +@router.post("/batch-operation") +async def batch_operation(operations: List[Dict[str, Any]]): + """ + 批量操作多个文件 + + Args: + operations: 操作列表,每个操作包含type和相应的参数 + """ + try: + results = [] + + for op in operations: + op_type = op.get("type") + op_result = {"type": op_type, "success": False} + + try: + if op_type == "delete": + await delete_item(op["path"]) + op_result["success"] = True + op_result["message"] = "删除成功" + elif op_type == "move": + await move_item(op["source_path"], op["target_path"]) + op_result["success"] = True + op_result["message"] = "移动成功" + elif op_type == "copy": + await copy_item(op["source_path"], op["target_path"]) + op_result["success"] = True + op_result["message"] = "复制成功" + else: + op_result["error"] = f"不支持的操作类型: {op_type}" + + except Exception as e: + op_result["error"] = str(e) + + results.append(op_result) + + return { + "success": True, + "results": results, + "total": len(operations), + "successful": sum(1 for r in results if r["success"]) + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"批量操作失败: {str(e)}") \ No newline at end of file diff --git a/public/file-manager.html b/public/file-manager.html new file mode 100644 index 0000000..39d6439 --- /dev/null +++ b/public/file-manager.html @@ -0,0 +1,990 @@ + + + + + + 文件管理器 + + + +
+
+

📁 项目文件管理器

+

管理您的项目文件,支持上传、下载、删除等操作

+
+ +
+ + + + + + + +
+ + + +
+ +
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..7964a7f --- /dev/null +++ b/public/index.html @@ -0,0 +1,1138 @@ + + + + + + AI聊天助手 + + + + + + + + +
+
+
AI聊天助手
+
+ + 已连接 +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
AI
+
+ 您好!我是AI助手,有什么可以帮助您的吗? +
+
+
+ +
+
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a49d26f..9597f2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ dependencies = [ "openpyxl>=3.0.0", "xlrd>=2.0.0", "chardet>=5.0.0", - "wsgidav>=4.0.0", ] diff --git a/requirements.txt b/requirements.txt index 88a07c3..0516799 100644 --- a/requirements.txt +++ b/requirements.txt @@ -95,7 +95,6 @@ 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 diff --git a/webdav_production.py b/webdav_production.py deleted file mode 100644 index 8dd44fe..0000000 --- a/webdav_production.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -""" -生产环境WebDAV配置,支持域名部署 -""" - -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 DomainDomainController: - """支持域名的域控制器""" - - def __init__(self, wsgidav_app, config): - self.wsgidav_app = wsgidav_app - self.config = config - - def get_domain_realm(self, path_info, environ): - return "WebDAV" - - 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_production_webdav_app(domain=None, ssl=False): - """ - 创建生产环境WebDAV应用 - - Args: - domain: 域名,如 'yourdomain.com' - ssl: 是否使用HTTPS - """ - # 确保projects目录存在 - projects_dir = Path("projects") - projects_dir.mkdir(exist_ok=True) - - # 根据域名配置host - if domain: - host = "0.0.0.0" # 生产环境监听所有接口 - scheme = "https" if ssl else "http" - server_url = f"{scheme}://{domain}" - else: - host = "localhost" - server_url = "http://localhost" - - # 配置WebDAV - config = { - "host": host, - "port": 8001, - "provider_mapping": { - "/": FilesystemProvider(str(projects_dir)) - }, - "http_authenticator": { - "domain_controller": DomainDomainController, - "accept_basic": True, - "accept_digest": True, - "default_to_digest": False, - }, - "verbose": 2 if domain else 1, # 生产环境更详细日志 - "dir_browser": { - "enable": True, - "response_trailer": True, - "davmount_path": "/webdav", - "ms_mount_path": "/webdav", - }, - # 添加CORS支持 - "middleware": [ - { - "class": "wsgidav.middleware.cors.CorsMiddleware", - "options": { - "cors_origin": "*", - "cors_methods": "GET, POST, PUT, DELETE, OPTIONS, PROPFIND, MKCOL, COPY, MOVE", - "cors_headers": "Authorization, Content-Type, Depth, Destination, Overwrite", - } - } - ] - } - - # 创建应用 - app = WsgiDAVApp(config) - - print(f"WebDAV Server configured for domain: {domain or 'localhost'}") - print(f"Server URL: {server_url}:8001/webdav") - - return app - - -if __name__ == "__main__": - # 可以通过环境变量配置域名 - domain = os.environ.get("WEBDAV_DOMAIN") - ssl_enabled = os.environ.get("WEBDAV_SSL", "false").lower() == "true" - - app = create_production_webdav_app(domain, ssl_enabled) - - from wsgidav.server.run_server import run_server - run_server(app) \ No newline at end of file diff --git a/webdav_server.py b/webdav_server.py deleted file mode 100644 index 30e31be..0000000 --- a/webdav_server.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/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 SimpleDomainController: - """简单域控制器,支持固定用户名密码认证""" - - def __init__(self, wsgidav_app, config): - # 简单的用户名密码配置 - self.users = { - "admin": "admin123", # 用户名: admin, 密码: admin123 - "webdav": "webdav123", # 用户名: webdav, 密码: webdav123 - } - - def get_domain_realm(self, path_info, environ): - return "WebDAV" - - 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): - # 如果用户名为空,允许通过(匿名访问) - if not user_name: - return True - # 检查用户名密码 - return self.users.get(user_name) == password - - 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.SimpleDomainController", - "accept_basic": True, - "accept_digest": True, - "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() \ No newline at end of file