first commit
This commit is contained in:
commit
4985344911
64
.dockerignore
Normal file
64
.dockerignore
Normal 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
31
.env
Normal 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
31
.env.example
Normal 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
52
Dockerfile
Normal 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
66
Dockerfile.multi
Normal 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
369
README.md
Normal 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
419
app.py
Normal 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
149
build.sh
Executable 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
365
cli.py
Normal 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
71
curl_examples.sh
Executable 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"
|
||||
1
data/uploads/65313572.txt
Normal file
1
data/uploads/65313572.txt
Normal file
@ -0,0 +1 @@
|
||||
Hello World”
|
||||
102
demo.sh
Executable file
102
demo.sh
Executable 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
132
docker-compose.aliyun.yml
Normal 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
88
docker-compose.yml
Normal 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
250
fileshare_functions.sh
Executable 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
127
fix_paths.py
Executable 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
8
requirements.txt
Normal 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
88
start.sh
Executable 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
547
static/app.js
Normal 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
512
static/curl-guide.html
Normal 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
262
static/index.html
Normal 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>© 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
829
static/style.css
Normal 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
84
test_server.py
Executable 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
124
verify_setup.sh
Executable 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
|
||||
Loading…
Reference in New Issue
Block a user