Go+Gin实现安全多文件上传功能
Go+Gin实现安全多文件上传:带MD5校验的完整解决方案
完整代码如下
后端
package main import ( "encoding/json" "fmt" "log" "net/http" "os" "path/filepath" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) // 前端传来的文件元数据 type FileMetaRequest struct { FileName string `json:"fileName" binding:"required"` FileSize int64 `json:"fileSize" binding:"required"` FileType string `json:"fileType" binding:"required"` FileMD5 string `json:"fileMD5" binding:"required"` } // 返回给前端的响应结构 type UploadResponse struct { OriginalName string `json:"originalName"` SavedPath string `json:"savedPath"` ReceivedMD5 string `json:"receivedMD5"` IsVerified bool `json:"isVerified"` // 是否通过验证 } func main() { r := gin.Deandroidfault() // 配置CORS r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"POST"}, })) // 上传目录 uploadDir := "uploads" if _, err := os.Stat(uploadDir); os.IsNotExist(err) { os.Mkdir(uploadDir, 0755) } r.POST("/upload", func(c *gin.Context) { // 1. 获取元数据JSON metaJson := c.PostForm("metadata") var fileMetas []FileMetaRequest if err := json.Unmarshal([]byte(metaJson), &fileMetas); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "元数据解析失败"}) return } // 2. 获取文件 form, err := c.MultipartForm() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "文件获取失败"}) return } files := form.File["files"] // 3. 验证文件数量匹配 if len(files) != len(fileMetas) { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("元数据与文件数量不匹配(元数据:%d 文件:%d)", len(fileMetas), len(files)), }) return } var results []UploadResponse for i, file := range files { meta := fileMetas[i] // 4. 验证基本元数据 if file.Filename != meta.FileName || file.Size != meta.FileSize { results = append(results, UploadResponse{ OriginalName: file.Filename, IsVerified: false, }) continue } // 5. 保存文件 savedName := fmt.Sprintf("%s%s", meta.FileMD5, filepath.Ext(file.Filename)) savePath := filepath.Join(uploadDir, savedName) if err := c.SaveUploadedFile(file, savePath); err != nil { results = append(results, UploadResponse{ OriginalName: file.Filename, IsVerified: false, }) continue } // 6. 记录结果(实际项目中这里应该做MD5校验) results = append(results, UploadResponse{ OriginalName: file.Filename, SavedPath: savePath, ReceivedMD5: meta.FileMD5, IsVerified: true, }) } c.JSON(http.StatusOK, gin.H{ "success": true, "results": results, }) }) log.Println("服务启动在 :8080") r.Run(":8080") }
前端
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>文件上传系统</title> <script src="https://cdnjs.cloudflare.com/AJAX/libs/blueimp-md5/2.19.0/js/md5.min.js"></script> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } h1 { color: #2c3e50; text-align: center; margin-bottom: 30px; } .upload-container { background-color: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .file-drop-area { border: 2px dashed #3498db; border-radius: 5px; padding: 30px; text-align: center; margin-bottom: 20px; transition: all 0.3s; } .file-drop-area.highlight { background-color: #f0f8ff; border-color: #2980b9; } #fileInput { display: none; } .file-label { display: inline-block; padding: 10px 20px; background-color: #3498db; color: white; border-radius: 5px; cursor: pointer; transition: background-color 0.3s; } .file-label:hover { background-color: #2980b9; } .file-list { margin-top: 20px; } .file-item { display: Flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #eee; } .file-info { flex: 1; } .file-name { font-weight: bold; } .file-meta { font-size: 0.8em; color: #7f8c8d; } .file-type { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; margin-left: 10px; } .type-body { background-color: #2ecc71; color: white; } .type-attachment { background-color: #e74c3c; color: white; } .progress-container { margin-top: 20px; } .progress-bar { height: 20px; background-color: #ecf0f1; border-radius: 4px; margin-bottom: 10px; overflow: hidden; } .progress { height: 100%; background-color: #3498db; width: 0%; transition: width 0.3s; } .results { margin-top: 30px; } .result-item { padding: 10px; margin-bottom: 10px; border-radius: 4px; background-color: #f8f9fa; } .success { border-left: 4px solid #2ecc71; } .error { border-left: 4px solid #e74c3c; } button { padding: 10px 20px; background-color: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s; } button:hover { background-color: #2980b9; } button:disabled { background-color: #95a5a6; cursor: not-allowed; } </style> </head> <body> <h1>邮件文件上传系统</h1> <div class="upload-container"> <div class="file-drop-area" id="dropArea"> <input type="file" id="fileInput" multiple> <label for="fileInput" class="file-label">选择文件或拖放到此处</label> <p>支持多文件上传,自动计算MD5校验值</p> </div> &lwww.devze.comt;div class="file-list" id="fileList"></div> <div class="progress-container" id="progressContainer"> <h3>上传进度</h3> <div class="progress-bar"> <div class="progress" id="progressBar"></div> </div> <div id="progressText">准备上传...</div> </div> <button id="uploadBtn" disabled>开始上传</button> <button id="clearBtn">清空列表</button> </div> <div class="results" id="results"></div> <script> // 全局变量 let files = []; const dropArea = document.getElementById('dropArea'); const fileInput = document.getElementById('fileInput'); const fileList = document.getElementById('fileList'); const uploadBtn = document.getElementById('uploadBtn'); const clearBtn = document.getElementById('clearBtn'); const progressContainer = document.getElementById('progressContainer'); const progressBar = document.getElementById('progressBar'); const progressText = document.getElementById('progressText'); const resultsContainer = document.getElementById('results'); // 拖放功能 dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.classList.add('highlight'); }); dropArea.addEventListener('dragleave', () => { dropArea.classList.remove('highlight'); }); dropArea.addEventListener('drop', (e) => { e.preventDefault(); dropArea.classList.remove('highlight'); if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleFiles(); } }); // 文件选择处理 fileInput.addEventListener('change', handleFiles); async function handleFiles() { const newFiles = Array.from(fileInput.files); if (newFiles.length === 0) return; // 为每个文件计算MD5并创建元数据 for (const file of newFiles) { const fileMeta = { file: file, name: file.name, size: file.size, type: file.type, md5: await calculateMD5(file), }; files.push(fileMeta); } renderFileList(); uploadBtn.disabled = false; } // 计算MD5 async function calculateMD5(file) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { const hash = md5(e.target.result); resolve(hash); }; reader.readAsBinaryString(file); // 注意这里使用 readAsBinaryString }); } // 渲染文件列表 function renderFileList() { fileList.innerHTML = ''; if (files.length === 0) { fileLi编程客栈st.innerHTML = '<p>没有选择文件</p>'; uploadBtn.disabled = true; return; } files.forEach((fileMeta, index) => { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = ` <div class="file-info"> <div class="file-name">${fileMeta.name}</div> <div class="file-meta"> 大小: ${formatFileSize(fileMeta.size)} | MD5: ${fileMeta.md5.substring(0, 8)}... | 类型: ${fileMeta.type || '未知'} </div> </div> <div> <button onclick="toggleFileType(${index})" class="file-type ${fileMeta.isAttachment ? 'type-attachment' : 'type-body'}"> ${fileMeta.isAttachment ? '附件' : '正文'} </button> </div> `; fileList.appendChild(fileItem); }); } // 格式化文件大小 function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 上传文件 uploadBtn.addEventListener('click', async () => { if (files.length === 0) return; uploadBtn.disabled = true; progressContainer.style.display = 'block'; resultsContainer.innerHTML = '<h3>上传结果</h3>'; try { const formData = new FormData(); // 添加元数据 const metadata = files.map(f => ({ fileName: f.name, fileSize: f.size, fileType: f.type, fileMD5: f.md5, })); formData.append('metadata', JSON.stringify(metadata)); // 添加文件 files.forEach(f => formData.append('files', f.file)); // 使用Fetch API上传 const xhr = new XMLHttpRequest(); xhr.open('POST', 'http://localhost:8080/upload', true); // 进度监听 xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); progressBar.style.width = percent + '%'; progressText.textContent = `上传中: ${percent}% (${formatFileSize(e.loaded)}/${formatFileSize(e.total)})`; } }; xhr.onload = () => { if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); showResults(response); } else { showError('上传失败: ' + xhr.statusText); } }; xhr.onerror = () => { showError('网络错误,上传失败'); }; xhr.send(formData); } catch (error) { showError('上传出错: ' + error.message); } }); // 显示上传结果 function showResults(response) { progressText.textContent = '上传完成!'; if (response.success) { response.results.forEach(result => { const resultItem = document.createElement('div'); resultItem.className = `result-item ${result.isVerified ? 'success' : 'error'}`; resultItem.innerHTML = ` <div><strong>编程客栈${result.originalName}</strong></div> <div>保存路径: ${result.savedPath || '无'}</div> <div>MD5校验: ${result.receivedMD5 || '无'} - <span> ${result.isVerified ? '✓ 验证通过' : ' 验证失败'} </span> </div> `; resultsContainer.appendChild(resultItem); }); } else { showError(response.error || '上传失败'); } } // 显示错误 function showError(message) { const errorItem = document.createElement('div'); errorItem.className = 'result-item error'; errorItem.textContent = message; resultsContainer.appendChild(errorItem); } // 清空列表 clearBtn.addEventListener('click', () => { files = []; fileInput.value = ''; renderFileList(); progressContainer.style.display = 'none'; resultsContainer.innerHTML = ''; uploadBtn.disabledandroid = true; }); </script> </body> </html>
上传截图
到此这篇关于Go+Gin实现安全多文件上传功能的文章就介绍到这了,更多相关Go Gin多文件上传内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论