commit 4985344911c1773243c210cbc65984a5a7c455f1 Author: 朱潮 Date: Sun Aug 10 12:57:17 2025 +0800 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c9449f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,64 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +venv +.venv +pip-log.txt +pip-delete-this-directory.txt +.tox +.coverage +.coverage.* +.pytest_cache +nosetests.xml +coverage.xml +*.cover +*.log +.cache +.mypy_cache +.pytest_cache +.hypothesis + +# Docker +Dockerfile* +docker-compose*.yml +.dockerignore + +# 开发文件 +.env +.env.* +*.env +.vscode +.idea +*.swp +*.swo +*~ + +# 文档 +README.md +*.md +docs/ + +# 数据目录 +data/ +uploads/ +logs/ +*.db +*.sqlite + +# 临时文件 +*.tmp +*.temp +.DS_Store +Thumbs.db + +# 脚本 +start.sh +*.sh \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..4181036 --- /dev/null +++ b/.env @@ -0,0 +1,31 @@ +# 文件传输服务环境变量配置 + +# 服务器配置 +HOST=0.0.0.0 +PORT=8000 + +# 文件上传配置 +MAX_FILE_SIZE=104857600 # 100MB (字节) +EXPIRE_MINUTES=15 # 文件过期时间(分钟) + +# 客户端配置 +FILESHARE_SERVER=http://localhost:8000 # 客户端使用的服务器地址 + +# SSL/TLS配置(生产环境) +ACME_EMAIL=admin@yourdomain.com # Let's Encrypt 邮箱 + +# 可选:数据库配置(如果使用持久化存储) +# DATABASE_URL=sqlite:///./fileshare.db + +# 可选:Redis配置(如果使用Redis缓存) +# REDIS_URL=redis://localhost:6379/0 + +# 可选:安全配置 +# SECRET_KEY=your-secret-key-here +# ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com + +# 日志配置 +LOG_LEVEL=INFO + +# 开发模式 +DEBUG=false \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4181036 --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# 文件传输服务环境变量配置 + +# 服务器配置 +HOST=0.0.0.0 +PORT=8000 + +# 文件上传配置 +MAX_FILE_SIZE=104857600 # 100MB (字节) +EXPIRE_MINUTES=15 # 文件过期时间(分钟) + +# 客户端配置 +FILESHARE_SERVER=http://localhost:8000 # 客户端使用的服务器地址 + +# SSL/TLS配置(生产环境) +ACME_EMAIL=admin@yourdomain.com # Let's Encrypt 邮箱 + +# 可选:数据库配置(如果使用持久化存储) +# DATABASE_URL=sqlite:///./fileshare.db + +# 可选:Redis配置(如果使用Redis缓存) +# REDIS_URL=redis://localhost:6379/0 + +# 可选:安全配置 +# SECRET_KEY=your-secret-key-here +# ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com + +# 日志配置 +LOG_LEVEL=INFO + +# 开发模式 +DEBUG=false \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b790af1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# 文件传输服务 Docker 镜像 +FROM python:3.11-slim + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV HOST=0.0.0.0 +ENV PORT=8000 + +# 配置阿里云镜像源 +RUN sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources \ + && sed -i 's|http://security.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources + +# 安装系统依赖 +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 复制requirements.txt并安装Python依赖 +COPY requirements.txt . +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ \ + && pip config set global.trusted-host mirrors.aliyun.com \ + && pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +# 复制应用程序代码 +COPY . . + +# 创建上传目录并设置权限 +RUN mkdir -p uploads \ + && chmod 755 uploads + +# 创建非root用户 +RUN groupadd -r appuser && useradd -r -g appuser appuser \ + && chown -R appuser:appuser /app + +# 切换到非root用户 +USER appuser + +# 暴露端口 +EXPOSE 8000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/ || exit 1 + +# 启动命令 +CMD ["python", "app.py"] \ No newline at end of file diff --git a/Dockerfile.multi b/Dockerfile.multi new file mode 100644 index 0000000..b20da8d --- /dev/null +++ b/Dockerfile.multi @@ -0,0 +1,66 @@ +# 文件传输服务 Docker 镜像 (多阶段构建优化版本) +FROM python:3.11-slim as builder + +# 配置阿里云pip镜像源 +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ \ + && pip config set global.trusted-host mirrors.aliyun.com + +# 安装构建依赖 +COPY requirements.txt /tmp/ +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir --user -r /tmp/requirements.txt + +# 生产镜像 +FROM python:3.11-slim + +# 设置环境变量 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV HOST=0.0.0.0 +ENV PORT=8000 +ENV PATH=/root/.local/bin:$PATH + +# 配置阿里云apt镜像源 +RUN sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources \ + && sed -i 's|http://security.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources + +# 安装运行时依赖 +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# 从构建阶段复制Python包 +COPY --from=builder /root/.local /root/.local + +# 设置工作目录 +WORKDIR /app + +# 复制应用程序代码 +COPY . . + +# 创建上传目录并设置权限 +RUN mkdir -p uploads \ + && chmod 755 uploads + +# 创建非root用户 +RUN groupadd -r appuser && useradd -r -g appuser appuser \ + && chown -R appuser:appuser /app + +# 切换到非root用户 +USER appuser + +# 暴露端口 +EXPOSE 8000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/ || exit 1 + +# 使用tini作为init进程 +ENTRYPOINT ["/usr/bin/tini", "--"] + +# 启动命令 +CMD ["python", "app.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e192b45 --- /dev/null +++ b/README.md @@ -0,0 +1,369 @@ +# 文件传输服务 + +一个简单易用的文件和文本临时分享服务,支持生成分享口令,文件自动过期清理。 + +## 功能特性 + +- 🚀 **文件上传分享** - 支持任意类型文件上传,生成分享口令 +- 📝 **文本分享** - 支持纯文本内容分享 +- 🔐 **口令访问** - 通过8位分享码下载文件 +- ⏰ **自动过期** - 文件15分钟后自动过期删除 +- 🛡️ **安全可靠** - 支持文件大小限制,自动清理 +- 🌐 **RESTful API** - 完整的API接口 +- 💻 **命令行工具** - 便捷的CLI工具 +- 🐳 **Docker支持** - 开箱即用的容器化部署 + +## 快速开始 + +### 方式1: 一键启动 (推荐) + +```bash +cd fileshare +./start.sh +``` + +### 方式2: Docker部署 + +**国内用户 (使用阿里云加速):** +```bash +cd fileshare +cp .env.example .env + +# 构建并启动 (使用阿里云镜像加速) +./build.sh --aliyun +docker-compose -f docker-compose.aliyun.yml up -d +``` + +**海外用户:** +```bash +cd fileshare +cp .env.example .env + +# 基础部署 +docker-compose up -d + +# 包含Traefik反向代理 (生产环境推荐) +docker-compose --profile traefik up -d + +# 包含Redis缓存 +docker-compose --profile redis up -d +``` + +**访问服务:** +- 🌐 Web界面: http://localhost:8000 +- 📖 API文档: http://localhost:8000/docs +- ℹ️ API信息: http://localhost:8000/api +- 如果启用了Traefik: http://traefik.localhost:8080 + +### 方式3: 本地运行 + +1. 安装Python依赖: +```bash +pip install -r requirements.txt +``` + +2. 启动服务: +```bash +python app.py +``` + +## 使用方法 + +### 🌐 Web界面 + +访问 http://localhost:8000 使用友好的Web界面: + +1. **📁 上传文件** - 拖拽或点击选择文件上传 +2. **📝 分享文本** - 在线编辑文本内容分享 +3. **⬇️ 下载文件** - 输入8位分享码下载 +4. **💻 curl命令** - 无需安装工具的命令行使用教程 + +### 💻 curl命令行(推荐) + +**无需安装任何额外工具**,使用系统自带的curl命令: + +```bash +# 上传文件 +curl -X POST -F "file=@文件路径" http://localhost:8000/api/upload + +# 分享文本(超级简单) +curl -X POST --data "你的文本" http://localhost:8000/api/text + +# 下载文件 +curl -O -J http://localhost:8000/api/download/分享码 + +# 查看文件信息 +curl http://localhost:8000/api/info/分享码 +``` + +📖 **完整教程**: 访问 http://localhost:8000/curl + +### 🚀 Shell便捷函数(更简单) + +**一次设置,永久便捷**: + +```bash +# 加载便捷函数 +source fileshare_functions.sh + +# 现在可以使用超级简单的命令 +upload photo.jpg # 上传文件 +share_text "Hello World!" # 分享文本 +download AB12CD34 # 下载文件 +info AB12CD34 # 查看信息 +list_shares # 列出分享 +``` + +**便捷功能**: +- ✅ 自动提取分享码 +- ✅ 彩色输出和错误提示 +- ✅ 支持批量操作 +- ✅ 自动服务器检测 + +### Web API + +#### 上传文件 +```bash +curl -X POST "http://localhost:8000/api/upload" \ + -F "file=@example.txt" +``` + +#### 分享文本 +```bash +curl -X POST "http://localhost:8000/api/share-text" \ + -H "Content-Type: application/json" \ + -d '{"content": "Hello World!", "filename": "hello.txt"}' +``` + +#### 下载文件 +```bash +curl -O "http://localhost:8000/api/download/ABCD1234" +``` + +#### 获取文件信息 +```bash +curl "http://localhost:8000/api/info/ABCD1234" +``` + +### 🐍 Python命令行工具(可选) + +如果需要更丰富的功能,可以使用Python CLI工具: + +```bash +# 安装依赖 +pip install -r requirements.txt + +# 上传文件 +python cli.py upload example.txt + +# 分享文本 +python cli.py share-text -t "Hello World!" + +# 下载文件 +python cli.py download ABCD1234 + +# 查看信息 +python cli.py info ABCD1234 +``` + +**注意**: 对于简单使用,推荐使用curl命令,无需安装Python依赖。 + +### 环境变量配置 + +可以通过环境变量或`.env`文件配置: + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `HOST` | `0.0.0.0` | 服务器监听地址 | +| `PORT` | `8000` | 服务器端口 | +| `MAX_FILE_SIZE` | `104857600` | 最大文件大小(字节) | +| `EXPIRE_MINUTES` | `15` | 文件过期时间(分钟) | +| `FILESHARE_SERVER` | `http://localhost:8000` | 客户端服务器地址 | + +## API接口 + +### 完整API文档 + +启动服务后访问 http://localhost:8000/docs 查看自动生成的API文档。 + +### 主要接口 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/` | GET | 服务信息 | +| `/api/upload` | POST | 上传文件 | +| `/api/share-text` | POST | 分享文本 | +| `/api/download/{code}` | GET | 下载文件 | +| `/api/info/{code}` | GET | 获取分享信息 | +| `/api/shares` | GET | 列出所有分享 | +| `/api/shares/{code}` | DELETE | 删除分享 | +| `/api/cleanup` | POST | 手动清理过期文件 | + +## 部署建议 + +### 生产环境 + +1. 使用Traefik反向代理: +```bash +docker-compose --profile traefik up -d +``` + +2. 配置SSL证书: + - 修改`.env`中的`ACME_EMAIL` + - 确保域名正确指向服务器 + +3. 数据持久化: + - 上传文件会保存在`./data/uploads` + - 可以备份该目录 + +4. 监控和日志: + - 日志输出到`./data/logs` + - 支持健康检查 + +### 安全建议 + +1. 设置合适的文件大小限制 +2. 定期清理过期文件 +3. 使用HTTPS(生产环境) +4. 限制访问IP(如需要) +5. 设置反向代理限流 + +## 开发 + +### 项目结构 +``` +fileshare/ +├── app.py # 主应用程序 +├── cli.py # 命令行工具 +├── requirements.txt # Python依赖 +├── Dockerfile # Docker镜像 (标准版) +├── Dockerfile.multi # Docker镜像 (多阶段构建优化版) +├── docker-compose.yml # Docker编排 (标准版) +├── docker-compose.aliyun.yml # Docker编排 (阿里云优化版) +├── .dockerignore # Docker构建忽略文件 +├── .env.example # 环境变量示例 +├── start.sh # 一键启动脚本 +├── build.sh # Docker构建脚本 +├── README.md # 项目说明 +├── static/ # 前端文件 +│ ├── index.html # 主页面 +│ ├── style.css # 样式表 +│ ├── app.js # 前端逻辑 +│ └── curl-guide.html # curl命令教程页面 +├── uploads/ # 上传文件目录 +├── test_server.py # 服务测试脚本 +├── fix_paths.py # 路径修复工具 +├── verify_setup.sh # 环境验证脚本 +├── curl_examples.sh # curl使用示例 +├── fileshare_functions.sh # Shell便捷函数 +└── demo.sh # 功能演示脚本 +``` + +### 开发模式 + +1. 安装开发依赖: +```bash +pip install -r requirements.txt +``` + +2. 启动开发服务器: +```bash +python app.py +# 或使用uvicorn +uvicorn app:app --reload --host 0.0.0.0 --port 8000 +``` + +3. 测试CLI工具: +```bash +python cli.py --help +``` + +### 国内加速部署 + +**使用阿里云镜像加速构建:** +```bash +# 构建优化镜像 +./build.sh --aliyun + +# 使用阿里云compose文件启动 +docker-compose -f docker-compose.aliyun.yml up -d + +# 或指定镜像仓库 +./build.sh --aliyun --push --registry registry.cn-hangzhou.aliyuncs.com/yourname +``` + +**加速特性:** +- apt使用阿里云Debian镜像源 +- pip使用阿里云PyPI镜像源 +- 容器镜像使用阿里云容器镜像服务 +- 多阶段构建减少镜像体积 + +## 故障排除 + +### 🔧 快速诊断 + +遇到问题时,首先运行验证脚本: +```bash +./verify_setup.sh +``` + +### 常见问题 + +1. **CSS/JS文件404错误** + ```bash + python fix_paths.py # 修复静态资源路径 + ``` + +2. **文件上传失败** + - 检查文件大小是否超过限制 + - 确认磁盘空间充足 + +3. **下载失败** + - 确认分享码正确 + - 检查文件是否已过期 + +4. **服务无法启动** + - 检查端口是否被占用 + - 确认Python版本>=3.8 + - 运行: `pip install -r requirements.txt` + +5. **Docker问题** + - 确认Docker和docker-compose已安装 + - 检查端口映射配置 + +### 🧪 测试工具 + +```bash +# 服务验证 +./verify_setup.sh + +# 功能演示(推荐) +./demo.sh + +# 运行时测试 +python test_server.py + +# 查看示例 +./curl_examples.sh + +# 路径修复 +python fix_paths.py +``` + +### 日志查看 + +```bash +# Docker日志 +docker-compose logs -f + +# 单个服务日志 +docker-compose logs fileshare +``` + +## 许可证 + +MIT License + +## 贡献 + +欢迎提交Issue和Pull Request! \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..580adbc --- /dev/null +++ b/app.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +""" +文件传输服务 - 主应用程序 +支持文件和文本分享,生成分享口令,15分钟过期 +""" +import os +import uuid +import hashlib +import mimetypes +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, Any + +import uvicorn +from fastapi import FastAPI, UploadFile, File, HTTPException, Request, Form +from fastapi.responses import FileResponse, HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +import asyncio +from contextlib import asynccontextmanager + +# 配置 +UPLOAD_DIR = Path("uploads") +MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB +EXPIRE_MINUTES = 15 +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 8000)) + +# 确保目录存在 +UPLOAD_DIR.mkdir(exist_ok=True) +STATIC_DIR = Path("static") +STATIC_DIR.mkdir(exist_ok=True) + +# 内存存储分享记录 +shares: Dict[str, Dict[str, Any]] = {} + +class TextShare(BaseModel): + content: str + filename: str = "shared_text.txt" + +class ShareResponse(BaseModel): + code: str + expires_at: str + download_url: str + +class ShareInfo(BaseModel): + code: str + filename: str + file_type: str + size: int + created_at: str + expires_at: str + is_expired: bool + +def generate_share_code() -> str: + """生成分享口令""" + return hashlib.md5(str(uuid.uuid4()).encode()).hexdigest()[:8].upper() + +def is_expired(expires_at: datetime) -> bool: + """检查是否过期""" + return datetime.now() > expires_at + +def cleanup_expired_files(): + """清理过期文件""" + expired_codes = [] + + for code, share_info in shares.items(): + if is_expired(share_info["expires_at"]): + expired_codes.append(code) + # 删除文件 + if share_info["file_path"] and Path(share_info["file_path"]).exists(): + try: + os.remove(share_info["file_path"]) + print(f"删除过期文件: {share_info['file_path']}") + except Exception as e: + print(f"删除文件失败 {share_info['file_path']}: {e}") + + # 从内存中删除过期记录 + for code in expired_codes: + del shares[code] + print(f"删除过期分享记录: {code}") + +async def cleanup_task(): + """定期清理任务""" + while True: + try: + cleanup_expired_files() + await asyncio.sleep(60) # 每分钟检查一次 + except Exception as e: + print(f"清理任务错误: {e}") + await asyncio.sleep(60) + +@asynccontextmanager +async def lifespan(_: FastAPI): + # 启动时创建清理任务 + cleanup_task_handle = asyncio.create_task(cleanup_task()) + try: + yield + finally: + # 关闭时取消清理任务 + cleanup_task_handle.cancel() + +# 创建FastAPI应用 +app = FastAPI( + title="文件传输服务", + description="支持文件和文本分享的临时传输服务", + version="1.0.0", + lifespan=lifespan +) + +# 添加CORS支持 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 挂载静态文件目录 +app.mount("/static", StaticFiles(directory="static"), name="static") + +@app.get("/", response_class=HTMLResponse) +async def root(): + """根路径 - 返回前端页面""" + try: + with open("static/index.html", "r", encoding="utf-8") as f: + return HTMLResponse(content=f.read()) + except FileNotFoundError: + return { + "service": "文件传输服务", + "version": "1.0.0", + "message": "前端页面不存在,请检查static/index.html文件", + "features": [ + "文件上传分享", + "文本分享", + "口令下载", + "15分钟过期" + ], + "endpoints": { + "upload": "POST /api/upload - 上传文件", + "share_text": "POST /api/share-text - 分享文本", + "download": "GET /api/download/{code} - 下载文件", + "info": "GET /api/info/{code} - 获取分享信息", + "list": "GET /api/shares - 列出所有分享" + } + } + +@app.get("/api") +async def api_info(): + """API信息""" + return { + "service": "文件传输服务 API", + "version": "1.0.0", + "features": [ + "文件上传分享", + "文本分享", + "口令下载", + "15分钟过期" + ], + "endpoints": { + "upload": "POST /api/upload - 上传文件", + "share_text": "POST /api/share-text - 分享文本(JSON)", + "share_text_simple": "POST /api/text - 分享文本(简化版) ⭐", + "share_text_form": "POST /api/share-text-form - 分享文本(表单)", + "download": "GET /api/download/{code} - 下载文件", + "info": "GET /api/info/{code} - 获取分享信息", + "list": "GET /api/shares - 列出所有分享" + } + } + +@app.get("/curl") +async def curl_guide(): + """curl命令使用教程页面""" + try: + with open("static/curl-guide.html", "r", encoding="utf-8") as f: + return HTMLResponse(content=f.read()) + except FileNotFoundError: + return HTMLResponse(content="

curl教程页面不存在

请检查static/curl-guide.html文件

", status_code=404) + +@app.post("/api/upload", response_model=ShareResponse) +async def upload_file(file: UploadFile = File(...)): + """上传文件并生成分享口令""" + + # 检查文件大小 + if file.size and file.size > MAX_FILE_SIZE: + raise HTTPException(status_code=413, detail=f"文件太大,最大支持 {MAX_FILE_SIZE // 1024 // 1024}MB") + + # 生成分享码和文件路径 + share_code = generate_share_code() + file_extension = Path(file.filename or "").suffix + safe_filename = f"{share_code}{file_extension}" + file_path = UPLOAD_DIR / safe_filename + + try: + # 保存文件 + content = await file.read() + + # 再次检查文件大小 + if len(content) > MAX_FILE_SIZE: + raise HTTPException(status_code=413, detail=f"文件太大,最大支持 {MAX_FILE_SIZE // 1024 // 1024}MB") + + with open(file_path, "wb") as f: + f.write(content) + + # 保存分享信息 + now = datetime.now() + expires_at = now + timedelta(minutes=EXPIRE_MINUTES) + + shares[share_code] = { + "filename": file.filename or "unknown", + "file_path": str(file_path), + "file_type": file.content_type or "application/octet-stream", + "size": len(content), + "created_at": now, + "expires_at": expires_at, + "is_text": False + } + + return ShareResponse( + code=share_code, + expires_at=expires_at.isoformat(), + download_url=f"/api/download/{share_code}" + ) + + except Exception as e: + # 清理可能创建的文件 + if file_path.exists(): + file_path.unlink() + raise HTTPException(status_code=500, detail=f"上传失败: {str(e)}") + +@app.post("/api/share-text", response_model=ShareResponse) +async def share_text(text_share: TextShare): + """分享文本内容(JSON格式)""" + return await _create_text_share(text_share.content, text_share.filename) + +@app.post("/api/text", response_model=ShareResponse) +async def share_text_simple(request: Request): + """分享文本内容(简化版 - 直接发送文本)""" + try: + # 读取请求体作为纯文本 + content = (await request.body()).decode('utf-8') + + if not content.strip(): + raise HTTPException(status_code=400, detail="文本内容不能为空") + + return await _create_text_share(content, "shared_text.txt") + + except UnicodeDecodeError: + raise HTTPException(status_code=400, detail="文本编码错误,请使用UTF-8编码") + except Exception as e: + raise HTTPException(status_code=500, detail=f"分享失败: {str(e)}") + +@app.post("/api/share-text-form", response_model=ShareResponse) +async def share_text_form(content: str = Form(...), filename: str = Form("shared_text.txt")): + """分享文本内容(表单格式)""" + if not content or not content.strip(): + raise HTTPException(status_code=400, detail="文本内容不能为空") + + return await _create_text_share(content, filename) + +async def _create_text_share(content: str, filename: str) -> ShareResponse: + """创建文本分享的通用函数""" + if not content.strip(): + raise HTTPException(status_code=400, detail="文本内容不能为空") + + # 生成分享码 + share_code = generate_share_code() + file_path = UPLOAD_DIR / f"{share_code}.txt" + + try: + # 保存文本到文件 + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + + # 保存分享信息 + now = datetime.now() + expires_at = now + timedelta(minutes=EXPIRE_MINUTES) + + shares[share_code] = { + "filename": filename, + "file_path": str(file_path), + "file_type": "text/plain", + "size": len(content.encode("utf-8")), + "created_at": now, + "expires_at": expires_at, + "is_text": True, + "content": content + } + + return ShareResponse( + code=share_code, + expires_at=expires_at.isoformat(), + download_url=f"/api/download/{share_code}" + ) + + except Exception as e: + # 清理可能创建的文件 + if file_path.exists(): + file_path.unlink() + raise HTTPException(status_code=500, detail=f"分享失败: {str(e)}") + +@app.get("/api/download/{code}") +async def download_file(code: str): + """通过分享码下载文件""" + + share_info = shares.get(code) + if not share_info: + raise HTTPException(status_code=404, detail="分享码不存在") + + if is_expired(share_info["expires_at"]): + # 清理过期文件 + cleanup_expired_files() + raise HTTPException(status_code=410, detail="分享已过期") + + file_path = Path(share_info["file_path"]) + if not file_path.exists(): + raise HTTPException(status_code=404, detail="文件不存在") + + # 获取MIME类型 + mime_type, _ = mimetypes.guess_type(str(file_path)) + if not mime_type: + mime_type = share_info["file_type"] + + return FileResponse( + path=file_path, + filename=share_info["filename"], + media_type=mime_type + ) + +@app.get("/api/info/{code}", response_model=ShareInfo) +async def get_share_info(code: str): + """获取分享信息""" + + share_info = shares.get(code) + if not share_info: + raise HTTPException(status_code=404, detail="分享码不存在") + + return ShareInfo( + code=code, + filename=share_info["filename"], + file_type=share_info["file_type"], + size=share_info["size"], + created_at=share_info["created_at"].isoformat(), + expires_at=share_info["expires_at"].isoformat(), + is_expired=is_expired(share_info["expires_at"]) + ) + +@app.get("/api/shares") +async def list_shares(): + """列出所有分享(管理用)""" + result = [] + current_time = datetime.now() + + for code, share_info in shares.items(): + result.append({ + "code": code, + "filename": share_info["filename"], + "file_type": share_info["file_type"], + "size": share_info["size"], + "created_at": share_info["created_at"].isoformat(), + "expires_at": share_info["expires_at"].isoformat(), + "is_expired": is_expired(share_info["expires_at"]), + "remaining_minutes": max(0, int((share_info["expires_at"] - current_time).total_seconds() / 60)) + }) + + return { + "total": len(result), + "shares": result + } + +@app.delete("/api/shares/{code}") +async def delete_share(code: str): + """删除分享""" + share_info = shares.get(code) + if not share_info: + raise HTTPException(status_code=404, detail="分享码不存在") + + # 删除文件 + file_path = Path(share_info["file_path"]) + if file_path.exists(): + try: + file_path.unlink() + except Exception as e: + print(f"删除文件失败: {e}") + + # 删除记录 + del shares[code] + + return {"message": "删除成功", "code": code} + +@app.post("/api/cleanup") +async def manual_cleanup(): + """手动清理过期文件""" + before_count = len(shares) + cleanup_expired_files() + after_count = len(shares) + + return { + "message": "清理完成", + "removed": before_count - after_count, + "remaining": after_count + } + +if __name__ == "__main__": + print(f"启动文件传输服务...") + print(f"地址: http://{HOST}:{PORT}") + print(f"上传目录: {UPLOAD_DIR.absolute()}") + print(f"最大文件大小: {MAX_FILE_SIZE // 1024 // 1024}MB") + print(f"过期时间: {EXPIRE_MINUTES}分钟") + + uvicorn.run( + "app:app", + host=HOST, + port=PORT, + reload=True, + log_level="info" + ) \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..c096d8a --- /dev/null +++ b/build.sh @@ -0,0 +1,149 @@ +#!/bin/bash +# Docker镜像构建脚本 (支持阿里云加速) + +set -e + +echo "=== 文件传输服务 Docker 构建脚本 ===" +echo + +# 默认配置 +IMAGE_NAME="fileshare" +TAG="latest" +DOCKERFILE="Dockerfile" +USE_ALIYUN="false" +PUSH_TO_REGISTRY="false" +REGISTRY="" + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + --aliyun) + USE_ALIYUN="true" + DOCKERFILE="Dockerfile.multi" + echo "✅ 启用阿里云镜像加速" + shift + ;; + --name) + IMAGE_NAME="$2" + shift 2 + ;; + --tag) + TAG="$2" + shift 2 + ;; + --push) + PUSH_TO_REGISTRY="true" + shift + ;; + --registry) + REGISTRY="$2" + shift 2 + ;; + -h|--help) + echo "使用方法: $0 [选项]" + echo + echo "选项:" + echo " --aliyun 使用阿里云镜像加速" + echo " --name IMAGE 指定镜像名称 (默认: fileshare)" + echo " --tag TAG 指定镜像标签 (默认: latest)" + echo " --push 构建后推送到镜像仓库" + echo " --registry URL 指定镜像仓库地址" + echo " -h, --help 显示帮助信息" + echo + echo "示例:" + echo " $0 # 基础构建" + echo " $0 --aliyun # 使用阿里云加速" + echo " $0 --name myapp --tag v1.0.0" + echo " $0 --aliyun --push --registry registry.cn-hangzhou.aliyuncs.com/myname" + exit 0 + ;; + *) + echo "❌ 未知选项: $1" + echo "使用 $0 --help 查看帮助" + exit 1 + ;; + esac +done + +# 完整镜像名 +FULL_IMAGE_NAME="${REGISTRY:+$REGISTRY/}${IMAGE_NAME}:${TAG}" + +echo "📋 构建配置:" +echo " - 镜像名称: $FULL_IMAGE_NAME" +echo " - Dockerfile: $DOCKERFILE" +echo " - 阿里云加速: $USE_ALIYUN" +echo " - 推送镜像: $PUSH_TO_REGISTRY" +echo + +# 检查Dockerfile是否存在 +if [ ! -f "$DOCKERFILE" ]; then + echo "❌ Dockerfile不存在: $DOCKERFILE" + exit 1 +fi + +# 检查Docker是否运行 +if ! docker info >/dev/null 2>&1; then + echo "❌ Docker未运行或无权限访问" + exit 1 +fi + +# 构建镜像 +echo "🔨 开始构建镜像..." +echo + +if [ "$USE_ALIYUN" = "true" ]; then + # 使用阿里云加速构建 + docker build \ + --file "$DOCKERFILE" \ + --tag "$FULL_IMAGE_NAME" \ + --build-arg PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ \ + --build-arg PIP_TRUSTED_HOST=mirrors.aliyun.com \ + --progress=plain \ + . +else + # 标准构建 + docker build \ + --file "$DOCKERFILE" \ + --tag "$FULL_IMAGE_NAME" \ + --progress=plain \ + . +fi + +if [ $? -eq 0 ]; then + echo + echo "✅ 镜像构建成功: $FULL_IMAGE_NAME" + + # 显示镜像信息 + echo + echo "📊 镜像信息:" + docker images "$FULL_IMAGE_NAME" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" + + # 推送镜像 + if [ "$PUSH_TO_REGISTRY" = "true" ]; then + echo + echo "📤 推送镜像到仓库..." + docker push "$FULL_IMAGE_NAME" + + if [ $? -eq 0 ]; then + echo "✅ 镜像推送成功" + else + echo "❌ 镜像推送失败" + exit 1 + fi + fi + + echo + echo "🚀 运行命令:" + echo " docker run -d -p 8000:8000 --name fileshare $FULL_IMAGE_NAME" + echo + echo "🐳 或使用docker-compose:" + if [ "$USE_ALIYUN" = "true" ]; then + echo " docker-compose -f docker-compose.aliyun.yml up -d" + else + echo " docker-compose up -d" + fi + +else + echo "❌ 镜像构建失败" + exit 1 +fi \ No newline at end of file diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..0cb8474 --- /dev/null +++ b/cli.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python3 +""" +文件传输服务 - 命令行工具 +支持文件上传、文本分享和下载功能 +""" +import os +import sys +import json +from pathlib import Path +from typing import Optional + +import click +import httpx +from rich.console import Console +from rich.table import Table +from rich.progress import Progress, SpinnerColumn, TextColumn +from rich.panel import Panel +from rich.prompt import Prompt + +console = Console() + +# 默认服务器配置 +DEFAULT_SERVER = "http://localhost:8000" +SERVER_URL = os.getenv("FILESHARE_SERVER", DEFAULT_SERVER) + +class FileShareClient: + def __init__(self, server_url: str = SERVER_URL): + self.server_url = server_url.rstrip("/") + self.client = httpx.Client(timeout=60.0) + + def upload_file(self, file_path: Path) -> dict: + """上传文件""" + if not file_path.exists(): + raise FileNotFoundError(f"文件不存在: {file_path}") + + with open(file_path, "rb") as f: + files = {"file": (file_path.name, f, "application/octet-stream")} + response = self.client.post(f"{self.server_url}/api/upload", files=files) + + if response.status_code == 200: + return response.json() + else: + raise Exception(f"上传失败: {response.status_code} - {response.text}") + + def share_text(self, content: str, filename: str = "shared_text.txt") -> dict: + """分享文本""" + data = {"content": content, "filename": filename} + response = self.client.post(f"{self.server_url}/api/share-text", json=data) + + if response.status_code == 200: + return response.json() + else: + raise Exception(f"分享失败: {response.status_code} - {response.text}") + + def download_file(self, code: str, output_dir: Path = Path(".")) -> Path: + """下载文件""" + # 先获取文件信息 + info_response = self.client.get(f"{self.server_url}/api/info/{code}") + if info_response.status_code != 200: + raise Exception(f"获取文件信息失败: {info_response.status_code}") + + file_info = info_response.json() + filename = file_info["filename"] + + # 下载文件 + response = self.client.get(f"{self.server_url}/api/download/{code}") + if response.status_code != 200: + raise Exception(f"下载失败: {response.status_code} - {response.text}") + + # 保存文件 + output_path = output_dir / filename + + # 如果文件存在,添加序号 + counter = 1 + original_path = output_path + while output_path.exists(): + stem = original_path.stem + suffix = original_path.suffix + output_path = output_dir / f"{stem}_{counter}{suffix}" + counter += 1 + + with open(output_path, "wb") as f: + f.write(response.content) + + return output_path + + def get_info(self, code: str) -> dict: + """获取分享信息""" + response = self.client.get(f"{self.server_url}/api/info/{code}") + if response.status_code == 200: + return response.json() + else: + raise Exception(f"获取信息失败: {response.status_code} - {response.text}") + + def list_shares(self) -> dict: + """列出所有分享""" + response = self.client.get(f"{self.server_url}/api/shares") + if response.status_code == 200: + return response.json() + else: + raise Exception(f"获取列表失败: {response.status_code} - {response.text}") + +@click.group() +@click.option("--server", default=SERVER_URL, help="服务器地址") +@click.pass_context +def cli(ctx, server): + """文件传输服务命令行工具""" + ctx.ensure_object(dict) + ctx.obj['client'] = FileShareClient(server) + ctx.obj['server'] = server + +@cli.command() +@click.argument("file_path", type=click.Path(exists=True, path_type=Path)) +@click.pass_context +def upload(ctx, file_path: Path): + """上传文件并获取分享码""" + client = ctx.obj['client'] + + try: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) as progress: + task = progress.add_task(f"上传文件 {file_path.name}...", total=None) + result = client.upload_file(file_path) + + # 显示结果 + panel = Panel.fit( + f"[green]✓ 文件上传成功![/green]\n\n" + f"[bold]分享码:[/bold] [yellow]{result['code']}[/yellow]\n" + f"[bold]过期时间:[/bold] {result['expires_at']}\n" + f"[bold]下载链接:[/bold] {ctx.obj['server']}{result['download_url']}", + title="上传成功", + border_style="green" + ) + console.print(panel) + console.print(f"\n[dim]使用命令下载: [bold]python cli.py download {result['code']}[/bold][/dim]") + + except Exception as e: + console.print(f"[red]❌ 上传失败: {e}[/red]") + sys.exit(1) + +@cli.command() +@click.option("--text", "-t", help="要分享的文本内容") +@click.option("--file", "-f", "text_file", type=click.Path(exists=True, path_type=Path), help="要分享的文本文件") +@click.option("--filename", default="shared_text.txt", help="分享文件名") +@click.pass_context +def share_text(ctx, text: Optional[str], text_file: Optional[Path], filename: str): + """分享文本内容""" + client = ctx.obj['client'] + + # 确定文本内容 + if text_file: + try: + with open(text_file, "r", encoding="utf-8") as f: + content = f.read() + if not filename.endswith(".txt") and text_file.suffix: + filename = f"shared_{text_file.name}" + except Exception as e: + console.print(f"[red]❌ 读取文件失败: {e}[/red]") + sys.exit(1) + elif text: + content = text + else: + # 交互式输入 + console.print("[blue]请输入要分享的文本内容(按Ctrl+D或Ctrl+Z结束):[/blue]") + lines = [] + try: + while True: + line = input() + lines.append(line) + except EOFError: + content = "\n".join(lines) + + if not content.strip(): + console.print("[red]❌ 文本内容不能为空[/red]") + sys.exit(1) + + try: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) as progress: + task = progress.add_task("分享文本...", total=None) + result = client.share_text(content, filename) + + # 显示结果 + panel = Panel.fit( + f"[green]✓ 文本分享成功![/green]\n\n" + f"[bold]分享码:[/bold] [yellow]{result['code']}[/yellow]\n" + f"[bold]文件名:[/bold] {filename}\n" + f"[bold]过期时间:[/bold] {result['expires_at']}\n" + f"[bold]下载链接:[/bold] {ctx.obj['server']}{result['download_url']}", + title="分享成功", + border_style="green" + ) + console.print(panel) + console.print(f"\n[dim]使用命令下载: [bold]python cli.py download {result['code']}[/bold][/dim]") + + except Exception as e: + console.print(f"[red]❌ 分享失败: {e}[/red]") + sys.exit(1) + +@cli.command() +@click.argument("code") +@click.option("--output", "-o", type=click.Path(path_type=Path), default=".", help="输出目录") +@click.pass_context +def download(ctx, code: str, output: Path): + """通过分享码下载文件""" + client = ctx.obj['client'] + + try: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) as progress: + task = progress.add_task(f"下载分享码 {code}...", total=None) + output_path = client.download_file(code, output) + + # 显示结果 + file_size = output_path.stat().st_size + panel = Panel.fit( + f"[green]✓ 文件下载成功![/green]\n\n" + f"[bold]文件路径:[/bold] {output_path.absolute()}\n" + f"[bold]文件大小:[/bold] {file_size:,} 字节", + title="下载成功", + border_style="green" + ) + console.print(panel) + + except Exception as e: + console.print(f"[red]❌ 下载失败: {e}[/red]") + sys.exit(1) + +@cli.command() +@click.argument("code") +@click.pass_context +def info(ctx, code: str): + """获取分享信息""" + client = ctx.obj['client'] + + try: + result = client.get_info(code) + + # 计算文件大小显示 + size = result['size'] + if size < 1024: + size_str = f"{size} B" + elif size < 1024 * 1024: + size_str = f"{size / 1024:.1f} KB" + else: + size_str = f"{size / (1024 * 1024):.1f} MB" + + # 显示信息 + status = "[red]已过期[/red]" if result['is_expired'] else "[green]有效[/green]" + + panel = Panel.fit( + f"[bold]分享码:[/bold] [yellow]{result['code']}[/yellow]\n" + f"[bold]文件名:[/bold] {result['filename']}\n" + f"[bold]文件类型:[/bold] {result['file_type']}\n" + f"[bold]文件大小:[/bold] {size_str}\n" + f"[bold]创建时间:[/bold] {result['created_at']}\n" + f"[bold]过期时间:[/bold] {result['expires_at']}\n" + f"[bold]状态:[/bold] {status}", + title="分享信息", + border_style="blue" + ) + console.print(panel) + + except Exception as e: + console.print(f"[red]❌ 获取信息失败: {e}[/red]") + sys.exit(1) + +@cli.command() +@click.pass_context +def list(ctx): + """列出所有分享""" + client = ctx.obj['client'] + + try: + result = client.list_shares() + + if result['total'] == 0: + console.print("[yellow]没有找到任何分享[/yellow]") + return + + # 创建表格 + table = Table(title=f"所有分享 (共 {result['total']} 个)") + table.add_column("分享码", style="yellow", no_wrap=True) + table.add_column("文件名", style="blue") + table.add_column("类型", style="green") + table.add_column("大小", justify="right") + table.add_column("剩余时间", justify="center") + table.add_column("状态", justify="center") + + for share in result['shares']: + # 计算文件大小显示 + size = share['size'] + if size < 1024: + size_str = f"{size}B" + elif size < 1024 * 1024: + size_str = f"{size / 1024:.1f}K" + else: + size_str = f"{size / (1024 * 1024):.1f}M" + + # 剩余时间 + remaining = share.get('remaining_minutes', 0) + if remaining > 0: + remaining_str = f"{remaining}分钟" + else: + remaining_str = "已过期" + + # 状态 + status = "[red]过期[/red]" if share['is_expired'] else "[green]有效[/green]" + + table.add_row( + share['code'], + share['filename'][:30] + ("..." if len(share['filename']) > 30 else ""), + share['file_type'].split('/')[-1] if '/' in share['file_type'] else share['file_type'], + size_str, + remaining_str, + status + ) + + console.print(table) + console.print(f"\n[dim]使用 'python cli.py info <分享码>' 查看详细信息[/dim]") + + except Exception as e: + console.print(f"[red]❌ 获取列表失败: {e}[/red]") + sys.exit(1) + +@cli.command() +@click.pass_context +def server_info(ctx): + """获取服务器信息""" + client = ctx.obj['client'] + + try: + response = client.client.get(f"{client.server_url}/") + if response.status_code == 200: + info = response.json() + + panel = Panel.fit( + f"[bold]服务名称:[/bold] {info['service']}\n" + f"[bold]版本:[/bold] {info['version']}\n" + f"[bold]服务器地址:[/bold] {client.server_url}\n\n" + f"[bold]功能特性:[/bold]\n" + + "\n".join([f" • {feature}" for feature in info['features']]), + title="服务器信息", + border_style="blue" + ) + console.print(panel) + else: + console.print(f"[red]❌ 无法连接到服务器: {response.status_code}[/red]") + + except Exception as e: + console.print(f"[red]❌ 连接服务器失败: {e}[/red]") + console.print(f"[dim]请确保服务器正在运行: {client.server_url}[/dim]") + sys.exit(1) + +if __name__ == "__main__": + cli() \ No newline at end of file diff --git a/curl_examples.sh b/curl_examples.sh new file mode 100755 index 0000000..418e92e --- /dev/null +++ b/curl_examples.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# curl命令示例脚本 - 展示文件传输服务的基本用法 + +SERVER_URL="http://localhost:8000" + +echo "📡 文件传输服务 - curl命令示例" +echo "=================================" +echo + +# 检查服务器是否运行 +echo "🔍 检查服务器状态..." +if curl -s "$SERVER_URL/api" > /dev/null; then + echo "✅ 服务器运行正常" +else + echo "❌ 无法连接到服务器" + echo "💡 请先启动服务: python app.py" + exit 1 +fi + +echo +echo "🌐 Web界面: $SERVER_URL" +echo "📖 curl教程: $SERVER_URL/curl" +echo + +# 显示使用示例 +echo "📝 使用示例:" +echo + +echo "1. 📤 上传文件:" +echo " curl -X POST -F \"file=@文件路径\" $SERVER_URL/api/upload" +echo +echo " 示例:" +echo " curl -X POST -F \"file=@photo.jpg\" $SERVER_URL/api/upload" +echo + +echo "2. 📝 分享文本(超级简单):" +echo " curl -X POST --data \"你的文本\" $SERVER_URL/api/text" +echo +echo " 示例:" +echo " curl -X POST --data \"Hello World!\" $SERVER_URL/api/text" +echo +echo " 🔧 指定文件名:" +echo " curl -X POST -F \"content=Hello World!\" -F \"filename=hello.txt\" $SERVER_URL/api/share-text-form" +echo + +echo "3. ⬇️ 下载文件:" +echo " curl -O -J $SERVER_URL/api/download/分享码" +echo +echo " 示例:" +echo " curl -O -J $SERVER_URL/api/download/AB12CD34" +echo + +echo "4. ℹ️ 查看文件信息:" +echo " curl $SERVER_URL/api/info/分享码" +echo +echo " 示例:" +echo " curl $SERVER_URL/api/info/AB12CD34" +echo + +echo "5. 📊 列出所有分享:" +echo " curl $SERVER_URL/api/shares" +echo + +echo "=================================" +echo "💡 提示:" +echo " - 无需安装任何额外工具" +echo " - 分享码为8位大写字母+数字" +echo " - 文件15分钟后自动过期" +echo " - 最大支持100MB文件" +echo +echo "📖 查看完整教程: $SERVER_URL/curl" \ No newline at end of file diff --git a/data/uploads/65313572.txt b/data/uploads/65313572.txt new file mode 100644 index 0000000..9e2377e --- /dev/null +++ b/data/uploads/65313572.txt @@ -0,0 +1 @@ +Hello World” \ No newline at end of file diff --git a/demo.sh b/demo.sh new file mode 100755 index 0000000..54b3293 --- /dev/null +++ b/demo.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# 文件传输服务演示脚本 + +SERVER_URL="http://localhost:8000" + +echo "🎉 文件传输服务演示" +echo "====================" +echo + +# 检查服务器 +echo "🔍 检查服务器状态..." +if ! curl -s "$SERVER_URL/api" > /dev/null; then + echo "❌ 服务器未启动,请先运行: python app.py" + exit 1 +fi +echo "✅ 服务器运行正常" +echo + +# 演示1: 分享文本(超级简单方式) +echo "📝 演示1: 分享文本(超级简单)" +echo "命令: curl -X POST --data \"Hello from demo!\" $SERVER_URL/api/text" +echo +result1=$(curl -s -X POST --data "Hello from demo!" "$SERVER_URL/api/text") +if echo "$result1" | grep -q '"code"'; then + code1=$(echo "$result1" | grep -o '"code":"[^"]*"' | cut -d'"' -f4) + echo "✅ 分享成功! 分享码: $code1" + echo "🔗 下载链接: $SERVER_URL/api/download/$code1" +else + echo "❌ 分享失败" +fi +echo + +# 演示2: 创建临时文件并上传 +echo "📤 演示2: 上传文件" +temp_file=$(mktemp) +echo "这是一个演示文件 +创建时间: $(date) +文件内容测试" > "$temp_file" + +echo "命令: curl -X POST -F \"file=@$temp_file\" $SERVER_URL/api/upload" +echo +result2=$(curl -s -X POST -F "file=@$temp_file" "$SERVER_URL/api/upload") +if echo "$result2" | grep -q '"code"'; then + code2=$(echo "$result2" | grep -o '"code":"[^"]*"' | cut -d'"' -f4) + echo "✅ 上传成功! 分享码: $code2" + echo "🔗 下载链接: $SERVER_URL/api/download/$code2" +else + echo "❌ 上传失败" +fi +rm -f "$temp_file" +echo + +# 演示3: 表单方式分享文本 +echo "📋 演示3: 表单方式分享文本(可指定文件名)" +echo "命令: curl -X POST -F \"content=#!/bin/bash +echo 'Hello Shell!'\" -F \"filename=demo.sh\" $SERVER_URL/api/share-text-form" +echo +result3=$(curl -s -X POST -F "content=#!/bin/bash +echo 'Hello Shell!'" -F "filename=demo.sh" "$SERVER_URL/api/share-text-form") +if echo "$result3" | grep -q '"code"'; then + code3=$(echo "$result3" | grep -o '"code":"[^"]*"' | cut -d'"' -f4) + echo "✅ 分享成功! 分享码: $code3" + echo "🔗 下载链接: $SERVER_URL/api/download/$code3" +else + echo "❌ 分享失败" +fi +echo + +# 演示4: 查看所有分享 +echo "📊 演示4: 查看所有分享" +echo "命令: curl $SERVER_URL/api/shares" +echo +shares_result=$(curl -s "$SERVER_URL/api/shares") +if echo "$shares_result" | grep -q '"total"'; then + total=$(echo "$shares_result" | grep -o '"total":[0-9]*' | cut -d':' -f2) + echo "✅ 当前共有 $total 个分享" + echo + echo "$shares_result" | python3 -m json.tool 2>/dev/null || echo "$shares_result" +else + echo "❌ 获取分享列表失败" +fi +echo + +# 演示便捷函数 +echo "🚀 演示5: 便捷函数使用" +echo "加载函数: source fileshare_functions.sh" +echo "然后就可以使用超级简单的命令:" +echo +echo " upload photo.jpg # 上传文件" +echo " share_text \"Hello World!\" # 分享文本" +echo " download AB12CD34 # 下载文件" +echo " info AB12CD34 # 查看信息" +echo " list_shares # 列出分享" +echo + +echo "====================" +echo "🎉 演示完成!" +echo +echo "📖 查看完整教程: $SERVER_URL/curl" +echo "🌐 Web界面: $SERVER_URL" +echo "🧪 运行测试: ./verify_setup.sh" +echo "📝 查看示例: ./curl_examples.sh" \ No newline at end of file diff --git a/docker-compose.aliyun.yml b/docker-compose.aliyun.yml new file mode 100644 index 0000000..5237a53 --- /dev/null +++ b/docker-compose.aliyun.yml @@ -0,0 +1,132 @@ +version: '3.8' + +services: + fileshare: + build: + context: . + dockerfile: Dockerfile.multi # 使用多阶段构建优化镜像 + args: + - PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ + - PIP_TRUSTED_HOST=mirrors.aliyun.com + image: fileshare:latest + container_name: fileshare-service + restart: unless-stopped + ports: + - "${PORT:-8000}:8000" + environment: + - HOST=0.0.0.0 + - PORT=8000 + - MAX_FILE_SIZE=${MAX_FILE_SIZE:-104857600} # 100MB + - EXPIRE_MINUTES=${EXPIRE_MINUTES:-15} + volumes: + # 持久化上传目录(可选,重启后文件会保留) + - ./data/uploads:/app/uploads + # 日志目录(可选) + - ./data/logs:/app/logs + networks: + - fileshare-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.5' + memory: 256M + + # Traefik 反向代理 (使用阿里云镜像) + traefik: + image: registry.cn-hangzhou.aliyuncs.com/acs/traefik:v3.0 + container_name: fileshare-traefik + restart: unless-stopped + profiles: + - traefik + command: + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" + - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:-admin@localhost}" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + - "--log.level=INFO" + - "--accesslog=true" + ports: + - "80:80" + - "443:443" + - "8080:8080" # Traefik dashboard + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./data/letsencrypt:/letsencrypt + - ./data/logs/traefik:/var/log/traefik + networks: + - fileshare-network + labels: + - "traefik.enable=true" + - "traefik.http.routers.dashboard.rule=Host(`traefik.localhost`)" + - "traefik.http.routers.dashboard.service=api@internal" + - "traefik.http.middlewares.dashboard-auth.basicauth.users=${TRAEFIK_AUTH:-admin:$$2y$$10$$WQiE8P/7W8MZ0GKJYLgVAOUV8D5e6Y7s8rF8w1M9i6QjLqN/3rZ0G}" + + # Redis缓存 (使用阿里云镜像) + redis: + image: registry.cn-hangzhou.aliyuncs.com/acs/redis:7-alpine + container_name: fileshare-redis + restart: unless-stopped + profiles: + - redis + ports: + - "6379:6379" + volumes: + - ./data/redis:/data + - ./config/redis.conf:/usr/local/etc/redis/redis.conf:ro + networks: + - fileshare-network + command: redis-server /usr/local/etc/redis/redis.conf + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.2' + memory: 128M + + # Nginx (可选,用于静态文件服务) + nginx: + image: registry.cn-hangzhou.aliyuncs.com/acs/nginx:alpine + container_name: fileshare-nginx + restart: unless-stopped + profiles: + - nginx + ports: + - "80:80" + volumes: + - ./config/nginx.conf:/etc/nginx/nginx.conf:ro + - ./data/uploads:/usr/share/nginx/html/uploads:ro + - ./data/logs/nginx:/var/log/nginx + networks: + - fileshare-network + depends_on: + - fileshare + +networks: + fileshare-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 + +volumes: + uploads: + driver: local + logs: + driver: local + redis_data: + driver: local \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..210df72 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,88 @@ +version: '3.8' + +services: + fileshare: + build: + context: . + dockerfile: Dockerfile # 或使用 Dockerfile.multi 获得更小镜像 + container_name: fileshare-service + restart: unless-stopped + ports: + - "${PORT:-8000}:8000" + environment: + - HOST=0.0.0.0 + - PORT=8000 + - MAX_FILE_SIZE=${MAX_FILE_SIZE:-104857600} # 100MB + - EXPIRE_MINUTES=${EXPIRE_MINUTES:-15} + volumes: + # 持久化上传目录(可选,重启后文件会保留) + - ./data/uploads:/app/uploads + # 日志目录(可选) + - ./data/logs:/app/logs + networks: + - fileshare-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - "traefik.enable=true" + - "traefik.http.routers.fileshare.rule=Host(`fileshare.localhost`)" + - "traefik.http.services.fileshare.loadbalancer.server.port=8000" + + # 可选:使用Traefik作为反向代理(生产环境推荐) + traefik: + image: traefik:v3.0 + container_name: fileshare-traefik + restart: unless-stopped + profiles: + - traefik # 使用profile控制是否启动 + command: + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" + - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:-admin@localhost}" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + ports: + - "80:80" + - "443:443" + - "8080:8080" # Traefik dashboard + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./data/letsencrypt:/letsencrypt + networks: + - fileshare-network + labels: + - "traefik.enable=true" + - "traefik.http.routers.dashboard.rule=Host(`traefik.localhost`)" + - "traefik.http.routers.dashboard.service=api@internal" + + # 可选:Redis缓存(用于集群部署时共享会话) + redis: + image: redis:7-alpine + container_name: fileshare-redis + restart: unless-stopped + profiles: + - redis # 使用profile控制是否启动 + ports: + - "6379:6379" + volumes: + - ./data/redis:/data + networks: + - fileshare-network + command: redis-server --appendonly yes + +networks: + fileshare-network: + driver: bridge + +volumes: + uploads: + driver: local + logs: + driver: local \ No newline at end of file diff --git a/fileshare_functions.sh b/fileshare_functions.sh new file mode 100755 index 0000000..577a03b --- /dev/null +++ b/fileshare_functions.sh @@ -0,0 +1,250 @@ +#!/bin/bash +# 文件传输服务便捷函数 +# 使用方法: source fileshare_functions.sh + +# 默认服务器地址 +FILESHARE_SERVER="${FILESHARE_SERVER:-http://localhost:8000}" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 显示帮助信息 +fileshare_help() { + echo -e "${BLUE}📡 文件传输服务便捷函数${NC}" + echo "==================================" + echo + echo -e "${YELLOW}📤 上传文件:${NC}" + echo " upload <文件路径>" + echo " 示例: upload photo.jpg" + echo + echo -e "${YELLOW}📝 分享文本:${NC}" + echo " share_text <文本内容>" + echo " 示例: share_text \"Hello World!\"" + echo + echo -e "${YELLOW}⬇️ 下载文件:${NC}" + echo " download <分享码>" + echo " 示例: download AB12CD34" + echo + echo -e "${YELLOW}ℹ️ 查看文件信息:${NC}" + echo " info <分享码>" + echo " 示例: info AB12CD34" + echo + echo -e "${YELLOW}📊 列出所有分享:${NC}" + echo " list_shares" + echo + echo -e "${YELLOW}🔧 设置服务器地址:${NC}" + echo " set_server <地址>" + echo " 示例: set_server https://your-domain.com" + echo + echo -e "${BLUE}当前服务器: ${FILESHARE_SERVER}${NC}" +} + +# 上传文件 +upload() { + if [ $# -eq 0 ]; then + echo -e "${RED}❌ 请指定要上传的文件${NC}" + echo "用法: upload <文件路径>" + return 1 + fi + + local file="$1" + if [ ! -f "$file" ]; then + echo -e "${RED}❌ 文件不存在: $file${NC}" + return 1 + fi + + echo -e "${BLUE}📤 上传文件: $file${NC}" + local result=$(curl -s -X POST -F "file=@$file" "$FILESHARE_SERVER/api/upload") + + if echo "$result" | grep -q '"code"'; then + local code=$(echo "$result" | grep -o '"code":"[^"]*"' | cut -d'"' -f4) + echo -e "${GREEN}✅ 上传成功!${NC}" + echo -e "${YELLOW}分享码: $code${NC}" + echo -e "${BLUE}下载命令: download $code${NC}" + else + echo -e "${RED}❌ 上传失败${NC}" + echo "$result" + return 1 + fi +} + +# 分享文本 +share_text() { + if [ $# -eq 0 ]; then + echo -e "${RED}❌ 请提供要分享的文本内容${NC}" + echo "用法: share_text \"文本内容\"" + return 1 + fi + + local content="$*" + echo -e "${BLUE}📝 分享文本...${NC}" + local result=$(curl -s -X POST --data "$content" "$FILESHARE_SERVER/api/text") + + if echo "$result" | grep -q '"code"'; then + local code=$(echo "$result" | grep -o '"code":"[^"]*"' | cut -d'"' -f4) + echo -e "${GREEN}✅ 分享成功!${NC}" + echo -e "${YELLOW}分享码: $code${NC}" + echo -e "${BLUE}下载命令: download $code${NC}" + else + echo -e "${RED}❌ 分享失败${NC}" + echo "$result" + return 1 + fi +} + +# 下载文件 +download() { + if [ $# -eq 0 ]; then + echo -e "${RED}❌ 请提供分享码${NC}" + echo "用法: download <分享码>" + return 1 + fi + + local code="$1" + echo -e "${BLUE}⬇️ 下载文件: $code${NC}" + + # 先检查文件信息 + local info_result=$(curl -s "$FILESHARE_SERVER/api/info/$code") + if echo "$info_result" | grep -q '"filename"'; then + local filename=$(echo "$info_result" | grep -o '"filename":"[^"]*"' | cut -d'"' -f4) + echo -e "${BLUE}文件名: $filename${NC}" + + # 下载文件 + if curl -f -O -J "$FILESHARE_SERVER/api/download/$code"; then + echo -e "${GREEN}✅ 下载成功!${NC}" + else + echo -e "${RED}❌ 下载失败${NC}" + return 1 + fi + else + echo -e "${RED}❌ 分享码不存在或已过期${NC}" + return 1 + fi +} + +# 查看文件信息 +info() { + if [ $# -eq 0 ]; then + echo -e "${RED}❌ 请提供分享码${NC}" + echo "用法: info <分享码>" + return 1 + fi + + local code="$1" + echo -e "${BLUE}ℹ️ 查看文件信息: $code${NC}" + local result=$(curl -s "$FILESHARE_SERVER/api/info/$code") + + if echo "$result" | grep -q '"filename"'; then + echo -e "${GREEN}✅ 文件信息:${NC}" + echo "$result" | python3 -m json.tool 2>/dev/null || echo "$result" + else + echo -e "${RED}❌ 分享码不存在或已过期${NC}" + return 1 + fi +} + +# 列出所有分享 +list_shares() { + echo -e "${BLUE}📊 获取分享列表...${NC}" + local result=$(curl -s "$FILESHARE_SERVER/api/shares") + + if echo "$result" | grep -q '"total"'; then + echo -e "${GREEN}✅ 分享列表:${NC}" + echo "$result" | python3 -m json.tool 2>/dev/null || echo "$result" + else + echo -e "${RED}❌ 获取列表失败${NC}" + echo "$result" + return 1 + fi +} + +# 设置服务器地址 +set_server() { + if [ $# -eq 0 ]; then + echo -e "${YELLOW}当前服务器: $FILESHARE_SERVER${NC}" + echo "用法: set_server <服务器地址>" + return 0 + fi + + FILESHARE_SERVER="$1" + echo -e "${GREEN}✅ 服务器地址已设置为: $FILESHARE_SERVER${NC}" + + # 测试连接 + if curl -s "$FILESHARE_SERVER/api" > /dev/null; then + echo -e "${GREEN}✅ 服务器连接正常${NC}" + else + echo -e "${YELLOW}⚠️ 无法连接到服务器,请检查地址是否正确${NC}" + fi +} + +# 从文件分享文本 +share_file() { + if [ $# -eq 0 ]; then + echo -e "${RED}❌ 请指定要分享的文本文件${NC}" + echo "用法: share_file <文件路径>" + return 1 + fi + + local file="$1" + if [ ! -f "$file" ]; then + echo -e "${RED}❌ 文件不存在: $file${NC}" + return 1 + fi + + echo -e "${BLUE}📝 分享文件内容: $file${NC}" + local result=$(curl -s -X POST --data "@$file" "$FILESHARE_SERVER/api/text") + + if echo "$result" | grep -q '"code"'; then + local code=$(echo "$result" | grep -o '"code":"[^"]*"' | cut -d'"' -f4) + echo -e "${GREEN}✅ 分享成功!${NC}" + echo -e "${YELLOW}分享码: $code${NC}" + echo -e "${BLUE}下载命令: download $code${NC}" + else + echo -e "${RED}❌ 分享失败${NC}" + echo "$result" + return 1 + fi +} + +# 批量上传 +batch_upload() { + if [ $# -eq 0 ]; then + echo -e "${RED}❌ 请指定文件或目录${NC}" + echo "用法: batch_upload <文件1> <文件2> ... 或 batch_upload <目录>/*" + return 1 + fi + + echo -e "${BLUE}📤 批量上传文件...${NC}" + local success_count=0 + local total_count=0 + + for file in "$@"; do + if [ -f "$file" ]; then + total_count=$((total_count + 1)) + echo -e "\n${BLUE}上传: $(basename "$file")${NC}" + if upload "$file"; then + success_count=$((success_count + 1)) + fi + fi + done + + echo -e "\n${GREEN}✅ 批量上传完成: $success_count/$total_count 成功${NC}" +} + +# 自动显示帮助 +echo -e "${GREEN}✅ 文件传输服务便捷函数已加载${NC}" +echo -e "${BLUE}输入 fileshare_help 查看使用帮助${NC}" +echo -e "${YELLOW}服务器地址: $FILESHARE_SERVER${NC}" + +# 创建别名 +alias fs_help='fileshare_help' +alias fs_upload='upload' +alias fs_share='share_text' +alias fs_download='download' +alias fs_info='info' +alias fs_list='list_shares' +alias fs_server='set_server' \ No newline at end of file diff --git a/fix_paths.py b/fix_paths.py new file mode 100755 index 0000000..6b81b2f --- /dev/null +++ b/fix_paths.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +路径修复脚本 - 确保所有文件路径正确 +""" +import os +import re +from pathlib import Path + +def fix_html_paths(): + """修复HTML中的静态资源路径""" + html_file = Path("static/index.html") + + if not html_file.exists(): + print(f"❌ 文件不存在: {html_file}") + return False + + print("🔧 修复HTML中的静态资源路径...") + + with open(html_file, 'r', encoding='utf-8') as f: + content = f.read() + + # 修复CSS路径 + content = re.sub( + r' /dev/null; then + echo "❌ Docker 未安装,请先安装Docker" + echo "📖 安装指南: https://docs.docker.com/get-docker/" + exit 1 +fi + +# 检查docker-compose是否安装 +if ! command -v docker-compose &> /dev/null; then + echo "❌ docker-compose 未安装,请先安装docker-compose" + echo "📖 安装指南: https://docs.docker.com/compose/install/" + exit 1 +fi + +# 检查.env文件 +if [ ! -f .env ]; then + echo "📝 创建环境配置文件..." + cp .env.example .env + echo "✅ 已创建 .env 文件,请根据需要修改配置" +fi + +# 创建数据目录 +echo "📁 创建数据目录..." +mkdir -p data/uploads data/logs data/redis data/letsencrypt + +# 选择启动方式 +echo "请选择启动方式:" +echo "1) 基础模式 (仅文件传输服务)" +echo "2) 完整模式 (包含Traefik反向代理)" +echo "3) 集群模式 (包含Redis缓存)" +read -p "请输入选择 (1-3): " choice + +case $choice in + 1) + echo "🚀 启动基础模式..." + docker-compose up -d fileshare + ;; + 2) + echo "🚀 启动完整模式 (包含Traefik)..." + docker-compose --profile traefik up -d + ;; + 3) + echo "🚀 启动集群模式 (包含Redis)..." + docker-compose --profile redis up -d + ;; + *) + echo "❌ 无效选择,使用基础模式启动..." + docker-compose up -d fileshare + ;; +esac + +echo +echo "⏳ 等待服务启动..." +sleep 5 + +# 检查服务状态 +if docker-compose ps | grep -q "Up"; then + echo "✅ 服务启动成功!" + echo + echo "🌐 访问地址:" + echo " - API服务: http://localhost:8000" + echo " - API文档: http://localhost:8000/docs" + + if [ "$choice" = "2" ]; then + echo " - Traefik面板: http://traefik.localhost:8080" + fi + + echo + echo "🔧 常用命令:" + echo " - 查看日志: docker-compose logs -f" + echo " - 停止服务: docker-compose down" + echo " - 重启服务: docker-compose restart" + echo + echo "💻 CLI工具使用:" + echo " - 安装依赖: pip install -r requirements.txt" + echo " - 上传文件: python cli.py upload <文件路径>" + echo " - 分享文本: python cli.py share-text -t '内容'" + echo " - 下载文件: python cli.py download <分享码>" +else + echo "❌ 服务启动失败,请检查日志:" + docker-compose logs +fi \ No newline at end of file diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..004b543 --- /dev/null +++ b/static/app.js @@ -0,0 +1,547 @@ +// 文件传输服务 - 前端JavaScript + +class FileShareApp { + constructor() { + this.baseURL = window.location.origin; + this.init(); + } + + init() { + this.setupEventListeners(); + this.setupTabSwitching(); + this.setupDragAndDrop(); + } + + // 设置事件监听器 + setupEventListeners() { + // 文件选择 + const fileInput = document.getElementById('fileInput'); + const selectBtn = document.querySelector('.select-btn'); + + selectBtn?.addEventListener('click', () => fileInput?.click()); + fileInput?.addEventListener('change', (e) => this.handleFileSelect(e.target.files[0])); + + // 文本分享 + const shareTextBtn = document.getElementById('shareTextBtn'); + shareTextBtn?.addEventListener('click', () => this.handleTextShare()); + + // 下载文件 + const downloadBtn = document.getElementById('downloadBtn'); + const downloadCode = document.getElementById('downloadCode'); + + downloadBtn?.addEventListener('click', () => this.handleDownloadInfo()); + downloadCode?.addEventListener('keypress', (e) => { + if (e.key === 'Enter') this.handleDownloadInfo(); + }); + + // 重置按钮 + window.resetUpload = () => this.resetUploadSection(); + window.resetText = () => this.resetTextSection(); + + // 复制功能 + window.copyCode = (elementId) => this.copyToClipboard(elementId); + window.copyText = (text) => this.copyTextToClipboard(text); + } + + // 设置选项卡切换 + setupTabSwitching() { + const tabBtns = document.querySelectorAll('.tab-btn'); + const tabContents = document.querySelectorAll('.tab-content'); + + tabBtns.forEach(btn => { + btn.addEventListener('click', () => { + const targetTab = btn.getAttribute('data-tab'); + + // 更新按钮状态 + tabBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + // 更新内容显示 + tabContents.forEach(content => { + content.classList.remove('active'); + }); + + const targetContent = document.getElementById(`${targetTab}-tab`); + if (targetContent) { + targetContent.classList.add('active'); + } + }); + }); + } + + // 设置拖拽上传 + setupDragAndDrop() { + const dropZone = document.getElementById('dropZone'); + + if (!dropZone) return; + + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropZone.addEventListener(eventName, this.preventDefaults, false); + }); + + ['dragenter', 'dragover'].forEach(eventName => { + dropZone.addEventListener(eventName, () => dropZone.classList.add('dragover'), false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropZone.addEventListener(eventName, () => dropZone.classList.remove('dragover'), false); + }); + + dropZone.addEventListener('drop', (e) => { + const files = e.dataTransfer.files; + if (files.length > 0) { + this.handleFileSelect(files[0]); + } + }, false); + } + + preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + // 处理文件选择 + async handleFileSelect(file) { + if (!file) return; + + // 检查文件大小 + const maxSize = 100 * 1024 * 1024; // 100MB + if (file.size > maxSize) { + this.showToast('文件太大,最大支持100MB', 'error'); + return; + } + + try { + // 显示进度界面 + this.showProgress(); + + // 创建FormData + const formData = new FormData(); + formData.append('file', file); + + // 上传文件 + const response = await fetch(`${this.baseURL}/api/upload`, { + method: 'POST', + body: formData, + onUploadProgress: (progressEvent) => { + const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); + this.updateProgress(percentCompleted); + } + }); + + if (!response.ok) { + throw new Error(`上传失败: ${response.statusText}`); + } + + const result = await response.json(); + this.showUploadResult(result); + this.showToast('文件上传成功!', 'success'); + + } catch (error) { + console.error('Upload error:', error); + this.showToast(error.message || '上传失败', 'error'); + this.hideProgress(); + } + } + + // 处理文本分享 + async handleTextShare() { + const content = document.getElementById('textContent').value.trim(); + const filename = document.getElementById('textFilename').value.trim() || 'shared_text.txt'; + + if (!content) { + this.showToast('请输入要分享的文本内容', 'warning'); + return; + } + + try { + const shareBtn = document.getElementById('shareTextBtn'); + shareBtn.classList.add('loading'); + shareBtn.disabled = true; + + const response = await fetch(`${this.baseURL}/api/share-text`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: content, + filename: filename + }) + }); + + if (!response.ok) { + throw new Error(`分享失败: ${response.statusText}`); + } + + const result = await response.json(); + this.showTextResult(result); + this.showToast('文本分享成功!', 'success'); + + } catch (error) { + console.error('Text share error:', error); + this.showToast(error.message || '分享失败', 'error'); + } finally { + const shareBtn = document.getElementById('shareTextBtn'); + shareBtn.classList.remove('loading'); + shareBtn.disabled = false; + } + } + + // 处理下载信息获取 + async handleDownloadInfo() { + const code = document.getElementById('downloadCode').value.trim().toUpperCase(); + + if (!code) { + this.showToast('请输入分享码', 'warning'); + return; + } + + if (code.length !== 8) { + this.showToast('分享码格式错误,应为8位字符', 'warning'); + return; + } + + try { + const downloadBtn = document.getElementById('downloadBtn'); + downloadBtn.classList.add('loading'); + downloadBtn.disabled = true; + + const response = await fetch(`${this.baseURL}/api/info/${code}`); + + if (response.status === 404) { + throw new Error('分享码不存在'); + } + + if (response.status === 410) { + throw new Error('分享已过期'); + } + + if (!response.ok) { + throw new Error(`获取信息失败: ${response.statusText}`); + } + + const fileInfo = await response.json(); + this.showFileInfo(fileInfo); + + } catch (error) { + console.error('Download info error:', error); + this.showToast(error.message || '获取文件信息失败', 'error'); + this.hideFileInfo(); + } finally { + const downloadBtn = document.getElementById('downloadBtn'); + downloadBtn.classList.remove('loading'); + downloadBtn.disabled = false; + } + } + + // 处理文件下载 + handleFileDownload(code) { + const downloadUrl = `${this.baseURL}/api/download/${code}`; + + // 创建隐藏的下载链接 + const link = document.createElement('a'); + link.href = downloadUrl; + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + this.showToast('开始下载文件...', 'success'); + } + + // 显示上传进度 + showProgress() { + const progressSection = document.getElementById('progressSection'); + const uploadResult = document.getElementById('uploadResult'); + const dropZone = document.getElementById('dropZone'); + + dropZone.style.display = 'none'; + uploadResult.style.display = 'none'; + progressSection.style.display = 'block'; + + this.updateProgress(0); + } + + // 更新进度条 + updateProgress(percent) { + const progressFill = document.getElementById('progressFill'); + const progressText = document.getElementById('progressText'); + + if (progressFill) { + progressFill.style.width = `${percent}%`; + } + + if (progressText) { + progressText.textContent = `上传中... ${percent}%`; + } + } + + // 隐藏进度条 + hideProgress() { + const progressSection = document.getElementById('progressSection'); + const dropZone = document.getElementById('dropZone'); + + progressSection.style.display = 'none'; + dropZone.style.display = 'block'; + } + + // 显示上传结果 + showUploadResult(result) { + const progressSection = document.getElementById('progressSection'); + const uploadResult = document.getElementById('uploadResult'); + const uploadCode = document.getElementById('uploadCode'); + const uploadLink = document.getElementById('uploadLink'); + const uploadExpire = document.getElementById('uploadExpire'); + + progressSection.style.display = 'none'; + uploadResult.style.display = 'block'; + + uploadCode.textContent = result.code; + uploadLink.textContent = `${this.baseURL}${result.download_url}`; + + const expireTime = new Date(result.expires_at); + const now = new Date(); + const diffMinutes = Math.ceil((expireTime - now) / (1000 * 60)); + uploadExpire.textContent = `${diffMinutes}分钟后过期`; + + // 设置下载按钮 + const downloadFileBtn = document.getElementById('downloadFileBtn'); + if (downloadFileBtn) { + downloadFileBtn.onclick = () => this.handleFileDownload(result.code); + } + } + + // 显示文本分享结果 + showTextResult(result) { + const textResult = document.getElementById('textResult'); + const textCode = document.getElementById('textCode'); + const textLink = document.getElementById('textLink'); + const textExpire = document.getElementById('textExpire'); + + textResult.style.display = 'block'; + + textCode.textContent = result.code; + textLink.textContent = `${this.baseURL}${result.download_url}`; + + const expireTime = new Date(result.expires_at); + const now = new Date(); + const diffMinutes = Math.ceil((expireTime - now) / (1000 * 60)); + textExpire.textContent = `${diffMinutes}分钟后过期`; + + // 隐藏输入区域 + document.querySelector('.text-input-area').style.display = 'none'; + } + + // 显示文件信息 + showFileInfo(fileInfo) { + const fileInfoDiv = document.getElementById('fileInfo'); + const fileName = document.getElementById('fileName'); + const fileSize = document.getElementById('fileSize'); + const fileType = document.getElementById('fileType'); + const fileExpire = document.getElementById('fileExpire'); + const downloadFileBtn = document.getElementById('downloadFileBtn'); + + fileInfoDiv.style.display = 'block'; + + fileName.textContent = fileInfo.filename; + fileSize.textContent = this.formatFileSize(fileInfo.size); + fileType.textContent = fileInfo.file_type; + + if (fileInfo.is_expired) { + fileExpire.textContent = '已过期'; + fileExpire.style.color = 'var(--error-color)'; + downloadFileBtn.disabled = true; + downloadFileBtn.textContent = '文件已过期'; + } else { + const expireTime = new Date(fileInfo.expires_at); + const now = new Date(); + const diffMinutes = Math.ceil((expireTime - now) / (1000 * 60)); + fileExpire.textContent = `剩余时间: ${diffMinutes}分钟`; + fileExpire.style.color = 'var(--warning-color)'; + downloadFileBtn.disabled = false; + downloadFileBtn.textContent = '⬇️ 下载文件'; + downloadFileBtn.onclick = () => this.handleFileDownload(fileInfo.code); + } + } + + // 隐藏文件信息 + hideFileInfo() { + const fileInfoDiv = document.getElementById('fileInfo'); + fileInfoDiv.style.display = 'none'; + } + + // 重置上传区域 + resetUploadSection() { + const dropZone = document.getElementById('dropZone'); + const progressSection = document.getElementById('progressSection'); + const uploadResult = document.getElementById('uploadResult'); + const fileInput = document.getElementById('fileInput'); + + dropZone.style.display = 'block'; + progressSection.style.display = 'none'; + uploadResult.style.display = 'none'; + + if (fileInput) { + fileInput.value = ''; + } + } + + // 重置文本区域 + resetTextSection() { + const textResult = document.getElementById('textResult'); + const textInputArea = document.querySelector('.text-input-area'); + const textContent = document.getElementById('textContent'); + const textFilename = document.getElementById('textFilename'); + + textResult.style.display = 'none'; + textInputArea.style.display = 'block'; + + textContent.value = ''; + textFilename.value = 'shared_text.txt'; + } + + // 复制到剪贴板 + async copyToClipboard(elementId) { + const element = document.getElementById(elementId); + if (!element) return; + + const text = element.textContent; + await this.copyTextToClipboard(text); + } + + // 复制文本到剪贴板 + async copyTextToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + this.showToast('已复制到剪贴板', 'success'); + } catch (err) { + // 降级处理 + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.opacity = '0'; + document.body.appendChild(textArea); + textArea.select(); + + try { + document.execCommand('copy'); + this.showToast('已复制到剪贴板', 'success'); + } catch (err) { + this.showToast('复制失败', 'error'); + } + + document.body.removeChild(textArea); + } + } + + // 格式化文件大小 + formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + } + + // 显示通知 + showToast(message, type = 'success') { + const toast = document.getElementById('toast'); + const toastMessage = document.getElementById('toastMessage'); + + if (!toast || !toastMessage) return; + + // 清除之前的类 + toast.className = 'toast'; + + // 添加类型类 + if (type !== 'success') { + toast.classList.add(type); + } + + toastMessage.textContent = message; + toast.style.display = 'block'; + + // 3秒后自动隐藏 + setTimeout(() => { + toast.style.display = 'none'; + }, 3000); + } + + // 格式化时间 + formatTime(dateString) { + const date = new Date(dateString); + return date.toLocaleString('zh-CN'); + } + + // 计算剩余时间 + calculateRemainingTime(expireTime) { + const now = new Date(); + const expire = new Date(expireTime); + const diffMs = expire - now; + + if (diffMs <= 0) { + return '已过期'; + } + + const diffMinutes = Math.ceil(diffMs / (1000 * 60)); + + if (diffMinutes > 60) { + const hours = Math.floor(diffMinutes / 60); + const minutes = diffMinutes % 60; + return `${hours}小时${minutes}分钟`; + } + + return `${diffMinutes}分钟`; + } +} + +// 页面加载完成后初始化应用 +document.addEventListener('DOMContentLoaded', () => { + new FileShareApp(); +}); + +// 工具函数:检测移动设备 +function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} + +// 工具函数:检测是否支持文件API +function supportsFileAPI() { + return window.File && window.FileReader && window.FileList && window.Blob; +} + +// 工具函数:检测是否支持拖拽 +function supportsDragAndDrop() { + const div = document.createElement('div'); + return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && + 'FormData' in window && + 'FileReader' in window; +} + +// 页面可见性变化时的处理 +document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + // 页面重新可见时,可以刷新一些数据 + console.log('页面重新可见'); + } +}); + +// 全局错误处理 +window.addEventListener('error', (e) => { + console.error('全局错误:', e.error); +}); + +// 未处理的Promise错误 +window.addEventListener('unhandledrejection', (e) => { + console.error('未处理的Promise错误:', e.reason); +}); + +// 导出工具函数供其他脚本使用 +window.FileShareUtils = { + isMobile, + supportsFileAPI, + supportsDragAndDrop +}; \ No newline at end of file diff --git a/static/curl-guide.html b/static/curl-guide.html new file mode 100644 index 0000000..352c5aa --- /dev/null +++ b/static/curl-guide.html @@ -0,0 +1,512 @@ + + + + + + curl命令使用教程 - 文件传输服务 + + + +
+
+

📡 curl命令使用教程

+

无需安装任何工具,使用系统自带的curl命令即可操作文件传输服务

+
+ +
+

📤 上传文件

+

使用curl上传任意文件,获取分享码:

+ +
+ +
curl -X POST -F "file=@文件路径" http://localhost:8000/api/upload
+
+ +
+
📝 示例:
+
+ +
# 上传图片
+curl -X POST -F "file=@photo.jpg" http://localhost:8000/api/upload
+
+# 上传文档
+curl -X POST -F "file=@document.pdf" http://localhost:8000/api/upload
+
+# 上传任意文件
+curl -X POST -F "file=@/path/to/your/file.txt" http://localhost:8000/api/upload
+
+
+ +
+
✅ 成功响应示例:
+
+
{
+  "code": "AB12CD34",
+  "expires_at": "2024-01-01T12:15:00",
+  "download_url": "/api/download/AB12CD34"
+}
+
+
+ +
+
💡 提示:
+
    +
  • 文件路径支持绝对路径和相对路径
  • +
  • 最大支持100MB文件
  • +
  • 分享码为8位大写字母+数字组合
  • +
+
+
+ +
+

📝 分享文本

+

直接分享文本内容,无需创建文件。提供多种简化方式:

+ +

✨ 方式1: 超级简单(推荐)

+
+ +
curl -X POST --data "你的文本内容" http://localhost:8000/api/text
+
+ +
+
📝 示例:
+
+ +
# 分享简单文本 ⭐ 最简单
+curl -X POST --data "Hello World!" http://localhost:8000/api/text
+
+# 分享多行文本
+curl -X POST --data "第一行
+第二行  
+第三行" http://localhost:8000/api/text
+
+# 从文件分享
+curl -X POST --data @myfile.txt http://localhost:8000/api/text
+
+# 通过管道分享
+echo "Hello from pipeline!" | curl -X POST --data @- http://localhost:8000/api/text
+
+
+ +

🔧 方式2: 表单方式(可指定文件名)

+
+ +
curl -X POST -F "content=文本内容" -F "filename=文件名.txt" http://localhost:8000/api/share-text-form
+
+ +
+
📝 示例:
+
+ +
# 指定文件名
+curl -X POST -F "content=Hello World!" -F "filename=hello.txt" http://localhost:8000/api/share-text-form
+
+# 分享代码
+curl -X POST -F "content=#!/bin/bash
+echo Hello" -F "filename=script.sh" http://localhost:8000/api/share-text-form
+
+
+ +

📋 方式3: JSON方式(兼容性)

+
+ +
curl -X POST -H "Content-Type: application/json" \
+  -d '{"content":"文本内容","filename":"文件名.txt"}' \
+  http://localhost:8000/api/share-text
+
+ +
+
💡 推荐使用顺序:
+
    +
  • ✨ 超级简单方式 - 最短命令,适合快速分享
  • +
  • 🔧 表单方式 - 需要自定义文件名时使用
  • +
  • 📋 JSON方式 - 需要更复杂控制时使用
  • +
+
+
+ +
+

⬇️ 下载文件

+

使用分享码下载文件到本地:

+ +
+ +
curl -O -J http://localhost:8000/api/download/分享码
+
+ +
+
📝 示例:
+
+ +
# 下载文件(保持原文件名)
+curl -O -J http://localhost:8000/api/download/AB12CD34
+
+# 下载并指定文件名
+curl -o myfile.pdf http://localhost:8000/api/download/AB12CD34
+
+# 下载到指定目录
+curl -o ~/Downloads/file.txt http://localhost:8000/api/download/AB12CD34
+
+
+ +
+
💡 参数说明:
+
    +
  • -O:保存文件,使用服务器端文件名
  • +
  • -J:使用服务器提供的文件名
  • +
  • -o filename:指定保存的文件名
  • +
+
+
+ +
+

ℹ️ 查看文件信息

+

在下载前查看文件的详细信息:

+ +
+ +
curl http://localhost:8000/api/info/分享码
+
+ +
+
📝 示例:
+
+ +
curl http://localhost:8000/api/info/AB12CD34
+
+
+ +
+
✅ 响应示例:
+
+
{
+  "code": "AB12CD34",
+  "filename": "document.pdf",
+  "file_type": "application/pdf",
+  "size": 1048576,
+  "created_at": "2024-01-01T12:00:00",
+  "expires_at": "2024-01-01T12:15:00",
+  "is_expired": false
+}
+
+
+
+ +
+

📊 列出所有分享

+

查看当前所有有效的分享:

+ +
+ +
curl http://localhost:8000/api/shares
+
+ +
+
💡 用途:
+
    +
  • 查看所有分享的文件列表
  • +
  • 检查文件过期时间
  • +
  • 管理和清理分享
  • +
+
+
+ +
+

🔧 高级用法

+ +

🎯 一键上传并获取分享码

+
+ +
# Linux/macOS 一行命令
+curl -X POST -F "file=@myfile.txt" http://localhost:8000/api/upload | grep -o '"code":"[^"]*"' | cut -d'"' -f4
+
+# 或使用jq解析JSON(需要安装jq)
+curl -X POST -F "file=@myfile.txt" http://localhost:8000/api/upload | jq -r '.code'
+
+ +

📱 批量上传

+
+ +
# 批量上传当前目录所有txt文件
+for file in *.txt; do
+    echo "上传: $file"
+    curl -X POST -F "file=@$file" http://localhost:8000/api/upload
+    echo -e "\n---"
+done
+
+ +

🔄 下载重试

+
+ +
# 网络不稳定时使用重试
+curl --retry 3 --retry-delay 1 -O -J http://localhost:8000/api/download/AB12CD34
+
+ +

📈 显示进度

+
+ +
# 上传时显示进度
+curl --progress-bar -X POST -F "file=@largefile.zip" http://localhost:8000/api/upload
+
+# 下载时显示进度
+curl --progress-bar -O -J http://localhost:8000/api/download/AB12CD34
+
+
+ +
+

❌ 错误处理

+ +
+
常见错误响应:
+
+
# 404 - 分享码不存在
+{
+  "detail": "分享码不存在"
+}
+
+# 410 - 分享已过期
+{
+  "detail": "分享已过期"
+}
+
+# 413 - 文件太大
+{
+  "detail": "文件太大,最大支持100MB"
+}
+
+
+ +
+
🔍 调试技巧:
+
    +
  • 添加 -v 参数查看详细信息
  • +
  • 添加 -w "%{http_code}" 查看HTTP状态码
  • +
  • 添加 -s 参数静默模式(仅显示结果)
  • +
+
+
+ +
+

🌐 远程服务器

+

如果服务部署在远程服务器,只需替换URL中的地址:

+ +
+ +
# 替换为您的服务器地址
+curl -X POST -F "file=@myfile.txt" https://your-domain.com/api/upload
+
+# 或使用IP地址
+curl -X POST -F "file=@myfile.txt" http://192.168.1.100:8000/api/upload
+
+
+ + ← 返回Web界面 +
+ + + + \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..53bb111 --- /dev/null +++ b/static/index.html @@ -0,0 +1,262 @@ + + + + + + 文件传输服务 + + + + +
+
+ +

快速分享文件和文本,15分钟自动过期

+
+ +
+ +
+ + + + +
+ + +
+
+
+
+
📁
+

拖拽文件到此处或点击选择

+

最大支持 100MB

+ + +
+
+ + + + +
+
+ + +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+

输入分享码下载文件

+
+ + +
+
+ + +
+
+ + +
+
+

💻 curl命令使用教程

+

+ 🎉 无需安装任何工具! 使用系统自带的curl命令即可操作文件传输服务。 + 📖 查看完整教程 +

+ +
+

📤 上传文件

+
+ curl -X POST -F "file=@文件路径" http://localhost:8000/api/upload + +
+

支持任意类型文件,最大100MB

+
+ +
+

📝 分享文本 ⭐

+
+
+ ✨ 超级简单: + curl -X POST --data "Hello World!" http://localhost:8000/api/text + +
+
+ 表单方式: + curl -X POST -F "content=Hello World!" -F "filename=hello.txt" http://localhost:8000/api/share-text-form + +
+
+ JSON方式: + curl -X POST -H "Content-Type: application/json" -d '{"content":"Hello World!"}' http://localhost:8000/api/share-text + +
+
+

✨ 推荐使用超级简单方式,只需要一个--data参数

+
+ +
+

⬇️ 下载文件

+
+ curl -O -J http://localhost:8000/api/download/分享码 + +
+

使用8位分享码下载文件,-O -J参数保持原文件名

+
+ +
+

ℹ️ 查看文件信息

+
+ curl http://localhost:8000/api/info/分享码 + +
+

查看文件名、大小、类型和过期时间

+
+ +
+

📊 列出所有分享

+
+ curl http://localhost:8000/api/shares + +
+

查看当前所有有效的分享

+
+ +
+

🎯 实用示例

+
+
+ 上传图片: + curl -X POST -F "file=@photo.jpg" http://localhost:8000/api/upload + +
+
+ 分享代码: + curl -X POST --data "#!/bin/bash +echo Hello" http://localhost:8000/api/text + +
+
+ 显示进度: + curl --progress-bar -X POST -F "file=@largefile.zip" http://localhost:8000/api/upload + +
+
+
+
+
+
+ + + + + +
+ + + + \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..be3839c --- /dev/null +++ b/static/style.css @@ -0,0 +1,829 @@ +/* 文件传输服务 - 样式表 */ + +/* 基础重置和变量 */ +:root { + --primary-color: #3b82f6; + --primary-hover: #2563eb; + --success-color: #10b981; + --error-color: #ef4444; + --warning-color: #f59e0b; + --text-primary: #1f2937; + --text-secondary: #6b7280; + --text-muted: #9ca3af; + --bg-primary: #ffffff; + --bg-secondary: #f9fafb; + --bg-tertiary: #f3f4f6; + --border-color: #e5e7eb; + --border-radius: 8px; + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --transition: all 0.2s ease-in-out; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + line-height: 1.6; + color: var(--text-primary); +} + +/* 容器布局 */ +.container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* 头部样式 */ +header { + text-align: center; + padding: 2rem 1rem 1rem; + color: white; +} + +.logo { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.logo .icon { + font-size: 2rem; +} + +.logo h1 { + font-size: 2.5rem; + font-weight: 700; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.subtitle { + font-size: 1.1rem; + opacity: 0.9; + margin-bottom: 0; +} + +/* 主内容区 */ +main { + flex: 1; + max-width: 800px; + margin: 0 auto; + padding: 0 1rem; + width: 100%; +} + +/* 选项卡导航 */ +.tab-nav { + display: flex; + background: var(--bg-primary); + border-radius: var(--border-radius); + padding: 0.25rem; + margin-bottom: 2rem; + box-shadow: var(--shadow-md); + overflow-x: auto; +} + +.tab-btn { + flex: 1; + padding: 0.75rem 1rem; + border: none; + background: transparent; + color: var(--text-secondary); + font-weight: 500; + border-radius: calc(var(--border-radius) - 2px); + cursor: pointer; + transition: var(--transition); + white-space: nowrap; + min-width: 120px; +} + +.tab-btn:hover { + color: var(--primary-color); + background: var(--bg-secondary); +} + +.tab-btn.active { + background: var(--primary-color); + color: white; + box-shadow: var(--shadow-sm); +} + +/* 选项卡内容 */ +.tab-content { + display: none; + animation: fadeIn 0.3s ease-in-out; +} + +.tab-content.active { + display: block; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* 文件上传区域 */ +.upload-section { + background: var(--bg-primary); + border-radius: var(--border-radius); + padding: 2rem; + box-shadow: var(--shadow-md); +} + +.drop-zone { + border: 2px dashed var(--border-color); + border-radius: var(--border-radius); + padding: 3rem 2rem; + text-align: center; + transition: var(--transition); + cursor: pointer; +} + +.drop-zone:hover, +.drop-zone.dragover { + border-color: var(--primary-color); + background: var(--bg-secondary); +} + +.drop-content .drop-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.drop-text { + font-size: 1.2rem; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.drop-hint { + color: var(--text-muted); + margin-bottom: 1.5rem; +} + +.select-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: var(--border-radius); + font-weight: 500; + cursor: pointer; + transition: var(--transition); +} + +.select-btn:hover { + background: var(--primary-hover); +} + +/* 进度条 */ +.progress-section { + margin-top: 2rem; +} + +.progress-bar { + width: 100%; + height: 8px; + background: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; + margin-bottom: 1rem; +} + +.progress-fill { + height: 100%; + background: var(--success-color); + transition: width 0.3s ease; + border-radius: 4px; +} + +.progress-text { + text-align: center; + color: var(--text-secondary); +} + +/* 结果卡片 */ +.result-section { + margin-top: 2rem; +} + +.result-card { + background: var(--bg-secondary); + border-radius: var(--border-radius); + padding: 2rem; + border: 1px solid var(--border-color); +} + +.result-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.success-icon { + font-size: 1.5rem; +} + +.result-header h3 { + color: var(--success-color); + font-size: 1.25rem; +} + +.share-info { + margin-bottom: 1.5rem; +} + +.share-code, +.share-link { + margin-bottom: 1rem; +} + +.share-code label, +.share-link label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--text-secondary); +} + +.code-display, +.link-display { + display: flex; + align-items: center; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 0.75rem; + gap: 0.5rem; +} + +.code-text, +.link-text { + flex: 1; + font-family: 'Courier New', monospace; + font-weight: 600; + color: var(--primary-color); + font-size: 0.9rem; +} + +.copy-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 0.5rem; + border-radius: calc(var(--border-radius) - 2px); + cursor: pointer; + transition: var(--transition); + font-size: 0.9rem; +} + +.copy-btn:hover { + background: var(--primary-hover); +} + +.expire-info { + text-align: center; + padding: 0.75rem; + background: var(--bg-primary); + border-radius: var(--border-radius); + border: 1px solid var(--warning-color); +} + +.expire-text { + color: var(--warning-color); + font-weight: 500; +} + +.new-upload-btn, +.new-text-btn { + width: 100%; + background: var(--primary-color); + color: white; + border: none; + padding: 0.75rem; + border-radius: var(--border-radius); + font-weight: 500; + cursor: pointer; + transition: var(--transition); +} + +.new-upload-btn:hover, +.new-text-btn:hover { + background: var(--primary-hover); +} + +/* 文本分享区域 */ +.text-section { + background: var(--bg-primary); + border-radius: var(--border-radius); + padding: 2rem; + box-shadow: var(--shadow-md); +} + +.text-input-area { + margin-bottom: 2rem; +} + +#textContent { + width: 100%; + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-family: 'Courier New', monospace; + font-size: 0.9rem; + resize: vertical; + margin-bottom: 1rem; + transition: var(--transition); +} + +#textContent:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.text-controls { + display: flex; + gap: 1rem; + align-items: center; +} + +#textFilename { + flex: 1; + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + transition: var(--transition); +} + +#textFilename:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.share-text-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: var(--border-radius); + font-weight: 500; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; +} + +.share-text-btn:hover { + background: var(--primary-hover); +} + +/* 下载区域 */ +.download-section { + background: var(--bg-primary); + border-radius: var(--border-radius); + padding: 2rem; + box-shadow: var(--shadow-md); +} + +.download-input { + text-align: center; + margin-bottom: 2rem; +} + +.download-input h3 { + margin-bottom: 1.5rem; + color: var(--text-primary); +} + +.code-input-group { + display: flex; + max-width: 400px; + margin: 0 auto; + gap: 1rem; +} + +#downloadCode { + flex: 1; + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + text-align: center; + font-family: 'Courier New', monospace; + font-size: 1.1rem; + font-weight: 600; + text-transform: uppercase; + transition: var(--transition); +} + +#downloadCode:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.download-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: var(--border-radius); + font-weight: 500; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; +} + +.download-btn:hover { + background: var(--primary-hover); +} + +.file-info { + margin-top: 2rem; +} + +.info-card { + display: flex; + align-items: center; + gap: 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1.5rem; +} + +.file-icon { + font-size: 2rem; + flex-shrink: 0; +} + +.file-details { + flex: 1; +} + +.file-details h4 { + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.file-meta { + display: flex; + gap: 1rem; + margin-bottom: 0.5rem; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.file-expire { + font-size: 0.9rem; + color: var(--warning-color); + font-weight: 500; +} + +.download-file-btn { + background: var(--success-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: var(--border-radius); + font-weight: 500; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; +} + +.download-file-btn:hover { + background: #059669; +} + +/* 帮助区域 */ +.help-section { + background: var(--bg-primary); + border-radius: var(--border-radius); + padding: 2rem; + box-shadow: var(--shadow-md); +} + +.help-section h3 { + margin-bottom: 2rem; + text-align: center; + color: var(--text-primary); +} + +.help-group { + margin-bottom: 2rem; +} + +.help-group h4 { + margin-bottom: 1rem; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.code-block { + display: flex; + align-items: center; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 0.75rem 1rem; + margin-bottom: 0.5rem; + gap: 0.5rem; +} + +.code-blocks .code-block { + margin-bottom: 0.75rem; +} + +.code-label { + font-size: 0.85rem; + color: var(--text-secondary); + margin-right: 0.5rem; + font-weight: 500; + min-width: 100px; +} + +.code-block code { + flex: 1; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + color: var(--text-primary); + background: none; + border: none; + font-weight: 500; +} + +.copy-code-btn { + background: var(--text-muted); + color: white; + border: none; + padding: 0.25rem 0.5rem; + border-radius: calc(var(--border-radius) - 2px); + cursor: pointer; + transition: var(--transition); + font-size: 0.8rem; +} + +.copy-code-btn:hover { + background: var(--text-secondary); +} + +.help-desc { + color: var(--text-muted); + font-size: 0.9rem; + margin-top: 0.5rem; + padding-left: 0.5rem; + border-left: 3px solid var(--border-color); +} + +.config-table { + overflow-x: auto; +} + +.config-table table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + overflow: hidden; +} + +.config-table th, +.config-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.config-table th { + background: var(--bg-tertiary); + font-weight: 600; + color: var(--text-primary); +} + +.config-table td code { + background: var(--bg-tertiary); + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 0.85rem; +} + +/* 页脚 */ +footer { + padding: 2rem 1rem 1rem; + text-align: center; + color: white; + margin-top: 2rem; +} + +.footer-content { + max-width: 800px; + margin: 0 auto; +} + +.footer-content p { + margin-bottom: 0.5rem; + opacity: 0.8; +} + +.footer-links { + display: flex; + justify-content: center; + gap: 1rem; +} + +.footer-links a { + color: white; + text-decoration: none; + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + transition: var(--transition); + opacity: 0.8; +} + +.footer-links a:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +/* 通知提示 */ +.toast { + position: fixed; + top: 2rem; + right: 2rem; + background: var(--success-color); + color: white; + padding: 1rem 1.5rem; + border-radius: var(--border-radius); + box-shadow: var(--shadow-lg); + z-index: 1000; + animation: slideIn 0.3s ease-out; +} + +.toast.error { + background: var(--error-color); +} + +.toast.warning { + background: var(--warning-color); +} + +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .logo h1 { + font-size: 2rem; + } + + .tab-nav { + flex-direction: column; + gap: 0.25rem; + } + + .tab-btn { + min-width: auto; + } + + .upload-section, + .text-section, + .download-section, + .help-section { + padding: 1.5rem; + } + + .drop-zone { + padding: 2rem 1rem; + } + + .text-controls { + flex-direction: column; + } + + .code-input-group { + flex-direction: column; + } + + .info-card { + flex-direction: column; + text-align: center; + } + + .file-meta { + justify-content: center; + } + + .code-block { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; + } + + .code-label { + min-width: auto; + margin-right: 0; + } + + .toast { + right: 1rem; + left: 1rem; + top: 1rem; + } + + .footer-links { + flex-direction: column; + gap: 0.5rem; + } +} + +/* 暗色主题支持 */ +@media (prefers-color-scheme: dark) { + :root { + --text-primary: #f9fafb; + --text-secondary: #d1d5db; + --text-muted: #9ca3af; + --bg-primary: #1f2937; + --bg-secondary: #374151; + --bg-tertiary: #4b5563; + --border-color: #4b5563; + } + + body { + background: linear-gradient(135deg, #1e293b 0%, #334155 100%); + } +} + +/* 加载状态 */ +.loading { + opacity: 0.6; + pointer-events: none; + position: relative; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid var(--primary-color); + border-radius: 50%; + border-top-color: transparent; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* 错误状态 */ +.error-message { + background: var(--error-color); + color: white; + padding: 1rem; + border-radius: var(--border-radius); + margin: 1rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.error-message::before { + content: '❌'; +} + +/* 成功状态 */ +.success-message { + background: var(--success-color); + color: white; + padding: 1rem; + border-radius: var(--border-radius); + margin: 1rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.success-message::before { + content: '✅'; +} \ No newline at end of file diff --git a/test_server.py b/test_server.py new file mode 100755 index 0000000..9aad045 --- /dev/null +++ b/test_server.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +快速测试脚本 - 验证服务是否正常运行 +""" +import requests +import time +import sys + +def test_server(): + """测试服务器是否正常运行""" + base_url = "http://localhost:8000" + + print("🧪 文件传输服务测试") + print("=" * 50) + + # 测试首页 + print("1. 测试首页...") + try: + response = requests.get(base_url, timeout=10) + if response.status_code == 200: + print("✅ 首页正常 (200)") + if "文件传输服务" in response.text: + print("✅ 页面内容正确") + else: + print("⚠️ 页面内容异常") + else: + print(f"❌ 首页异常 ({response.status_code})") + return False + except Exception as e: + print(f"❌ 无法连接到服务器: {e}") + print("💡 请确保服务器已启动: python app.py") + return False + + # 测试静态文件 + print("\n2. 测试静态文件...") + static_files = [ + "/static/style.css", + "/static/app.js" + ] + + for file_path in static_files: + try: + response = requests.get(f"{base_url}{file_path}", timeout=5) + if response.status_code == 200: + print(f"✅ {file_path} - 正常") + else: + print(f"❌ {file_path} - 异常 ({response.status_code})") + except Exception as e: + print(f"❌ {file_path} - 错误: {e}") + + # 测试API信息 + print("\n3. 测试API...") + try: + response = requests.get(f"{base_url}/api", timeout=5) + if response.status_code == 200: + data = response.json() + print("✅ API信息正常") + print(f" 服务: {data.get('service', 'N/A')}") + print(f" 版本: {data.get('version', 'N/A')}") + else: + print(f"❌ API异常 ({response.status_code})") + except Exception as e: + print(f"❌ API错误: {e}") + + # 测试API文档 + print("\n4. 测试API文档...") + try: + response = requests.get(f"{base_url}/docs", timeout=5) + if response.status_code == 200: + print("✅ API文档正常") + else: + print(f"❌ API文档异常 ({response.status_code})") + except Exception as e: + print(f"❌ API文档错误: {e}") + + print("\n" + "=" * 50) + print("🎉 测试完成!") + print(f"🌐 访问地址: {base_url}") + + return True + +if __name__ == "__main__": + if not test_server(): + sys.exit(1) \ No newline at end of file diff --git a/verify_setup.sh b/verify_setup.sh new file mode 100755 index 0000000..e2cc64a --- /dev/null +++ b/verify_setup.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# 服务验证脚本 - 检查服务是否正确设置和运行 + +echo "🔍 文件传输服务验证脚本" +echo "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" + +# 检查当前目录 +if [ ! -f "app.py" ]; then + echo "❌ 请在fileshare目录中运行此脚本" + echo "💡 cd fileshare && ./verify_setup.sh" + exit 1 +fi + +echo "📂 检查文件结构..." + +# 检查必需文件 +files=("app.py" "cli.py" "requirements.txt" "static/index.html" "static/style.css" "static/app.js") +missing_files=() + +for file in "${files[@]}"; do + if [ -f "$file" ]; then + echo "✅ $file" + else + echo "❌ 缺少文件: $file" + missing_files+=("$file") + fi +done + +if [ ${#missing_files[@]} -ne 0 ]; then + echo + echo "⚠️ 发现 ${#missing_files[@]} 个缺失文件,请先运行: python fix_paths.py" + exit 1 +fi + +echo +echo "🔧 检查Python依赖..." + +# 检查Python是否安装 +if ! command -v python &> /dev/null && ! command -v python3 &> /dev/null; then + echo "❌ Python 未安装" + exit 1 +else + echo "✅ Python 已安装" +fi + +# 检查pip是否安装 +if ! command -v pip &> /dev/null && ! command -v pip3 &> /dev/null; then + echo "❌ pip 未安装" + exit 1 +else + echo "✅ pip 已安装" +fi + +# 检查虚拟环境(可选) +if [ -n "$VIRTUAL_ENV" ]; then + echo "✅ 当前在虚拟环境中: $(basename $VIRTUAL_ENV)" +else + echo "ℹ️ 未使用虚拟环境(可选)" +fi + +echo +echo "📦 检查依赖包..." + +# 检查主要依赖 +python_cmd="python" +if command -v python3 &> /dev/null; then + python_cmd="python3" +fi + +required_packages=("fastapi" "uvicorn" "httpx" "click" "rich") +missing_packages=() + +for package in "${required_packages[@]}"; do + if $python_cmd -c "import $package" 2>/dev/null; then + echo "✅ $package" + else + echo "❌ 缺少包: $package" + missing_packages+=("$package") + fi +done + +if [ ${#missing_packages[@]} -ne 0 ]; then + echo + echo "⚠️ 发现 ${#missing_packages[@]} 个缺失依赖包,请运行:" + echo " pip install -r requirements.txt" + echo +fi + +echo +echo "🚀 启动测试..." + +# 检查端口是否被占用 +if command -v lsof &> /dev/null; then + if lsof -i :8000 > /dev/null 2>&1; then + echo "⚠️ 端口 8000 已被占用" + echo "ℹ️ 当前占用进程:" + lsof -i :8000 + echo + echo "💡 可以使用其他端口: PORT=8001 python app.py" + else + echo "✅ 端口 8000 可用" + fi +fi + +# 提供启动建议 +echo +echo "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" "=" +echo "🎉 验证完成!" +echo + +if [ ${#missing_files[@]} -eq 0 ] && [ ${#missing_packages[@]} -eq 0 ]; then + echo "✅ 所有检查都通过,可以启动服务了!" + echo + echo "🚀 推荐启动方式:" + echo " 1. 直接启动: python app.py" + echo " 2. 开发模式: uvicorn app:app --reload" + echo " 3. Docker启动: ./start.sh" + echo + echo "🌐 启动后访问: http://localhost:8000" + echo + echo "🧪 启动后可运行测试: python test_server.py" +else + echo "⚠️ 请先解决上述问题,然后重新运行此脚本" +fi \ No newline at end of file