mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-04 03:25:03 +08:00
feat:环境变量处理
This commit is contained in:
53
Dockerfile
53
Dockerfile
@@ -1,53 +0,0 @@
|
||||
# 多阶段构建 - 构建阶段
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的工具
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
# 复制go.mod和go.sum文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用程序
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o main cmd/main.go
|
||||
|
||||
# 运行阶段
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装ca-certificates用于HTTPS请求
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/main .
|
||||
|
||||
# 复制静态文件
|
||||
COPY --from=builder /app/web ./web
|
||||
|
||||
# 创建必要的目录
|
||||
RUN mkdir -p uploads logs
|
||||
|
||||
# 设置权限
|
||||
RUN chmod +x ./main
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
|
||||
|
||||
# 运行应用程序
|
||||
CMD ["./main"]
|
||||
464
README.md
464
README.md
@@ -1,455 +1,63 @@
|
||||
# 传传传 - 跨平台文件传输工具
|
||||
# 文件快传 - P2P文件传输工具
|
||||
|
||||
> 简单、快速、安全的点对点文件传输解决方案
|
||||

|
||||
|
||||
> 安全、快速、简单的点对点文件传输解决方案 - 无需注册,即传即用
|
||||
|
||||
## ✨ 核心功能
|
||||
|
||||
- 📁 **文件传输** - 支持多文件同时传输,基于WebRTC的P2P技术
|
||||
- 📝 **文字传输** - 快速分享文本内容,支持大文本传输
|
||||
- 🖥️ **桌面共享** - 实时屏幕共享功能(开发中)
|
||||
- 🔗 **URL路由** - 支持直链分享特定功能和模式
|
||||
|
||||
## 🛡️ 安全特性
|
||||
|
||||
- **端到端加密** - WebRTC内置加密,数据传输安全
|
||||
- **无文件存储** - 服务器不存储任何文件内容
|
||||
- **临时连接** - 传输完成后自动清理连接
|
||||
- **房间隔离** - 每个取件码对应独立的传输房间
|
||||
- 📁 **文件传输** - 支持多文件同时传输,基于WebRTC的P2P直连
|
||||
- 📝 **文字传输** - 快速分享文本内容
|
||||
- 🖥️ **桌面共享** - 实时屏幕共享(开发中)
|
||||
- 🔒 **端到端加密** - 数据传输安全,服务器不存储文件
|
||||
- 📱 **响应式设计** - 完美适配手机、平板、电脑
|
||||
|
||||
## 🚀 技术栈
|
||||
|
||||
**前端架构**
|
||||
- Next.js 15 + React 18 + TypeScript
|
||||
- Tailwind CSS + 毛玻璃效果UI
|
||||
- WebRTC DataChannel + WebSocket
|
||||
**前端** - Next.js 15 + React 18 + TypeScript + Tailwind CSS
|
||||
**后端** - Go + WebSocket + 内存存储
|
||||
**传输** - WebRTC DataChannel + P2P直连
|
||||
|
||||
**后端架构**
|
||||
- Go + Gin框架 + WebSocket
|
||||
- 内存存储 + 房间管理
|
||||
- Docker容器化部署
|
||||
|
||||
## 📦 快速开始
|
||||
|
||||
### 方式一:Docker一键部署(推荐[未变写完成])
|
||||
## 📦 快速部署
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MatrixSeven/file-transfer-go.git
|
||||
cd file-transfer-go
|
||||
docker-compose up -d
|
||||
|
||||
# 访问应用
|
||||
open http://localhost:8080
|
||||
```
|
||||
|
||||
### 方式二:本地开发
|
||||
访问 http://localhost:8080 开始使用
|
||||
|
||||
## 🎯 使用方法
|
||||
|
||||
**发送文件**
|
||||
1. 选择文件 → 生成取件码 → 分享6位码
|
||||
|
||||
**接收文件**
|
||||
1. 输入取件码 → 自动连接 → 下载文件
|
||||
|
||||
## 📊 项目架构
|
||||
|
||||
```
|
||||
发送方 ←─── WebSocket信令 ───→ 服务器 ←─── WebSocket信令 ───→ 接收方
|
||||
│ │
|
||||
└────────────── WebRTC P2P直连传输 ──────────────────────────┘
|
||||
```
|
||||
|
||||
## 🛠️ 本地开发
|
||||
|
||||
```bash
|
||||
# 1. 启动后端服务
|
||||
# 后端
|
||||
make dev
|
||||
|
||||
# 2. 启动前端服务
|
||||
cd chuan-next
|
||||
yarn
|
||||
yarn dev
|
||||
|
||||
# 访问应用
|
||||
open http://localhost:3000
|
||||
# 前端
|
||||
cd chuan-next && yarn && yarn dev
|
||||
```
|
||||
|
||||
## 🎯 URL路由支持
|
||||
|
||||
支持通过URL参数直接跳转到特定功能:
|
||||
|
||||
```bash
|
||||
# 文件传输
|
||||
/?type=file&mode=send # 发送文件
|
||||
/?type=file&mode=receive # 接收文件
|
||||
|
||||
# 文字传输
|
||||
/?type=text&mode=send # 发送文字
|
||||
/?type=text&mode=receive # 接收文字
|
||||
|
||||
# 桌面共享
|
||||
/?type=desktop&mode=send # 共享桌面
|
||||
/?type=desktop&mode=receive # 观看桌面
|
||||
```
|
||||
|
||||
## 🌟 项目特色
|
||||
|
||||
- ⚡ **零配置** - 无需注册登录,即开即用
|
||||
- 🔒 **点对点** - 基于WebRTC的直接传输,服务器仅做信令
|
||||
- 📱 **响应式** - 完美适配手机、平板、电脑
|
||||
- <20> **现代UI** - 精美的毛玻璃效果,流畅的动画
|
||||
- 🚀 **高性能** - 64KB分块传输,支持大文件高速传输
|
||||
|
||||
## 📊 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────┐ WebSocket ┌──────────────┐ WebSocket ┌─────────────────┐
|
||||
│ 发送方 (A) │ ←──────────────→ │ 信令服务器 │ ←──────────────→ │ 接收方 (B) │
|
||||
│ │ │ │ │ │
|
||||
│ - 选择文件 │ │ - 房间管理 │ │ - 输入取件码 │
|
||||
│ - 生成取件码 │ │ - 信令转发 │ │ - 获取文件列表 │
|
||||
│ - 等待连接 │ │ - 状态同步 │ │ - 下载文件 │
|
||||
└─────────────────┘ └──────────────┘ └─────────────────┘
|
||||
│ │
|
||||
│ WebRTC P2P │
|
||||
│ ┌─────────────────┐ │
|
||||
└────────────────────→│ 直接文件传输 │←──────────────────────────────┘
|
||||
│ │
|
||||
│ - 端到端加密 │
|
||||
│ - 高速传输 │
|
||||
│ - 断点续传 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
.
|
||||
├── cmd/ # Go应用入口
|
||||
├── internal/ # Go后端核心代码
|
||||
│ ├── handlers/ # HTTP和WebSocket处理器
|
||||
│ ├── models/ # 数据模型
|
||||
│ └── services/ # 业务服务层
|
||||
├── chuan-next/ # Next.js前端应用
|
||||
│ ├── src/app/ # 应用页面
|
||||
│ ├── src/components/ # 组件库
|
||||
│ └── src/hooks/ # React Hooks
|
||||
├── web/ # 静态资源(测试页面)
|
||||
├── docker-compose.yml # Docker部署配置
|
||||
└── Makefile # 构建脚本
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎提交Issue和Pull Request来帮助改进项目!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
│ 发送方浏览器 │◄────────┤ 信令服务器 ├────────►│ 接收方浏览器 │
|
||||
│ │ │ │ │ │
|
||||
│ ┌───────────┐ │ │ ┌──────────┐ │ │ ┌───────────┐ │
|
||||
│ │ 文件选择 │ │ │ │ WebSocket│ │ │ │ 取件码输入│ │
|
||||
│ └───────────┘ │ │ │ 信令 │ │ │ └───────────┘ │
|
||||
│ ┌───────────┐ │ │ └──────────┘ │ │ ┌───────────┐ │
|
||||
│ │ 生成取件码│ │ │ ┌──────────┐ │ │ │ 文件接收 │ │
|
||||
│ └───────────┘ │ │ │ 房间管理 │ │ │ └───────────┘ │
|
||||
│ │ │ └──────────┘ │ │ │
|
||||
└─────────────────┘ └──────────────┘ └─────────────────┘
|
||||
│ │
|
||||
└─────────────────── WebRTC P2P 连接 ──────────────────┘
|
||||
(文件直接传输)
|
||||
```
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 后端
|
||||
- **Go 1.21+**:高性能Web服务器
|
||||
- **Chi Router**:轻量级HTTP路由
|
||||
- **Gorilla WebSocket**:WebSocket连接管理
|
||||
- **标准库**:HTML模板、JSON处理等
|
||||
|
||||
### 前端
|
||||
- **原生JavaScript**:无框架依赖
|
||||
- **WebRTC API**:浏览器P2P通信
|
||||
- **Tailwind CSS**:现代化UI样式
|
||||
- **模块化设计**:分离的JS文件结构
|
||||
|
||||
### 基础设施
|
||||
- **Docker支持**:容器化部署
|
||||
- **Nginx代理**:生产环境反向代理
|
||||
- **STUN服务器**:NAT穿透支持
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
chuan/
|
||||
├── cmd/
|
||||
│ └── main.go # 程序入口
|
||||
├── internal/
|
||||
│ ├── handlers/
|
||||
│ │ └── handlers.go # HTTP请求处理
|
||||
│ ├── models/
|
||||
│ │ └── models.go # 数据模型定义
|
||||
│ └── services/
|
||||
│ ├── file_service.go # 文件服务(预留)
|
||||
│ ├── memory_store.go # 内存存储
|
||||
│ ├── p2p_service.go # P2P连接管理
|
||||
│ └── webrtc_service.go # WebRTC服务(预留)
|
||||
├── web/
|
||||
│ ├── static/
|
||||
│ │ ├── css/
|
||||
│ │ │ └── style.css # 样式文件
|
||||
│ │ └── js/
|
||||
│ │ ├── common.js # 通用工具函数
|
||||
│ │ ├── p2p-transfer.js # P2P传输核心逻辑
|
||||
│ │ ├── webrtc-connection.js # WebRTC连接管理
|
||||
│ │ └── file-transfer.js # 文件传输处理
|
||||
│ └── templates/
|
||||
│ ├── base.html # 基础模板
|
||||
│ ├── index.html # 主页面
|
||||
│ ├── upload.html # 上传页面(预留)
|
||||
│ └── video.html # 视频传输页面
|
||||
├── uploads/ # 上传目录(预留)
|
||||
├── bin/
|
||||
│ └── chuan # 编译后的可执行文件
|
||||
├── docker-compose.yml # Docker Compose配置
|
||||
├── Dockerfile # Docker镜像构建
|
||||
├── nginx.conf # Nginx配置
|
||||
├── Makefile # 构建脚本
|
||||
├── deploy.sh # 部署脚本
|
||||
├── go.mod # Go模块定义
|
||||
├── go.sum # Go模块校验
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd chuan
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
3. **启动服务**
|
||||
```bash
|
||||
go run cmd/main.go
|
||||
```
|
||||
|
||||
4. **访问应用**
|
||||
```
|
||||
打开浏览器访问: http://localhost:8080
|
||||
```
|
||||
|
||||
### 使用Make命令
|
||||
|
||||
```bash
|
||||
# 运行开发服务器
|
||||
make run
|
||||
|
||||
# 构建可执行文件
|
||||
make build
|
||||
|
||||
# 清理构建文件
|
||||
make clean
|
||||
|
||||
# 运行测试
|
||||
make test
|
||||
```
|
||||
|
||||
### Docker部署
|
||||
|
||||
1. **构建镜像**
|
||||
```bash
|
||||
docker build -t chuan .
|
||||
```
|
||||
|
||||
2. **运行容器**
|
||||
```bash
|
||||
docker run -p 8080:8080 chuan
|
||||
```
|
||||
|
||||
3. **使用Docker Compose**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 📖 使用说明
|
||||
|
||||
### 发送文件
|
||||
|
||||
1. 访问主页面
|
||||
2. 点击选择文件或拖拽文件到上传区域
|
||||
3. 点击"生成取件码"按钮
|
||||
4. 分享6位取件码给接收方
|
||||
5. 等待接收方连接并开始传输
|
||||
|
||||
### 接收文件
|
||||
|
||||
1. 访问主页面
|
||||
2. 在"输入取件码"区域输入6位取件码
|
||||
3. 系统自动连接并显示文件列表
|
||||
4. 等待P2P连接建立(显示绿色状态)
|
||||
5. 点击"下载"按钮开始接收文件
|
||||
|
||||
### 传输状态说明
|
||||
|
||||
- 🟡 **等待连接**:正在建立WebSocket连接
|
||||
- 🟢 **P2P已连接**:可以开始文件传输
|
||||
- 🔴 **连接失败**:请检查网络或重试
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
- `PORT`:服务器端口(默认8080)
|
||||
- `HOST`:服务器主机(默认localhost)
|
||||
|
||||
### STUN服务器配置
|
||||
|
||||
系统默认使用以下STUN服务器(按优先级):
|
||||
|
||||
1. `stun:stun.chat.bilibili.com:3478` - 哔哩哔哩
|
||||
2. `stun:stun.voipbuster.com` - VoIP服务
|
||||
3. `stun:stun.voipstunt.com` - VoIP服务
|
||||
4. `stun:stun.qq.com:3478` - 腾讯QQ
|
||||
5. `stun:stun.l.google.com:19302` - Google(备用)
|
||||
|
||||
## 🔧 高级配置
|
||||
|
||||
### 传输参数优化
|
||||
|
||||
在 `file-transfer.js` 中可调整以下参数:
|
||||
|
||||
```javascript
|
||||
const chunkSize = 65536; // 分块大小(64KB)
|
||||
const transmissionDelay = 1; // 传输间隔(1ms)
|
||||
const maxRetransmits = 3; // 最大重传次数
|
||||
const maxPacketLifeTime = 3000; // 数据包最大生存时间
|
||||
```
|
||||
|
||||
### 连接超时设置
|
||||
|
||||
在 `webrtc-connection.js` 中可调整:
|
||||
|
||||
```javascript
|
||||
const connectionTimeout = 60000; // 连接超时(60秒)
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **P2P连接失败**
|
||||
- 检查防火墙设置
|
||||
- 确认浏览器支持WebRTC
|
||||
- 尝试使用不同的STUN服务器
|
||||
|
||||
2. **文件传输中断**
|
||||
- 检查网络连接稳定性
|
||||
- 避免浏览器标签页切换到后台
|
||||
- 大文件传输建议分批进行
|
||||
|
||||
3. **取件码无效**
|
||||
- 确认取件码输入正确(6位大写字母数字)
|
||||
- 检查房间是否已过期(默认1小时)
|
||||
- 确认发送方仍在线
|
||||
|
||||
### 调试模式
|
||||
|
||||
打开浏览器开发者工具(F12),查看Console标签页获取详细日志信息。
|
||||
|
||||
## 🚀 生产部署
|
||||
|
||||
### Nginx配置
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### systemd服务
|
||||
|
||||
创建 `/etc/systemd/system/chuan.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Chuan P2P File Transfer Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/opt/chuan
|
||||
ExecStart=/opt/chuan/bin/chuan
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启动服务:
|
||||
```bash
|
||||
sudo systemctl enable chuan
|
||||
sudo systemctl start chuan
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 开启 Pull Request
|
||||
|
||||
## 📝 开发计划
|
||||
|
||||
### v1.1 计划功能
|
||||
- [ ] 文件传输加密增强
|
||||
- [ ] 传输速度优化
|
||||
- [ ] 移动端适配改进
|
||||
- [ ] 批量文件操作
|
||||
|
||||
### v1.2 计划功能
|
||||
- [ ] 用户认证系统
|
||||
- [ ] 传输历史记录
|
||||
- [ ] 文件预览功能
|
||||
- [ ] API接口开放
|
||||
|
||||
### v2.0 计划功能
|
||||
- [ ] 视频通话功能完善
|
||||
- [ ] 屏幕共享支持
|
||||
- [ ] 多人会议室
|
||||
- [ ] 云存储集成
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
- [WebRTC](https://webrtc.org/) - 实时通信技术
|
||||
- [Go](https://golang.org/) - 后端开发语言
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - UI样式框架
|
||||
- [Chi Router](https://go-chi.io/) - Go HTTP路由库
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
- 项目主页:[GitHub Repository]
|
||||
- 问题反馈:[GitHub Issues]
|
||||
|
||||
---
|
||||
|
||||
⭐ 如果这个项目对你有帮助,请给它一个星标!
|
||||
⭐ 觉得有用请给个星标!
|
||||
|
||||
40
chuan-next/next.config.js
Normal file
40
chuan-next/next.config.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// 环境变量配置
|
||||
env: {
|
||||
GO_BACKEND_URL: process.env.GO_BACKEND_URL,
|
||||
},
|
||||
|
||||
// 公共运行时配置
|
||||
publicRuntimeConfig: {
|
||||
apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
|
||||
wsUrl: process.env.NEXT_PUBLIC_WS_URL,
|
||||
},
|
||||
|
||||
// 服务器端运行时配置
|
||||
serverRuntimeConfig: {
|
||||
goBackendUrl: process.env.GO_BACKEND_URL,
|
||||
},
|
||||
|
||||
// 重写规则 - 可选,用于代理API请求
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/proxy/:path*',
|
||||
destination: `${process.env.GO_BACKEND_URL}/api/:path*`,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
// 输出配置
|
||||
output: 'standalone',
|
||||
|
||||
// 实验性功能
|
||||
experimental: {
|
||||
serverActions: {
|
||||
allowedOrigins: ['localhost:3000', 'localhost:8080'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -4,9 +4,15 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev:prod": "NODE_ENV=production next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:dev": "NODE_ENV=development next build",
|
||||
"build:prod": "NODE_ENV=production next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"start:dev": "NODE_ENV=development next start",
|
||||
"start:prod": "NODE_ENV=production next start",
|
||||
"lint": "next lint",
|
||||
"env:check": "node -e \"console.log('Environment:', process.env.NODE_ENV); console.log('GO_BACKEND_URL:', process.env.GO_BACKEND_URL);\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Hero from '@/components/Hero';
|
||||
import FileTransfer from '@/components/FileTransfer';
|
||||
import TextTransfer from '@/components/TextTransfer';
|
||||
@@ -31,6 +33,10 @@ export default function HomePage() {
|
||||
// URL参数管理
|
||||
const [activeTab, setActiveTab] = useState<'file' | 'text' | 'desktop'>('file');
|
||||
|
||||
// 确认对话框状态
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [pendingTabSwitch, setPendingTabSwitch] = useState<string>('');
|
||||
|
||||
// 从URL参数中获取初始状态
|
||||
useEffect(() => {
|
||||
const type = searchParams.get('type') as 'file' | 'text' | 'desktop';
|
||||
@@ -81,49 +87,37 @@ export default function HomePage() {
|
||||
const hasActiveConnection = isConnected || pickupCode || isConnecting;
|
||||
|
||||
if (hasActiveConnection && value !== activeTab) {
|
||||
// 如果已有活跃连接且要切换到不同的tab,在新窗口打开
|
||||
const currentUrl = window.location.origin + window.location.pathname;
|
||||
const newUrl = `${currentUrl}?type=${value}`;
|
||||
|
||||
// 在新标签页打开
|
||||
window.open(newUrl, '_blank');
|
||||
|
||||
// 给出提示
|
||||
let currentMode = '';
|
||||
let targetMode = '';
|
||||
|
||||
switch (activeTab) {
|
||||
case 'file':
|
||||
currentMode = '文件传输';
|
||||
break;
|
||||
case 'text':
|
||||
currentMode = '文字传输';
|
||||
break;
|
||||
case 'desktop':
|
||||
currentMode = '桌面共享';
|
||||
break;
|
||||
}
|
||||
|
||||
switch (value) {
|
||||
case 'file':
|
||||
targetMode = '文件传输';
|
||||
break;
|
||||
case 'text':
|
||||
targetMode = '文字传输';
|
||||
break;
|
||||
case 'desktop':
|
||||
targetMode = '桌面共享';
|
||||
break;
|
||||
}
|
||||
|
||||
showNotification(`当前${currentMode}会话进行中,已在新标签页打开${targetMode}`, 'info');
|
||||
// 如果已有活跃连接且要切换到不同的tab,显示确认对话框
|
||||
setPendingTabSwitch(value);
|
||||
setShowConfirmDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有活跃连接,正常切换
|
||||
setActiveTab(value as 'file' | 'text' | 'desktop');
|
||||
updateUrlParams(value);
|
||||
}, [updateUrlParams, isConnected, pickupCode, isConnecting, activeTab, showNotification]);
|
||||
}, [updateUrlParams, isConnected, pickupCode, isConnecting, activeTab]);
|
||||
|
||||
// 确认切换tab
|
||||
const confirmTabSwitch = useCallback(() => {
|
||||
if (pendingTabSwitch) {
|
||||
const currentUrl = window.location.origin + window.location.pathname;
|
||||
const newUrl = `${currentUrl}?type=${pendingTabSwitch}`;
|
||||
|
||||
// 在新标签页打开
|
||||
window.open(newUrl, '_blank');
|
||||
|
||||
// 关闭对话框并清理状态
|
||||
setShowConfirmDialog(false);
|
||||
setPendingTabSwitch('');
|
||||
}
|
||||
}, [pendingTabSwitch]);
|
||||
|
||||
// 取消切换tab
|
||||
const cancelTabSwitch = useCallback(() => {
|
||||
setShowConfirmDialog(false);
|
||||
setPendingTabSwitch('');
|
||||
}, []);
|
||||
|
||||
// 初始化文件传输
|
||||
const initFileTransfer = useCallback((fileInfo: any) => {
|
||||
@@ -811,6 +805,55 @@ export default function HomePage() {
|
||||
<div className="h-8 sm:h-16"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 确认对话框 */}
|
||||
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>切换传输模式</DialogTitle>
|
||||
<DialogDescription>
|
||||
{(() => {
|
||||
let currentMode = '';
|
||||
let targetMode = '';
|
||||
|
||||
switch (activeTab) {
|
||||
case 'file':
|
||||
currentMode = '文件传输';
|
||||
break;
|
||||
case 'text':
|
||||
currentMode = '文字传输';
|
||||
break;
|
||||
case 'desktop':
|
||||
currentMode = '桌面共享';
|
||||
break;
|
||||
}
|
||||
|
||||
switch (pendingTabSwitch) {
|
||||
case 'file':
|
||||
targetMode = '文件传输';
|
||||
break;
|
||||
case 'text':
|
||||
targetMode = '文字传输';
|
||||
break;
|
||||
case 'desktop':
|
||||
targetMode = '桌面共享';
|
||||
break;
|
||||
}
|
||||
|
||||
return `当前${currentMode}会话进行中,是否要在新标签页中打开${targetMode}?`;
|
||||
})()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={cancelTabSwitch}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={confirmTabSwitch}>
|
||||
确认打开
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GO_BACKEND_URL = process.env.GO_BACKEND_URL || 'http://localhost:8080';
|
||||
import { getBackendUrl } from '@/lib/config';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// 使用配置管理获取后端URL
|
||||
const backendUrl = getBackendUrl('/api/create-room');
|
||||
|
||||
// 转发请求到Go后端
|
||||
const response = await fetch(`${GO_BACKEND_URL}/api/create-room`, {
|
||||
const response = await fetch(backendUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getBackendUrl } from '@/lib/config';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -13,7 +14,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
// 调用后端API创建文字传输房间
|
||||
const response = await fetch('http://localhost:8080/api/create-text-room', {
|
||||
const response = await fetch(getBackendUrl('/api/create-text-room'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GO_BACKEND_URL = process.env.GO_BACKEND_URL || 'http://localhost:8080';
|
||||
import { getBackendUrl } from '@/lib/config';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -15,7 +14,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 转发请求到Go后端
|
||||
const response = await fetch(`${GO_BACKEND_URL}/api/room-info?code=${code}`, {
|
||||
const response = await fetch(getBackendUrl(`/api/room-info?code=${code}`), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -6,25 +6,35 @@ import { Github } from 'lucide-react';
|
||||
export default function Hero() {
|
||||
return (
|
||||
<div className="text-center mb-8 sm:mb-12 animate-fade-in-up">
|
||||
<div className="flex items-center justify-center space-x-3 mb-4">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
|
||||
文件快传
|
||||
</h1>
|
||||
<a
|
||||
href="https://github.com/MatrixSeven/file-transfer-go"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center space-x-1 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-sm rounded-full transition-colors duration-200 hover:scale-105 transform"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">开源</span>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-base sm:text-lg text-slate-600 max-w-2xl mx-auto leading-relaxed px-4">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent mb-4">
|
||||
文件快传
|
||||
</h1>
|
||||
<p className="text-base sm:text-lg text-slate-600 max-w-2xl mx-auto leading-relaxed px-4 mb-4">
|
||||
安全、快速、简单的传输服务
|
||||
<br />
|
||||
<span className="text-sm sm:text-base text-slate-500">支持文件、文字、桌面共享 - 无需注册,即传即用</span>
|
||||
</p>
|
||||
|
||||
{/* GitHub开源链接 */}
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<a
|
||||
href="https://github.com/MatrixSeven/file-transfer-go"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center space-x-2 px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 text-sm rounded-lg transition-all duration-200 hover:scale-105 transform border border-slate-200 hover:border-slate-300"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
<span>开源项目</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/MatrixSeven/file-transfer-go"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-slate-400 font-mono hover:text-slate-600 transition-colors duration-200 hover:underline"
|
||||
>
|
||||
https://github.com/MatrixSeven/file-transfer-go
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
122
chuan-next/src/components/ui/dialog.tsx
Normal file
122
chuan-next/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
77
chuan-next/src/lib/config.ts
Normal file
77
chuan-next/src/lib/config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 环境配置管理
|
||||
*/
|
||||
|
||||
export const config = {
|
||||
// 环境判断
|
||||
isDev: process.env.NODE_ENV === 'development',
|
||||
isProd: process.env.NODE_ENV === 'production',
|
||||
|
||||
// API配置
|
||||
api: {
|
||||
// 后端API地址 (服务器端使用)
|
||||
backendUrl: process.env.GO_BACKEND_URL || 'http://localhost:8080',
|
||||
|
||||
// 前端API基础URL (客户端使用)
|
||||
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
|
||||
|
||||
// WebSocket地址
|
||||
wsUrl: process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8080/ws',
|
||||
},
|
||||
|
||||
// 超时配置
|
||||
timeout: {
|
||||
api: 30000, // 30秒
|
||||
ws: 60000, // 60秒
|
||||
},
|
||||
|
||||
// 重试配置
|
||||
retry: {
|
||||
max: 3,
|
||||
delay: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后端API完整URL
|
||||
* @param path API路径
|
||||
* @returns 完整的API URL
|
||||
*/
|
||||
export function getBackendUrl(path: string): string {
|
||||
const baseUrl = config.api.backendUrl.replace(/\/$/, '')
|
||||
const apiPath = path.startsWith('/') ? path : `/${path}`
|
||||
return `${baseUrl}${apiPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前端API完整URL
|
||||
* @param path API路径
|
||||
* @returns 完整的API URL
|
||||
*/
|
||||
export function getApiUrl(path: string): string {
|
||||
const baseUrl = config.api.baseUrl.replace(/\/$/, '')
|
||||
const apiPath = path.startsWith('/') ? path : `/${path}`
|
||||
return `${baseUrl}${apiPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取WebSocket URL
|
||||
* @returns WebSocket连接地址
|
||||
*/
|
||||
export function getWsUrl(): string {
|
||||
return config.api.wsUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* 环境配置调试信息
|
||||
*/
|
||||
export function getEnvInfo() {
|
||||
return {
|
||||
environment: process.env.NODE_ENV,
|
||||
backendUrl: config.api.backendUrl,
|
||||
baseUrl: config.api.baseUrl,
|
||||
wsUrl: config.api.wsUrl,
|
||||
isDev: config.isDev,
|
||||
isProd: config.isProd,
|
||||
}
|
||||
}
|
||||
256
deploy.sh
256
deploy.sh
@@ -1,256 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 文件传输系统部署脚本
|
||||
# 使用方法: ./deploy.sh [环境]
|
||||
# 环境选项: dev, staging, production
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 环境变量
|
||||
ENV=${1:-dev}
|
||||
APP_NAME="chuan"
|
||||
DOCKER_IMAGE="${APP_NAME}:${ENV}"
|
||||
COMPOSE_FILE="docker-compose.yml"
|
||||
|
||||
# 检查Docker和Docker Compose
|
||||
check_dependencies() {
|
||||
log_info "检查依赖..."
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker未安装,请先安装Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
log_error "Docker Compose未安装,请先安装Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "依赖检查完成"
|
||||
}
|
||||
|
||||
# 构建应用
|
||||
build_app() {
|
||||
log_info "构建应用..."
|
||||
|
||||
# 清理旧的构建
|
||||
docker-compose down --remove-orphans
|
||||
docker system prune -f
|
||||
|
||||
# 构建新镜像
|
||||
docker-compose build --no-cache
|
||||
|
||||
log_info "应用构建完成"
|
||||
}
|
||||
|
||||
# 生成SSL证书(开发环境)
|
||||
generate_ssl_cert() {
|
||||
if [ "$ENV" = "dev" ]; then
|
||||
log_info "生成开发环境SSL证书..."
|
||||
|
||||
mkdir -p ssl
|
||||
|
||||
if [ ! -f ssl/cert.pem ] || [ ! -f ssl/key.pem ]; then
|
||||
openssl req -x509 -newkey rsa:4096 -keyout ssl/key.pem -out ssl/cert.pem -days 365 -nodes \
|
||||
-subj "/C=CN/ST=Beijing/L=Beijing/O=Chuan/OU=Dev/CN=localhost"
|
||||
log_info "SSL证书生成完成"
|
||||
else
|
||||
log_info "SSL证书已存在,跳过生成"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 部署应用
|
||||
deploy_app() {
|
||||
log_info "部署应用到${ENV}环境..."
|
||||
|
||||
# 根据环境选择不同的配置
|
||||
case $ENV in
|
||||
"dev")
|
||||
export COMPOSE_FILE="docker-compose.yml"
|
||||
;;
|
||||
"staging")
|
||||
export COMPOSE_FILE="docker-compose.staging.yml"
|
||||
;;
|
||||
"production")
|
||||
export COMPOSE_FILE="docker-compose.prod.yml"
|
||||
;;
|
||||
*)
|
||||
log_error "未知环境: $ENV"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 启动服务
|
||||
docker-compose up -d
|
||||
|
||||
# 等待服务启动
|
||||
log_info "等待服务启动..."
|
||||
sleep 10
|
||||
|
||||
# 健康检查
|
||||
if curl -f http://localhost:8080/health > /dev/null 2>&1; then
|
||||
log_info "应用健康检查通过"
|
||||
else
|
||||
log_warn "应用健康检查失败,请检查日志"
|
||||
fi
|
||||
|
||||
log_info "部署完成"
|
||||
}
|
||||
|
||||
# 显示服务状态
|
||||
show_status() {
|
||||
log_info "服务状态:"
|
||||
docker-compose ps
|
||||
|
||||
log_info "服务日志(最近20行):"
|
||||
docker-compose logs --tail=20
|
||||
}
|
||||
|
||||
# 备份数据
|
||||
backup_data() {
|
||||
log_info "备份数据..."
|
||||
|
||||
BACKUP_DIR="backup/$(date +%Y%m%d_%H%M%S)"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# 备份上传文件
|
||||
if [ -d "uploads" ]; then
|
||||
cp -r uploads "$BACKUP_DIR/"
|
||||
log_info "上传文件已备份到 $BACKUP_DIR/uploads"
|
||||
fi
|
||||
|
||||
# 备份Redis数据
|
||||
docker-compose exec -T redis redis-cli BGSAVE
|
||||
docker cp $(docker-compose ps -q redis):/data/dump.rdb "$BACKUP_DIR/"
|
||||
log_info "Redis数据已备份到 $BACKUP_DIR/dump.rdb"
|
||||
|
||||
log_info "数据备份完成: $BACKUP_DIR"
|
||||
}
|
||||
|
||||
# 恢复数据
|
||||
restore_data() {
|
||||
BACKUP_DIR=$2
|
||||
|
||||
if [ -z "$BACKUP_DIR" ]; then
|
||||
log_error "请指定备份目录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$BACKUP_DIR" ]; then
|
||||
log_error "备份目录不存在: $BACKUP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "从 $BACKUP_DIR 恢复数据..."
|
||||
|
||||
# 恢复上传文件
|
||||
if [ -d "$BACKUP_DIR/uploads" ]; then
|
||||
rm -rf uploads/*
|
||||
cp -r "$BACKUP_DIR/uploads/"* uploads/
|
||||
log_info "上传文件已恢复"
|
||||
fi
|
||||
|
||||
# 恢复Redis数据
|
||||
if [ -f "$BACKUP_DIR/dump.rdb" ]; then
|
||||
docker-compose stop redis
|
||||
docker cp "$BACKUP_DIR/dump.rdb" $(docker-compose ps -q redis):/data/
|
||||
docker-compose start redis
|
||||
log_info "Redis数据已恢复"
|
||||
fi
|
||||
|
||||
log_info "数据恢复完成"
|
||||
}
|
||||
|
||||
# 清理资源
|
||||
cleanup() {
|
||||
log_info "清理资源..."
|
||||
|
||||
docker-compose down --volumes --remove-orphans
|
||||
docker system prune -af
|
||||
docker volume prune -f
|
||||
|
||||
log_info "清理完成"
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
show_help() {
|
||||
echo "文件传输系统部署脚本"
|
||||
echo ""
|
||||
echo "使用方法:"
|
||||
echo " $0 [命令] [环境/参数]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " deploy [env] - 部署应用 (环境: dev, staging, production)"
|
||||
echo " build - 构建应用"
|
||||
echo " status - 显示服务状态"
|
||||
echo " backup - 备份数据"
|
||||
echo " restore [dir] - 恢复数据"
|
||||
echo " cleanup - 清理资源"
|
||||
echo " help - 显示帮助信息"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 deploy dev # 部署到开发环境"
|
||||
echo " $0 deploy production # 部署到生产环境"
|
||||
echo " $0 backup # 备份数据"
|
||||
echo " $0 restore backup/20241128_120000 # 恢复数据"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
case ${1:-deploy} in
|
||||
"deploy")
|
||||
check_dependencies
|
||||
generate_ssl_cert
|
||||
build_app
|
||||
deploy_app
|
||||
show_status
|
||||
;;
|
||||
"build")
|
||||
check_dependencies
|
||||
build_app
|
||||
;;
|
||||
"status")
|
||||
show_status
|
||||
;;
|
||||
"backup")
|
||||
backup_data
|
||||
;;
|
||||
"restore")
|
||||
restore_data $@
|
||||
;;
|
||||
"cleanup")
|
||||
cleanup
|
||||
;;
|
||||
"help"|"-h"|"--help")
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "未知命令: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main $@
|
||||
@@ -1,57 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 主应用服务
|
||||
chuan:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./uploads:/root/uploads
|
||||
- ./logs:/root/logs
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- PORT=8080
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- chuan-network
|
||||
|
||||
# Redis服务(用于存储取件码)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes --requirepass ""
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- chuan-network
|
||||
|
||||
# Nginx反向代理
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./ssl:/etc/nginx/ssl
|
||||
- ./uploads:/var/www/uploads
|
||||
depends_on:
|
||||
- chuan
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- chuan-network
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
chuan-network:
|
||||
driver: bridge
|
||||
181
nginx.conf
181
nginx.conf
@@ -1,181 +0,0 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# 日志格式
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# 基本设置
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/json;
|
||||
|
||||
# 文件上传限制
|
||||
client_max_body_size 64G;
|
||||
client_body_timeout 60s;
|
||||
client_header_timeout 60s;
|
||||
|
||||
# 缓存设置
|
||||
open_file_cache max=10000 inactive=5m;
|
||||
open_file_cache_valid 2m;
|
||||
open_file_cache_min_uses 1;
|
||||
open_file_cache_errors on;
|
||||
|
||||
# 上游服务器
|
||||
upstream chuan_backend {
|
||||
server chuan:8080;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# HTTP服务器(重定向到HTTPS)
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS服务器
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# SSL配置
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# 现代SSL配置
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# 安全头
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
|
||||
# 静态文件缓存
|
||||
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary Accept-Encoding;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 上传文件服务
|
||||
location /uploads/ {
|
||||
alias /var/www/uploads/;
|
||||
expires 24h;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
# 安全设置
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline';";
|
||||
|
||||
# 限制访问
|
||||
valid_referers none blocked server_names;
|
||||
if ($invalid_referer) {
|
||||
return 403;
|
||||
}
|
||||
}
|
||||
|
||||
# WebSocket代理
|
||||
location /ws/ {
|
||||
proxy_pass http://chuan_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# API代理
|
||||
location /api/ {
|
||||
proxy_pass http://chuan_backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# 主应用代理
|
||||
location / {
|
||||
proxy_pass http://chuan_backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 缓存设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# 错误页面
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /404.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user