From 4985344911c1773243c210cbc65984a5a7c455f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Sun, 10 Aug 2025 12:57:17 +0800 Subject: [PATCH] first commit --- .dockerignore | 64 +++ .env | 31 ++ .env.example | 31 ++ Dockerfile | 52 +++ Dockerfile.multi | 66 +++ README.md | 369 +++++++++++++++++ app.py | 419 +++++++++++++++++++ build.sh | 149 +++++++ cli.py | 365 +++++++++++++++++ curl_examples.sh | 71 ++++ data/uploads/65313572.txt | 1 + demo.sh | 102 +++++ docker-compose.aliyun.yml | 132 ++++++ docker-compose.yml | 88 ++++ fileshare_functions.sh | 250 ++++++++++++ fix_paths.py | 127 ++++++ requirements.txt | 8 + start.sh | 88 ++++ static/app.js | 547 +++++++++++++++++++++++++ static/curl-guide.html | 512 +++++++++++++++++++++++ static/index.html | 262 ++++++++++++ static/style.css | 829 ++++++++++++++++++++++++++++++++++++++ test_server.py | 84 ++++ verify_setup.sh | 124 ++++++ 24 files changed, 4771 insertions(+) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 Dockerfile.multi create mode 100644 README.md create mode 100644 app.py create mode 100755 build.sh create mode 100644 cli.py create mode 100755 curl_examples.sh create mode 100644 data/uploads/65313572.txt create mode 100755 demo.sh create mode 100644 docker-compose.aliyun.yml create mode 100644 docker-compose.yml create mode 100755 fileshare_functions.sh create mode 100755 fix_paths.py create mode 100644 requirements.txt create mode 100755 start.sh create mode 100644 static/app.js create mode 100644 static/curl-guide.html create mode 100644 static/index.html create mode 100644 static/style.css create mode 100755 test_server.py create mode 100755 verify_setup.sh 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