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