|
1 | 1 | package ftp |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "crypto/md5" |
4 | 5 | "fmt" |
5 | 6 | "io" |
6 | 7 | "net/http" |
7 | 8 | "path/filepath" |
8 | 9 | "strconv" |
| 10 | + "sync" |
9 | 11 | "time" |
10 | 12 |
|
11 | 13 | "github.com/gin-contrib/cors" |
12 | 14 | "github.com/gin-gonic/gin" |
13 | 15 | "github.com/jlaffaye/ftp" |
14 | 16 | ) |
15 | 17 |
|
| 18 | +var ( |
| 19 | + downloadTokens = make(map[string]FTPConfig) |
| 20 | + tokenMutex sync.RWMutex |
| 21 | +) |
| 22 | + |
| 23 | +type FTPConfig struct { |
| 24 | + Host string |
| 25 | + Port int |
| 26 | + Username string |
| 27 | + Password string |
| 28 | + Path string |
| 29 | + Created time.Time |
| 30 | +} |
| 31 | + |
| 32 | +// 清理过期的令牌 |
| 33 | +func cleanupTokens() { |
| 34 | + tokenMutex.Lock() |
| 35 | + defer tokenMutex.Unlock() |
| 36 | + |
| 37 | + now := time.Now() |
| 38 | + for token, config := range downloadTokens { |
| 39 | + if now.Sub(config.Created) > 10*time.Minute { // 10分钟过期 |
| 40 | + delete(downloadTokens, token) |
| 41 | + } |
| 42 | + } |
| 43 | +} |
| 44 | + |
16 | 45 | func connectFTP(host string, port int, username, password string) (*ftp.ServerConn, error) { |
17 | 46 | address := fmt.Sprintf("%s:%d", host, port) |
18 | 47 | conn, err := ftp.Dial(address, ftp.DialWithTimeout(5*time.Second)) |
@@ -157,6 +186,120 @@ func StartWebFTP() { |
157 | 186 | } |
158 | 187 | }) |
159 | 188 |
|
| 189 | + // 生成下载令牌 |
| 190 | + r.POST("/api/ftp/generate-download-url", func(c *gin.Context) { |
| 191 | + var req struct { |
| 192 | + Host string `json:"host"` |
| 193 | + Port int `json:"port"` |
| 194 | + Username string `json:"username"` |
| 195 | + Password string `json:"password"` |
| 196 | + Path string `json:"path"` |
| 197 | + } |
| 198 | + if err := c.ShouldBindJSON(&req); err != nil { |
| 199 | + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |
| 200 | + return |
| 201 | + } |
| 202 | + |
| 203 | + // 验证FTP连接 |
| 204 | + conn, err := connectFTP(req.Host, req.Port, req.Username, req.Password) |
| 205 | + if err != nil { |
| 206 | + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
| 207 | + return |
| 208 | + } |
| 209 | + defer conn.Quit() |
| 210 | + |
| 211 | + // 验证文件存在 |
| 212 | + _, err = conn.FileSize(req.Path) |
| 213 | + if err != nil { |
| 214 | + c.JSON(http.StatusInternalServerError, gin.H{"error": "文件不存在: " + err.Error()}) |
| 215 | + return |
| 216 | + } |
| 217 | + |
| 218 | + // 生成唯一令牌 |
| 219 | + token := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s%d%s%s%s%d", |
| 220 | + req.Host, req.Port, req.Username, req.Password, req.Path, time.Now().UnixNano())))) |
| 221 | + |
| 222 | + // 存储令牌信息 |
| 223 | + tokenMutex.Lock() |
| 224 | + downloadTokens[token] = FTPConfig{ |
| 225 | + Host: req.Host, |
| 226 | + Port: req.Port, |
| 227 | + Username: req.Username, |
| 228 | + Password: req.Password, |
| 229 | + Path: req.Path, |
| 230 | + Created: time.Now(), |
| 231 | + } |
| 232 | + tokenMutex.Unlock() |
| 233 | + |
| 234 | + // 定期清理令牌 |
| 235 | + go cleanupTokens() |
| 236 | + |
| 237 | + downloadURL := fmt.Sprintf("http://127.0.0.1:52869/api/ftp/direct-download?token=%s", token) |
| 238 | + |
| 239 | + c.JSON(http.StatusOK, gin.H{"downloadUrl": downloadURL}) |
| 240 | + }) |
| 241 | + |
| 242 | + // 直接下载接口 |
| 243 | + r.GET("/api/ftp/direct-download", func(c *gin.Context) { |
| 244 | + token := c.Query("token") |
| 245 | + if token == "" { |
| 246 | + c.JSON(http.StatusBadRequest, gin.H{"error": "缺少下载令牌"}) |
| 247 | + return |
| 248 | + } |
| 249 | + |
| 250 | + // 获取令牌对应的配置 |
| 251 | + tokenMutex.RLock() |
| 252 | + config, exists := downloadTokens[token] |
| 253 | + tokenMutex.RUnlock() |
| 254 | + |
| 255 | + if !exists { |
| 256 | + c.JSON(http.StatusBadRequest, gin.H{"error": "下载令牌无效或已过期"}) |
| 257 | + return |
| 258 | + } |
| 259 | + |
| 260 | + // 连接FTP |
| 261 | + conn, err := connectFTP(config.Host, config.Port, config.Username, config.Password) |
| 262 | + if err != nil { |
| 263 | + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
| 264 | + return |
| 265 | + } |
| 266 | + defer conn.Quit() |
| 267 | + |
| 268 | + // 获取文件信息 |
| 269 | + size, err := conn.FileSize(config.Path) |
| 270 | + if err != nil { |
| 271 | + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取文件大小失败: " + err.Error()}) |
| 272 | + return |
| 273 | + } |
| 274 | + |
| 275 | + // 开始下载 |
| 276 | + resp, err := conn.Retr(config.Path) |
| 277 | + if err != nil { |
| 278 | + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
| 279 | + return |
| 280 | + } |
| 281 | + defer resp.Close() |
| 282 | + |
| 283 | + // 设置响应头 |
| 284 | + filename := filepath.Base(config.Path) |
| 285 | + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) |
| 286 | + c.Header("Content-Type", "application/octet-stream") |
| 287 | + c.Header("Content-Length", fmt.Sprintf("%d", size)) |
| 288 | + c.Status(http.StatusOK) |
| 289 | + |
| 290 | + // 流式传输 |
| 291 | + buf := make([]byte, 32*1024) |
| 292 | + _, err = io.CopyBuffer(c.Writer, io.LimitReader(resp, size), buf) |
| 293 | + if err != nil && err != io.EOF { |
| 294 | + fmt.Println("下载中断:", err) |
| 295 | + } |
| 296 | + |
| 297 | + // 下载完成后删除令牌(可选) |
| 298 | + tokenMutex.Lock() |
| 299 | + delete(downloadTokens, token) |
| 300 | + tokenMutex.Unlock() |
| 301 | + }) |
| 302 | + |
160 | 303 | // 删除文件或目录 |
161 | 304 | r.POST("/api/ftp/delete", func(c *gin.Context) { |
162 | 305 | var req struct { |
|
0 commit comments