• 设为首页
  • 收藏本站
  • 积分充值
  • VIP赞助
  • 手机版
  • 微博
  • 微信
    微信公众号 添加方式:
    1:搜索微信号(888888
    2:扫描左侧二维码
  • 快捷导航
    福建二哥 门户 查看主题

    Go+Gin实现安全多文件上传功能

    发布者: 琛瑞6678 | 发布时间: 2025-8-14 12:49| 查看数: 95| 评论数: 0|帖子模式

    Go+Gin实现安全多文件上传:带MD5校验的完整解决方案
    完整代码如下
    后端
    1. package main

    2. import (
    3.         "encoding/json"
    4.         "fmt"
    5.         "log"
    6.         "net/http"
    7.         "os"
    8.         "path/filepath"

    9.         "github.com/gin-contrib/cors"
    10.         "github.com/gin-gonic/gin"
    11. )

    12. // 前端传来的文件元数据
    13. type FileMetaRequest struct {
    14.         FileName     string `json:"fileName" binding:"required"`
    15.         FileSize     int64  `json:"fileSize" binding:"required"`
    16.         FileType     string `json:"fileType" binding:"required"`
    17.         FileMD5      string `json:"fileMD5" binding:"required"`
    18. }

    19. // 返回给前端的响应结构
    20. type UploadResponse struct {
    21.         OriginalName string `json:"originalName"`
    22.         SavedPath    string `json:"savedPath"`
    23.         ReceivedMD5  string `json:"receivedMD5"`
    24.         IsVerified   bool   `json:"isVerified"` // 是否通过验证
    25. }

    26. func main() {
    27.         r := gin.Default()

    28.         // 配置CORS
    29.         r.Use(cors.New(cors.Config{
    30.                 AllowOrigins: []string{"*"},
    31.                 AllowMethods: []string{"POST"},
    32.         }))

    33.         // 上传目录
    34.         uploadDir := "uploads"
    35.         if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
    36.                 os.Mkdir(uploadDir, 0755)
    37.         }

    38.         r.POST("/upload", func(c *gin.Context) {
    39.                 // 1. 获取元数据JSON
    40.                 metaJson := c.PostForm("metadata")
    41.                 var fileMetas []FileMetaRequest
    42.                 if err := json.Unmarshal([]byte(metaJson), &fileMetas); err != nil {
    43.                         c.JSON(http.StatusBadRequest, gin.H{"error": "元数据解析失败"})
    44.                         return
    45.                 }

    46.                 // 2. 获取文件
    47.                 form, err := c.MultipartForm()
    48.                 if err != nil {
    49.                         c.JSON(http.StatusBadRequest, gin.H{"error": "文件获取失败"})
    50.                         return
    51.                 }
    52.                 files := form.File["files"]

    53.                 // 3. 验证文件数量匹配
    54.                 if len(files) != len(fileMetas) {
    55.                         c.JSON(http.StatusBadRequest, gin.H{
    56.                                 "error": fmt.Sprintf("元数据与文件数量不匹配(元数据:%d 文件:%d)",
    57.                                         len(fileMetas), len(files)),
    58.                         })
    59.                         return
    60.                 }

    61.                 var results []UploadResponse
    62.                 for i, file := range files {
    63.                         meta := fileMetas[i]

    64.                         // 4. 验证基本元数据
    65.                         if file.Filename != meta.FileName ||
    66.                                 file.Size != meta.FileSize {
    67.                                 results = append(results, UploadResponse{
    68.                                         OriginalName: file.Filename,
    69.                                         IsVerified:   false,
    70.                                 })
    71.                                 continue
    72.                         }

    73.                         // 5. 保存文件
    74.                         savedName := fmt.Sprintf("%s%s", meta.FileMD5, filepath.Ext(file.Filename))
    75.                         savePath := filepath.Join(uploadDir, savedName)

    76.                         if err := c.SaveUploadedFile(file, savePath); err != nil {
    77.                                 results = append(results, UploadResponse{
    78.                                         OriginalName: file.Filename,
    79.                                         IsVerified:   false,
    80.                                 })
    81.                                 continue
    82.                         }

    83.                         // 6. 记录结果(实际项目中这里应该做MD5校验)
    84.                         results = append(results, UploadResponse{
    85.                                 OriginalName: file.Filename,
    86.                                 SavedPath:    savePath,
    87.                                 ReceivedMD5:  meta.FileMD5,
    88.                                 IsVerified:   true,
    89.                         })
    90.                 }

    91.                 c.JSON(http.StatusOK, gin.H{
    92.                         "success": true,
    93.                         "results": results,
    94.                 })
    95.         })

    96.         log.Println("服务启动在 :8080")
    97.         r.Run(":8080")
    98. }
    复制代码
    前端
    1. <!DOCTYPE html>
    2. <html lang="zh-CN">
    3. <head>
    4.     <meta charset="UTF-8">
    5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
    6.     <title>文件上传系统</title>
    7.     <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js"></script>

    8.     <style>
    9.         body {
    10.             font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    11.             max-width: 800px;
    12.             margin: 0 auto;
    13.             padding: 20px;
    14.             background-color: #f5f5f5;
    15.         }
    16.         h1 {
    17.             color: #2c3e50;
    18.             text-align: center;
    19.             margin-bottom: 30px;
    20.         }
    21.         .upload-container {
    22.             background-color: white;
    23.             padding: 25px;
    24.             border-radius: 8px;
    25.             box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    26.         }
    27.         .file-drop-area {
    28.             border: 2px dashed #3498db;
    29.             border-radius: 5px;
    30.             padding: 30px;
    31.             text-align: center;
    32.             margin-bottom: 20px;
    33.             transition: all 0.3s;
    34.         }
    35.         .file-drop-area.highlight {
    36.             background-color: #f0f8ff;
    37.             border-color: #2980b9;
    38.         }
    39.         #fileInput {
    40.             display: none;
    41.         }
    42.         .file-label {
    43.             display: inline-block;
    44.             padding: 10px 20px;
    45.             background-color: #3498db;
    46.             color: white;
    47.             border-radius: 5px;
    48.             cursor: pointer;
    49.             transition: background-color 0.3s;
    50.         }
    51.         .file-label:hover {
    52.             background-color: #2980b9;
    53.         }
    54.         .file-list {
    55.             margin-top: 20px;
    56.         }
    57.         .file-item {
    58.             display: flex;
    59.             justify-content: space-between;
    60.             align-items: center;
    61.             padding: 10px;
    62.             border-bottom: 1px solid #eee;
    63.         }
    64.         .file-info {
    65.             flex: 1;
    66.         }
    67.         .file-name {
    68.             font-weight: bold;
    69.         }
    70.         .file-meta {
    71.             font-size: 0.8em;
    72.             color: #7f8c8d;
    73.         }
    74.         .file-type {
    75.             display: inline-block;
    76.             padding: 2px 8px;
    77.             border-radius: 4px;
    78.             font-size: 0.8em;
    79.             margin-left: 10px;
    80.         }
    81.         .type-body {
    82.             background-color: #2ecc71;
    83.             color: white;
    84.         }
    85.         .type-attachment {
    86.             background-color: #e74c3c;
    87.             color: white;
    88.         }
    89.         .progress-container {
    90.             margin-top: 20px;
    91.         }
    92.         .progress-bar {
    93.             height: 20px;
    94.             background-color: #ecf0f1;
    95.             border-radius: 4px;
    96.             margin-bottom: 10px;
    97.             overflow: hidden;
    98.         }
    99.         .progress {
    100.             height: 100%;
    101.             background-color: #3498db;
    102.             width: 0%;
    103.             transition: width 0.3s;
    104.         }
    105.         .results {
    106.             margin-top: 30px;
    107.         }
    108.         .result-item {
    109.             padding: 10px;
    110.             margin-bottom: 10px;
    111.             border-radius: 4px;
    112.             background-color: #f8f9fa;
    113.         }
    114.         .success {
    115.             border-left: 4px solid #2ecc71;
    116.         }
    117.         .error {
    118.             border-left: 4px solid #e74c3c;
    119.         }
    120.         button {
    121.             padding: 10px 20px;
    122.             background-color: #3498db;
    123.             color: white;
    124.             border: none;
    125.             border-radius: 4px;
    126.             cursor: pointer;
    127.             font-size: 16px;
    128.             transition: background-color 0.3s;
    129.         }
    130.         button:hover {
    131.             background-color: #2980b9;
    132.         }
    133.         button:disabled {
    134.             background-color: #95a5a6;
    135.             cursor: not-allowed;
    136.         }
    137.     </style>
    138. </head>
    139. <body>
    140. <h1>邮件文件上传系统</h1>

    141. <div class="upload-container">
    142.     <div class="file-drop-area" id="dropArea">
    143.         <input type="file" id="fileInput" multiple>
    144.         <label for="fileInput" class="file-label">选择文件或拖放到此处</label>
    145.         <p>支持多文件上传,自动计算MD5校验值</p>
    146.     </div>

    147.     <div class="file-list" id="fileList"></div>

    148.     <div class="progress-container" id="progressContainer" style="display: none;">
    149.         <h3>上传进度</h3>
    150.         <div class="progress-bar">
    151.             <div class="progress" id="progressBar"></div>
    152.         </div>
    153.         <div id="progressText">准备上传...</div>
    154.     </div>

    155.     <button id="uploadBtn" disabled>开始上传</button>
    156.     <button id="clearBtn">清空列表</button>
    157. </div>

    158. <div class="results" id="results"></div>

    159. <script>
    160.     // 全局变量
    161.     let files = [];
    162.     const dropArea = document.getElementById('dropArea');
    163.     const fileInput = document.getElementById('fileInput');
    164.     const fileList = document.getElementById('fileList');
    165.     const uploadBtn = document.getElementById('uploadBtn');
    166.     const clearBtn = document.getElementById('clearBtn');
    167.     const progressContainer = document.getElementById('progressContainer');
    168.     const progressBar = document.getElementById('progressBar');
    169.     const progressText = document.getElementById('progressText');
    170.     const resultsContainer = document.getElementById('results');

    171.     // 拖放功能
    172.     dropArea.addEventListener('dragover', (e) => {
    173.         e.preventDefault();
    174.         dropArea.classList.add('highlight');
    175.     });

    176.     dropArea.addEventListener('dragleave', () => {
    177.         dropArea.classList.remove('highlight');
    178.     });

    179.     dropArea.addEventListener('drop', (e) => {
    180.         e.preventDefault();
    181.         dropArea.classList.remove('highlight');
    182.         if (e.dataTransfer.files.length) {
    183.             fileInput.files = e.dataTransfer.files;
    184.             handleFiles();
    185.         }
    186.     });

    187.     // 文件选择处理
    188.     fileInput.addEventListener('change', handleFiles);

    189.     async function handleFiles() {
    190.         const newFiles = Array.from(fileInput.files);
    191.         if (newFiles.length === 0) return;

    192.         // 为每个文件计算MD5并创建元数据
    193.         for (const file of newFiles) {
    194.             const fileMeta = {
    195.                 file: file,
    196.                 name: file.name,
    197.                 size: file.size,
    198.                 type: file.type,
    199.                 md5: await calculateMD5(file),
    200.             };
    201.             files.push(fileMeta);
    202.         }

    203.         renderFileList();
    204.         uploadBtn.disabled = false;
    205.     }

    206.     // 计算MD5
    207.     async function calculateMD5(file) {
    208.         return new Promise((resolve) => {
    209.             const reader = new FileReader();
    210.             reader.onload = (e) => {
    211.                 const hash = md5(e.target.result);
    212.                 resolve(hash);
    213.             };
    214.             reader.readAsBinaryString(file); // 注意这里使用 readAsBinaryString
    215.         });
    216.     }

    217.     // 渲染文件列表
    218.     function renderFileList() {
    219.         fileList.innerHTML = '';

    220.         if (files.length === 0) {
    221.             fileList.innerHTML = '<p>没有选择文件</p>';
    222.             uploadBtn.disabled = true;
    223.             return;
    224.         }

    225.         files.forEach((fileMeta, index) => {
    226.             const fileItem = document.createElement('div');
    227.             fileItem.className = 'file-item';

    228.             fileItem.innerHTML = `
    229.                     <div class="file-info">
    230.                         <div class="file-name">${fileMeta.name}</div>
    231.                         <div class="file-meta">
    232.                             大小: ${formatFileSize(fileMeta.size)} |
    233.                             MD5: ${fileMeta.md5.substring(0, 8)}... |
    234.                             类型: ${fileMeta.type || '未知'}
    235.                         </div>
    236.                     </div>
    237.                     <div>
    238.                         <button onclick="toggleFileType(${index})" class="file-type ${fileMeta.isAttachment ? 'type-attachment' : 'type-body'}">
    239.                             ${fileMeta.isAttachment ? '附件' : '正文'}
    240.                         </button>
    241.                     </div>
    242.                 `;

    243.             fileList.appendChild(fileItem);
    244.         });
    245.     }

    246.     // 格式化文件大小
    247.     function formatFileSize(bytes) {
    248.         if (bytes === 0) return '0 Bytes';
    249.         const k = 1024;
    250.         const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    251.         const i = Math.floor(Math.log(bytes) / Math.log(k));
    252.         return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    253.     }

    254.     // 上传文件
    255.     uploadBtn.addEventListener('click', async () => {
    256.         if (files.length === 0) return;

    257.         uploadBtn.disabled = true;
    258.         progressContainer.style.display = 'block';
    259.         resultsContainer.innerHTML = '<h3>上传结果</h3>';

    260.         try {
    261.             const formData = new FormData();

    262.             // 添加元数据
    263.             const metadata = files.map(f => ({
    264.                 fileName: f.name,
    265.                 fileSize: f.size,
    266.                 fileType: f.type,
    267.                 fileMD5: f.md5,
    268.             }));
    269.             formData.append('metadata', JSON.stringify(metadata));

    270.             // 添加文件
    271.             files.forEach(f => formData.append('files', f.file));

    272.             // 使用Fetch API上传
    273.             const xhr = new XMLHttpRequest();
    274.             xhr.open('POST', 'http://localhost:8080/upload', true);

    275.             // 进度监听
    276.             xhr.upload.onprogress = (e) => {
    277.                 if (e.lengthComputable) {
    278.                     const percent = Math.round((e.loaded / e.total) * 100);
    279.                     progressBar.style.width = percent + '%';
    280.                     progressText.textContent = `上传中: ${percent}% (${formatFileSize(e.loaded)}/${formatFileSize(e.total)})`;
    281.                 }
    282.             };

    283.             xhr.onload = () => {
    284.                 if (xhr.status === 200) {
    285.                     const response = JSON.parse(xhr.responseText);
    286.                     showResults(response);
    287.                 } else {
    288.                     showError('上传失败: ' + xhr.statusText);
    289.                 }
    290.             };

    291.             xhr.onerror = () => {
    292.                 showError('网络错误,上传失败');
    293.             };

    294.             xhr.send(formData);

    295.         } catch (error) {
    296.             showError('上传出错: ' + error.message);
    297.         }
    298.     });

    299.     // 显示上传结果
    300.     function showResults(response) {
    301.         progressText.textContent = '上传完成!';

    302.         if (response.success) {
    303.             response.results.forEach(result => {
    304.                 const resultItem = document.createElement('div');
    305.                 resultItem.className = `result-item ${result.isVerified ? 'success' : 'error'}`;

    306.                 resultItem.innerHTML = `
    307.                         <div><strong>${result.originalName}</strong></div>
    308.                         <div>保存路径: ${result.savedPath || '无'}</div>
    309.                         <div>MD5校验: ${result.receivedMD5 || '无'} -
    310.                             <span style="color: ${result.isVerified ? '#2ecc71' : '#e74c3c'}">
    311.                                 ${result.isVerified ? '✓ 验证通过' : '× 验证失败'}
    312.                             </span>
    313.                         </div>
    314.                     `;

    315.                 resultsContainer.appendChild(resultItem);
    316.             });
    317.         } else {
    318.             showError(response.error || '上传失败');
    319.         }
    320.     }

    321.     // 显示错误
    322.     function showError(message) {
    323.         const errorItem = document.createElement('div');
    324.         errorItem.className = 'result-item error';
    325.         errorItem.textContent = message;
    326.         resultsContainer.appendChild(errorItem);
    327.     }

    328.     // 清空列表
    329.     clearBtn.addEventListener('click', () => {
    330.         files = [];
    331.         fileInput.value = '';
    332.         renderFileList();
    333.         progressContainer.style.display = 'none';
    334.         resultsContainer.innerHTML = '';
    335.         uploadBtn.disabled = true;
    336.     });
    337. </script>
    338. </body>
    339. </html>
    复制代码
    上传截图

    到此这篇关于Go+Gin实现安全多文件上传功能的文章就介绍到这了,更多相关Go Gin多文件上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    来源:互联网
    免责声明:如果侵犯了您的权益,请联系站长(1277306191@qq.com),我们会及时删除侵权内容,谢谢合作!

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    最新评论

    浏览过的版块

    QQ Archiver 手机版 小黑屋 福建二哥 ( 闽ICP备2022004717号|闽公网安备35052402000345号 )

    Powered by Discuz! X3.5 © 2001-2023

    快速回复 返回顶部 返回列表