first commit

This commit is contained in:
朱潮 2025-08-10 12:57:17 +08:00
commit 4985344911
24 changed files with 4771 additions and 0 deletions

64
.dockerignore Normal file
View File

@ -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

31
.env Normal file
View File

@ -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

31
.env.example Normal file
View File

@ -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

52
Dockerfile Normal file
View File

@ -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"]

66
Dockerfile.multi Normal file
View File

@ -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"]

369
README.md Normal file
View File

@ -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

419
app.py Normal file
View File

@ -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="<h1>curl教程页面不存在</h1><p>请检查static/curl-guide.html文件</p>", 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"
)

149
build.sh Executable file
View File

@ -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

365
cli.py Normal file
View File

@ -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()

71
curl_examples.sh Executable file
View File

@ -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"

View File

@ -0,0 +1 @@
Hello World”

102
demo.sh Executable file
View File

@ -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"

132
docker-compose.aliyun.yml Normal file
View File

@ -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

88
docker-compose.yml Normal file
View File

@ -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

250
fileshare_functions.sh Executable file
View File

@ -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'

127
fix_paths.py Executable file
View File

@ -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'<link rel="stylesheet" href="(?!/)([^"]+\.css)"',
r'<link rel="stylesheet" href="/static/\1"',
content
)
# 修复JS路径
content = re.sub(
r'<script src="(?!/)([^"]+\.js)"',
r'<script src="/static/\1"',
content
)
# 修复图片路径(如果有的话)
content = re.sub(
r'<img src="(?!/)(?!http)([^"]+\.(png|jpg|jpeg|gif|svg))"',
r'<img src="/static/\1"',
content
)
with open(html_file, 'w', encoding='utf-8') as f:
f.write(content)
print("✅ HTML路径修复完成")
return True
def check_file_structure():
"""检查文件结构是否正确"""
print("📁 检查文件结构...")
required_files = [
"app.py",
"cli.py",
"requirements.txt",
"static/index.html",
"static/style.css",
"static/app.js"
]
missing_files = []
for file_path in required_files:
if not Path(file_path).exists():
missing_files.append(file_path)
print(f"❌ 缺少文件: {file_path}")
else:
print(f"✅ 文件存在: {file_path}")
if missing_files:
print(f"\n⚠️ 缺少 {len(missing_files)} 个文件")
return False
else:
print("\n🎉 所有必需文件都存在")
return True
def create_missing_directories():
"""创建缺失的目录"""
print("📂 创建必需目录...")
directories = [
"static",
"uploads",
"data",
"data/uploads",
"data/logs"
]
for dir_path in directories:
Path(dir_path).mkdir(parents=True, exist_ok=True)
print(f"✅ 目录已确保存在: {dir_path}")
def main():
"""主函数"""
print("🔧 文件传输服务路径修复工具")
print("=" * 50)
# 检查当前目录
if not Path("app.py").exists():
print("❌ 请在fileshare目录中运行此脚本")
print("💡 cd fileshare && python fix_paths.py")
return False
# 创建目录
create_missing_directories()
# 检查文件结构
if not check_file_structure():
print("\n❌ 文件结构不完整,请检查")
return False
# 修复HTML路径
if not fix_html_paths():
return False
print("\n" + "=" * 50)
print("✅ 路径修复完成!")
print("\n🚀 现在可以启动服务:")
print(" python app.py")
print("\n🧪 或运行测试:")
print(" python test_server.py")
return True
if __name__ == "__main__":
main()

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
pydantic==2.5.0
python-dotenv==1.0.0
httpx==0.25.2
click==8.1.7
rich==13.7.0

88
start.sh Executable file
View File

@ -0,0 +1,88 @@
#!/bin/bash
# 文件传输服务启动脚本
echo "=== 文件传输服务启动脚本 ==="
echo
# 检查Docker是否安装
if ! command -v docker &> /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

547
static/app.js Normal file
View File

@ -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
};

512
static/curl-guide.html Normal file
View File

@ -0,0 +1,512 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>curl命令使用教程 - 文件传输服务</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1rem;
}
.header {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
text-align: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.header h1 {
color: #3b82f6;
margin-bottom: 0.5rem;
font-size: 2rem;
}
.header p {
color: #666;
font-size: 1.1rem;
}
.section {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.section h2 {
color: #1f2937;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e5e7eb;
display: flex;
align-items: center;
gap: 0.5rem;
}
.code-block {
background: #1f2937;
color: #f8f9fa;
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
position: relative;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9rem;
}
.code-block pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
.copy-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: #3b82f6;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
transition: background 0.2s;
}
.copy-btn:hover {
background: #2563eb;
}
.example {
background: #f3f4f6;
border-left: 4px solid #10b981;
padding: 1rem;
margin: 1rem 0;
border-radius: 0 8px 8px 0;
}
.example-title {
font-weight: 600;
color: #10b981;
margin-bottom: 0.5rem;
}
.tip {
background: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 1rem;
margin: 1rem 0;
border-radius: 0 8px 8px 0;
}
.tip-title {
font-weight: 600;
color: #d97706;
margin-bottom: 0.5rem;
}
.response-example {
background: #ecfdf5;
border: 1px solid #10b981;
padding: 1rem;
border-radius: 8px;
margin-top: 0.5rem;
}
.response-title {
font-weight: 600;
color: #10b981;
margin-bottom: 0.5rem;
}
.back-link {
display: inline-block;
background: #3b82f6;
color: white;
text-decoration: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
margin-top: 2rem;
transition: background 0.2s;
}
.back-link:hover {
background: #2563eb;
}
@media (max-width: 768px) {
.header h1 {
font-size: 1.5rem;
}
.code-block {
font-size: 0.8rem;
}
.copy-btn {
font-size: 0.7rem;
padding: 0.2rem 0.4rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📡 curl命令使用教程</h1>
<p>无需安装任何工具使用系统自带的curl命令即可操作文件传输服务</p>
</div>
<div class="section">
<h2>📤 上传文件</h2>
<p>使用curl上传任意文件获取分享码</p>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre>curl -X POST -F "file=@文件路径" http://localhost:8000/api/upload</pre>
</div>
<div class="example">
<div class="example-title">📝 示例:</div>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 上传图片
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</pre>
</div>
</div>
<div class="response-example">
<div class="response-title">✅ 成功响应示例:</div>
<div class="code-block">
<pre>{
"code": "AB12CD34",
"expires_at": "2024-01-01T12:15:00",
"download_url": "/api/download/AB12CD34"
}</pre>
</div>
</div>
<div class="tip">
<div class="tip-title">💡 提示:</div>
<ul>
<li>文件路径支持绝对路径和相对路径</li>
<li>最大支持100MB文件</li>
<li>分享码为8位大写字母+数字组合</li>
</ul>
</div>
</div>
<div class="section">
<h2>📝 分享文本</h2>
<p>直接分享文本内容,无需创建文件。提供多种简化方式:</p>
<h3>✨ 方式1: 超级简单(推荐)</h3>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre>curl -X POST --data "你的文本内容" http://localhost:8000/api/text</pre>
</div>
<div class="example">
<div class="example-title">📝 示例:</div>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 分享简单文本 ⭐ 最简单
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</pre>
</div>
</div>
<h3>🔧 方式2: 表单方式(可指定文件名)</h3>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre>curl -X POST -F "content=文本内容" -F "filename=文件名.txt" http://localhost:8000/api/share-text-form</pre>
</div>
<div class="example">
<div class="example-title">📝 示例:</div>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 指定文件名
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</pre>
</div>
</div>
<h3>📋 方式3: JSON方式兼容性</h3>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre>curl -X POST -H "Content-Type: application/json" \
-d '{"content":"文本内容","filename":"文件名.txt"}' \
http://localhost:8000/api/share-text</pre>
</div>
<div class="tip">
<div class="tip-title">💡 推荐使用顺序:</div>
<ul>
<li><strong>✨ 超级简单方式</strong> - 最短命令,适合快速分享</li>
<li><strong>🔧 表单方式</strong> - 需要自定义文件名时使用</li>
<li><strong>📋 JSON方式</strong> - 需要更复杂控制时使用</li>
</ul>
</div>
</div>
<div class="section">
<h2>⬇️ 下载文件</h2>
<p>使用分享码下载文件到本地:</p>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre>curl -O -J http://localhost:8000/api/download/分享码</pre>
</div>
<div class="example">
<div class="example-title">📝 示例:</div>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 下载文件(保持原文件名)
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</pre>
</div>
</div>
<div class="tip">
<div class="tip-title">💡 参数说明:</div>
<ul>
<li><code>-O</code>:保存文件,使用服务器端文件名</li>
<li><code>-J</code>:使用服务器提供的文件名</li>
<li><code>-o filename</code>:指定保存的文件名</li>
</ul>
</div>
</div>
<div class="section">
<h2> 查看文件信息</h2>
<p>在下载前查看文件的详细信息:</p>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre>curl http://localhost:8000/api/info/分享码</pre>
</div>
<div class="example">
<div class="example-title">📝 示例:</div>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre>curl http://localhost:8000/api/info/AB12CD34</pre>
</div>
</div>
<div class="response-example">
<div class="response-title">✅ 响应示例:</div>
<div class="code-block">
<pre>{
"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
}</pre>
</div>
</div>
</div>
<div class="section">
<h2>📊 列出所有分享</h2>
<p>查看当前所有有效的分享:</p>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre>curl http://localhost:8000/api/shares</pre>
</div>
<div class="tip">
<div class="tip-title">💡 用途:</div>
<ul>
<li>查看所有分享的文件列表</li>
<li>检查文件过期时间</li>
<li>管理和清理分享</li>
</ul>
</div>
</div>
<div class="section">
<h2>🔧 高级用法</h2>
<h3>🎯 一键上传并获取分享码</h3>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 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'</pre>
</div>
<h3>📱 批量上传</h3>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 批量上传当前目录所有txt文件
for file in *.txt; do
echo "上传: $file"
curl -X POST -F "file=@$file" http://localhost:8000/api/upload
echo -e "\n---"
done</pre>
</div>
<h3>🔄 下载重试</h3>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 网络不稳定时使用重试
curl --retry 3 --retry-delay 1 -O -J http://localhost:8000/api/download/AB12CD34</pre>
</div>
<h3>📈 显示进度</h3>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 上传时显示进度
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</pre>
</div>
</div>
<div class="section">
<h2>❌ 错误处理</h2>
<div class="example">
<div class="example-title">常见错误响应:</div>
<div class="code-block">
<pre># 404 - 分享码不存在
{
"detail": "分享码不存在"
}
# 410 - 分享已过期
{
"detail": "分享已过期"
}
# 413 - 文件太大
{
"detail": "文件太大最大支持100MB"
}</pre>
</div>
</div>
<div class="tip">
<div class="tip-title">🔍 调试技巧:</div>
<ul>
<li>添加 <code>-v</code> 参数查看详细信息</li>
<li>添加 <code>-w "%{http_code}"</code> 查看HTTP状态码</li>
<li>添加 <code>-s</code> 参数静默模式(仅显示结果)</li>
</ul>
</div>
</div>
<div class="section">
<h2>🌐 远程服务器</h2>
<p>如果服务部署在远程服务器只需替换URL中的地址</p>
<div class="code-block">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre># 替换为您的服务器地址
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</pre>
</div>
</div>
<a href="/" class="back-link">← 返回Web界面</a>
</div>
<script>
function copyCode(button) {
const codeBlock = button.nextElementSibling;
const code = codeBlock.textContent;
navigator.clipboard.writeText(code).then(() => {
button.textContent = '已复制!';
button.style.background = '#10b981';
setTimeout(() => {
button.textContent = '复制';
button.style.background = '#3b82f6';
}, 2000);
}).catch(err => {
console.error('复制失败:', err);
button.textContent = '复制失败';
button.style.background = '#ef4444';
setTimeout(() => {
button.textContent = '复制';
button.style.background = '#3b82f6';
}, 2000);
});
}
// 自动替换localhost为当前域名
document.addEventListener('DOMContentLoaded', function() {
const currentHost = window.location.host;
const currentProtocol = window.location.protocol;
const baseUrl = `${currentProtocol}//${currentHost}`;
if (currentHost !== 'localhost:8000') {
const codeBlocks = document.querySelectorAll('.code-block pre');
codeBlocks.forEach(block => {
block.textContent = block.textContent.replace(/http:\/\/localhost:8000/g, baseUrl);
});
}
});
</script>
</body>
</html>

262
static/index.html Normal file
View File

@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件传输服务</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📤</text></svg>">
</head>
<body>
<div class="container">
<header>
<div class="logo">
<span class="icon">📤</span>
<h1>文件传输服务</h1>
</div>
<p class="subtitle">快速分享文件和文本15分钟自动过期</p>
</header>
<main>
<!-- 选项卡导航 -->
<div class="tab-nav">
<button class="tab-btn active" data-tab="upload">📁 上传文件</button>
<button class="tab-btn" data-tab="text">📝 分享文本</button>
<button class="tab-btn" data-tab="download">⬇️ 下载文件</button>
<button class="tab-btn" data-tab="help">💻 curl命令</button>
</div>
<!-- 文件上传 -->
<div class="tab-content active" id="upload-tab">
<div class="upload-section">
<div class="drop-zone" id="dropZone">
<div class="drop-content">
<div class="drop-icon">📁</div>
<p class="drop-text">拖拽文件到此处或点击选择</p>
<p class="drop-hint">最大支持 100MB</p>
<input type="file" id="fileInput" hidden>
<button class="select-btn">选择文件</button>
</div>
</div>
<div class="progress-section" id="progressSection" style="display: none;">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<p class="progress-text" id="progressText">上传中...</p>
</div>
<div class="result-section" id="uploadResult" style="display: none;">
<div class="result-card">
<div class="result-header">
<span class="success-icon"></span>
<h3>上传成功!</h3>
</div>
<div class="share-info">
<div class="share-code">
<label>分享码:</label>
<div class="code-display">
<span class="code-text" id="uploadCode">ABCD1234</span>
<button class="copy-btn" onclick="copyCode('uploadCode')">📋</button>
</div>
</div>
<div class="share-link">
<label>下载链接:</label>
<div class="link-display">
<span class="link-text" id="uploadLink">http://localhost:8000/api/download/ABCD1234</span>
<button class="copy-btn" onclick="copyCode('uploadLink')">📋</button>
</div>
</div>
<div class="expire-info">
<span class="expire-text" id="uploadExpire">15分钟后过期</span>
</div>
</div>
<button class="new-upload-btn" onclick="resetUpload()">上传新文件</button>
</div>
</div>
</div>
</div>
<!-- 文本分享 -->
<div class="tab-content" id="text-tab">
<div class="text-section">
<div class="text-input-area">
<textarea id="textContent" placeholder="在此输入要分享的文本内容..." rows="8"></textarea>
<div class="text-controls">
<input type="text" id="textFilename" placeholder="文件名 (可选)" value="shared_text.txt">
<button class="share-text-btn" id="shareTextBtn">📝 分享文本</button>
</div>
</div>
<div class="result-section" id="textResult" style="display: none;">
<div class="result-card">
<div class="result-header">
<span class="success-icon"></span>
<h3>分享成功!</h3>
</div>
<div class="share-info">
<div class="share-code">
<label>分享码:</label>
<div class="code-display">
<span class="code-text" id="textCode">ABCD1234</span>
<button class="copy-btn" onclick="copyCode('textCode')">📋</button>
</div>
</div>
<div class="share-link">
<label>下载链接:</label>
<div class="link-display">
<span class="link-text" id="textLink">http://localhost:8000/api/download/ABCD1234</span>
<button class="copy-btn" onclick="copyCode('textLink')">📋</button>
</div>
</div>
<div class="expire-info">
<span class="expire-text" id="textExpire">15分钟后过期</span>
</div>
</div>
<button class="new-text-btn" onclick="resetText()">分享新文本</button>
</div>
</div>
</div>
</div>
<!-- 文件下载 -->
<div class="tab-content" id="download-tab">
<div class="download-section">
<div class="download-input">
<h3>输入分享码下载文件</h3>
<div class="code-input-group">
<input type="text" id="downloadCode" placeholder="请输入8位分享码" maxlength="8">
<button class="download-btn" id="downloadBtn">📥 下载</button>
</div>
</div>
<div class="file-info" id="fileInfo" style="display: none;">
<div class="info-card">
<div class="file-icon">📄</div>
<div class="file-details">
<h4 id="fileName">文件名.txt</h4>
<div class="file-meta">
<span id="fileSize">1.2 MB</span>
<span id="fileType">text/plain</span>
</div>
<div class="file-expire">
<span id="fileExpire">剩余时间: 12分钟</span>
</div>
</div>
<button class="download-file-btn" id="downloadFileBtn">⬇️ 下载文件</button>
</div>
</div>
</div>
</div>
<!-- curl命令教程 -->
<div class="tab-content" id="help-tab">
<div class="help-section">
<h3>💻 curl命令使用教程</h3>
<p style="margin-bottom: 2rem; padding: 1rem; background: #e0f2fe; border-left: 4px solid #0284c7; border-radius: 4px;">
🎉 <strong>无需安装任何工具!</strong> 使用系统自带的curl命令即可操作文件传输服务。
<a href="/curl" target="_blank" style="color: #0284c7; font-weight: bold; text-decoration: none; margin-left: 1rem;">📖 查看完整教程</a>
</p>
<div class="help-group">
<h4>📤 上传文件</h4>
<div class="code-block">
<code>curl -X POST -F "file=@文件路径" http://localhost:8000/api/upload</code>
<button class="copy-code-btn" onclick="copyText('curl -X POST -F \"file=@文件路径\" http://localhost:8000/api/upload')">📋</button>
</div>
<p class="help-desc">支持任意类型文件最大100MB</p>
</div>
<div class="help-group">
<h4>📝 分享文本 ⭐</h4>
<div class="code-blocks">
<div class="code-block">
<span class="code-label">✨ 超级简单:</span>
<code>curl -X POST --data "Hello World!" http://localhost:8000/api/text</code>
<button class="copy-code-btn" onclick="copyText('curl -X POST --data \"Hello World!\" http://localhost:8000/api/text')">📋</button>
</div>
<div class="code-block">
<span class="code-label">表单方式:</span>
<code>curl -X POST -F "content=Hello World!" -F "filename=hello.txt" http://localhost:8000/api/share-text-form</code>
<button class="copy-code-btn" onclick="copyText('curl -X POST -F \"content=Hello World!\" -F \"filename=hello.txt\" http://localhost:8000/api/share-text-form')">📋</button>
</div>
<div class="code-block">
<span class="code-label">JSON方式:</span>
<code>curl -X POST -H "Content-Type: application/json" -d '{"content":"Hello World!"}' http://localhost:8000/api/share-text</code>
<button class="copy-code-btn" onclick="copyText('curl -X POST -H \"Content-Type: application/json\" -d \'{\"content\":\"Hello World!\"}\' http://localhost:8000/api/share-text')">📋</button>
</div>
</div>
<p class="help-desc">✨ 推荐使用超级简单方式,只需要一个--data参数</p>
</div>
<div class="help-group">
<h4>⬇️ 下载文件</h4>
<div class="code-block">
<code>curl -O -J http://localhost:8000/api/download/分享码</code>
<button class="copy-code-btn" onclick="copyText('curl -O -J http://localhost:8000/api/download/分享码')">📋</button>
</div>
<p class="help-desc">使用8位分享码下载文件-O -J参数保持原文件名</p>
</div>
<div class="help-group">
<h4> 查看文件信息</h4>
<div class="code-block">
<code>curl http://localhost:8000/api/info/分享码</code>
<button class="copy-code-btn" onclick="copyText('curl http://localhost:8000/api/info/分享码')">📋</button>
</div>
<p class="help-desc">查看文件名、大小、类型和过期时间</p>
</div>
<div class="help-group">
<h4>📊 列出所有分享</h4>
<div class="code-block">
<code>curl http://localhost:8000/api/shares</code>
<button class="copy-code-btn" onclick="copyText('curl http://localhost:8000/api/shares')">📋</button>
</div>
<p class="help-desc">查看当前所有有效的分享</p>
</div>
<div class="help-group">
<h4>🎯 实用示例</h4>
<div class="code-blocks">
<div class="code-block">
<span class="code-label">上传图片:</span>
<code>curl -X POST -F "file=@photo.jpg" http://localhost:8000/api/upload</code>
<button class="copy-code-btn" onclick="copyText('curl -X POST -F \"file=@photo.jpg\" http://localhost:8000/api/upload')">📋</button>
</div>
<div class="code-block">
<span class="code-label">分享代码:</span>
<code>curl -X POST --data "#!/bin/bash
echo Hello" http://localhost:8000/api/text</code>
<button class="copy-code-btn" onclick="copyText('curl -X POST --data \"#!/bin/bash\necho Hello\" http://localhost:8000/api/text')">📋</button>
</div>
<div class="code-block">
<span class="code-label">显示进度:</span>
<code>curl --progress-bar -X POST -F "file=@largefile.zip" http://localhost:8000/api/upload</code>
<button class="copy-code-btn" onclick="copyText('curl --progress-bar -X POST -F \"file=@largefile.zip\" http://localhost:8000/api/upload')">📋</button>
</div>
</div>
</div>
</div>
</div>
</main>
<footer>
<div class="footer-content">
<p>&copy; 2024 文件传输服务 | 开源项目</p>
<div class="footer-links">
<a href="/docs" target="_blank">📖 API文档</a>
<a href="https://github.com" target="_blank">⭐ GitHub</a>
</div>
</div>
</footer>
<!-- 通知提示 -->
<div class="toast" id="toast" style="display: none;">
<span class="toast-message" id="toastMessage">操作成功!</span>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>

829
static/style.css Normal file
View File

@ -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: '✅';
}

84
test_server.py Executable file
View File

@ -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)

124
verify_setup.sh Executable file
View File

@ -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