feat:环境变量处理

This commit is contained in:
MatrixSeven
2025-08-01 19:38:59 +08:00
parent 664fe2fdaa
commit dbfdbf0116
15 changed files with 396 additions and 1035 deletions

View File

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

@@ -1,455 +1,63 @@
# 传传传 - 跨平台文件传输工具
# 文件快传 - P2P文件传输工具
> 简单、快速、安全的点对点文件传输解决方案
![项目演示](img.png)
> 安全、快速、简单的点对点文件传输解决方案 - 无需注册,即传即用
## ✨ 核心功能
- 📁 **文件传输** - 支持多文件同时传输基于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
View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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,
}

View 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
View File

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

View File

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

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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