FormData/Go分片/分块文件上传

Posted view85

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FormData/Go分片/分块文件上传相关的知识,希望对你有一定的参考价值。

FormData 接口提供了一种表示表单数据的键值对的构造方式,经过它的数据可以使用 XMLHttpRequest.send() 方法送出,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。

如果你想构建一个简单的GET请求,并且通过<form>的形式带有查询参数,可以将它直接传递给URLSearchParams

更多解释MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/FormData

分块(分片,统称分块了,确实只是发送一块数据)文件上传主要分2部分。

1. 前端js用file.slice可以从文件中切出一块一块的数据,然后用FormData包装一下,用XMLHttpRequest把切出来的数据块,一块一块send到server.

2. Server接收到的每一块都是一个multipart/form-data Form表单。可以在表单里放很多附属信息,文件名,大小,块大小,块索引,最总带上这块切出来的二进制数据。

multipart/form-data 数据

POST /upload HTTP/1.1
Host: localhost:8080
Content-Length: 2098072
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/80.0.3987.149 Safari/537.36

Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymtng0xrR3ASR7wx7

------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="file_name"

apache-maven-3.6.3-bin.zip
------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="file_size"

9602303
------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="block_size"

2097152
------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="total_blocks"

5
------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="break_error"

true
------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="index"

3
------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="data"; filename="blob"
Content-Type: application/octet-stream
(binary)

在Server存储文件,基本也就2种方案:

1. 直接创建一个对应大小的文件,按照每块数据的offset位置,写进去。

2. 每个传过来的数据块,保存成一个单独的数据块文件,最后把所有文件块合并成文件。

我这里只是做了一份简单的演示代码,基本上是不能用于生产环境的。

Index.html,直接把js写进去了

技术图片
  1 <!DOCTYPE html>
  2 <html>
  3     <head>
  4         <meta charset="utf8">
  5         <title>Multil-Blocks upload</title>
  6     </head>
  7 
  8     <body>
  9         <h2>Multil-Blocks upload</h2>
 10 
 11         <input id="file" type="file" />
 12 
 13         <input type="checkbox" id="multil_block_file">multil block file</input>
 14         <button type="button" onclick="on_block_upload()">Block upload</button>
 15         <button type="button" onclick="on_concurrency_upload()">Concurrency upload</button>
 16         <hr/>
 17 
 18         <div>
 19             <label>File name: </label><span id="file_name"></span>
 20         </div>
 21         <div>
 22             <label>File size: </label><span id="file_size"></span>
 23         </div>
 24         <div>
 25             <label>Split blocks: </label><span id="block_count"></span>
 26         </div>
 27 
 28         <hr/>
 29 
 30         <p id="upload_info"></p>
 31 
 32         <script>
 33             var Block_Size = 1024 * 1024 * 2;
 34 
 35             var el_file = document.getElementById(file);
 36             var el_multil_block_file = document.getElementById(multil_block_file);
 37             var el_file_name = document.getElementById(file_name);
 38             var el_file_size = document.getElementById(file_size);
 39             var el_block_count = document.getElementById(block_count);
 40             var el_upload_info = document.getElementById(upload_info);
 41 
 42             var file = null;
 43             var total_blocks = 0;
 44             var block_index = -1;
 45             var block_index_random_arr = [];
 46             var form_data = null;
 47 
 48 
 49             el_file.onchange = function() {
 50                 if (this.files.length === 0) return;
 51 
 52                 file = this.files[0];
 53                 total_blocks = Math.ceil( file.size / Block_Size );
 54 
 55                 el_file_name.innerText = file.name;
 56                 el_file_size.innerText = file.size;
 57                 el_block_count.innerText = total_blocks;
 58             }
 59 
 60             function print_info(msg) {
 61                 el_upload_info.innerHTML += `${msg}<br/>`;
 62             }
 63 
 64             function done() {
 65                 file = null;
 66                 total_blocks = 0;
 67                 block_index = -1;
 68                 form_data = null;
 69                 
 70                 el_file.value = ‘‘;
 71             }
 72 
 73 
 74             function get_base_form_data() {
 75                 var base_data = new FormData();
 76                 base_data.append(file_name, file.name);
 77                 base_data.append(file_size, file.size);
 78                 base_data.append(block_size, Block_Size);
 79                 base_data.append(total_blocks, total_blocks);
 80                 base_data.append(break_error, true);
 81                 base_data.append(index, 0);
 82                 base_data.append(data, null);
 83 
 84                 return base_data
 85             }
 86 
 87 
 88             function build_block_index_random_arr() {
 89                 block_index_random_arr = new Array(total_blocks).fill(0).map((v,i) => i);
 90                 block_index_random_arr.sort((n, m) => Math.random() > .5 ? -1 : 1);
 91 
 92                 print_info(`Upload sequence: ${block_index_random_arr}`);
 93             }
 94 
 95 
 96             function post(index, success_cb, failed_cb) {
 97                 if (!form_data) {
 98                     form_data = get_base_form_data();
 99                 }
100                 var start = index * Block_Size;
101                 var end = Math.min(file.size, start + Block_Size);
102 
103                 form_data.set(index, index);
104                 form_data.set(data, file.slice(start, end));
105 
106                 print_info(`Post ${index}/${total_blocks}, offset: ${start} -- ${end}`);
107 
108 
109                 var xhr = new XMLHttpRequest();
110                 xhr.open(POST, /upload, true);
111                 /*
112                     Browser-based general content types
113                     Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysXH5DIES2XFMuLXL
114 
115                     Error content type:
116                     xhr.setRequestHeader(‘Content-Type‘, ‘multipart/form-data‘);
117                     Content-Type: multipart/form-data;
118                 */
119                 xhr.onreadystatechange = function() {
120 
121                     if (xhr.readyState === XMLHttpRequest.DONE) {
122 
123                         if (xhr.status >= 200 && xhr.status < 300 && success_cb) {
124                             return success_cb();
125                         }
126 
127                         if (xhr.status >= 400 && failed_cb) {
128                             failed_cb();
129                         }
130                     }
131                 }
132 
133                 // xhr.onerror event
134                 xhr.send(form_data);
135             }
136 
137 
138             function block_upload() {
139                 if (!file) {
140                     return;
141                 }
142                 if (block_index + 1 >= total_blocks) {
143                     return done();
144                 }
145 
146                 block_index++;
147                 var index = block_index_random_arr[block_index];
148             
149                 post(index, block_upload);
150             }
151 
152 
153             function concurrency_upload() {
154                 if (!file || total_blocks === 0) {
155                     return;
156                 }
157 
158                 build_block_index_random_arr();
159 
160                 form_data = get_base_form_data();
161                 form_data.set(break_error, false);
162                 form_data.set(multil_block, el_multil_block_file.checked);
163 
164                 for (var i of block_index_random_arr) {
165                     ((idx) => {
166                         post(idx, null, function() {
167                             print_info(`Failed: ${idx}`);
168                             setTimeout(() => post(idx), 1000);
169                         });
170                     })(i);
171                 }
172             }
173 
174 
175             function on_block_upload() {
176                 if (file) {
177                     print_info(Block upload);
178 
179                     form_data = get_base_form_data();
180                     form_data.set(multil_block, el_multil_block_file.checked);
181 
182                     build_block_index_random_arr();
183 
184                     block_index = -1;
185                     block_upload();
186                 }
187             }
188 
189             function on_concurrency_upload() {
190                 if (file) {
191                     print_info(Concurrency upload);
192                     concurrency_upload();
193                 }
194             }
195         </script>
196 
197     </body>
198 </html>
View Code

简单的Go server和保存文件,基本忽略所有的错误处理

技术图片
  1 package main
  2 
  3 import (
  4     "fmt"
  5     "io/ioutil"
  6     "log"
  7     "net/http"
  8     "os"
  9     "path"
 10     "path/filepath"
 11     "regexp"
 12     "strconv"
 13     "strings"
 14     "syscall"
 15     "text/template"
 16 )
 17 
 18 type MultilBlockFile struct {
 19     FileName    string
 20     Size        int64
 21     BlockSize   int64
 22     TotalBlocks int
 23     Index       int
 24     Bufs        []byte
 25     BreakError  bool
 26 }
 27 
 28 func fileIsExist(f string) bool {
 29     _, err := os.Stat(f)
 30     return err == nil || os.IsExist(err)
 31 }
 32 
 33 func lockFile(f *os.File) error {
 34     err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
 35     if err != nil {
 36         return fmt.Errorf("get flock failed. err: %s", err)
 37     }
 38 
 39     return nil
 40 }
 41 
 42 func unlockFile(f *os.File) error {
 43     defer f.Close()
 44     return syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
 45 }
 46 
 47 func singleFileSave(mbf *MultilBlockFile) error {
 48 
 49     dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
 50     filePath := path.Join(dir, "tmp", mbf.FileName)
 51 
 52     offset := int64(mbf.Index) * mbf.BlockSize
 53 
 54     fmt.Println(">>> Single file save ---------------------")
 55     fmt.Printf("Save file: %s 
", filePath)
 56     fmt.Printf("File offset: %d 
", offset)
 57 
 58     var f *os.File
 59     var needTruncate bool = false
 60     if !fileIsExist(filePath) {
 61         needTruncate = true
 62     }
 63 
 64     f, _ = os.OpenFile(filePath, syscall.O_CREAT|syscall.O_WRONLY, 0777)
 65 
 66     err := lockFile(f)
 67     if err != nil {
 68         if mbf.BreakError {
 69             log.Fatalf("get flock failed. err: %s", err)
 70         } else {
 71             return err
 72         }
 73     }
 74 
 75     if needTruncate {
 76         f.Truncate(mbf.Size)
 77     }
 78 
 79     f.WriteAt(mbf.Bufs, offset)
 80 
 81     unlockFile(f)
 82 
 83     return nil
 84 }
 85 
 86 func multilBlocksSave(mbf *MultilBlockFile) error {
 87     dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
 88     tmpFolderPath := path.Join(dir, "tmp")
 89     tmpFileName := fmt.Sprintf("%s.%d", mbf.FileName, mbf.Index)
 90     fileBlockPath := path.Join(tmpFolderPath, tmpFileName)
 91 
 92     f, _ := os.OpenFile(fileBlockPath, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_TRUNC, 0777)
 93     defer f.Close()
 94 
 95     f.Write(mbf.Bufs)
 96     f.Close()
 97 
 98     re := regexp.MustCompile(`(?i:^` + mbf.FileName + `).d$`)
 99 
100     files, _ := ioutil.ReadDir(tmpFolderPath)
101     matchFiles := make(map[string]bool)
102 
103     for _, file := range files {
104         if file.IsDir() {
105             continue
106         }
107 
108         fname := file.Name()
109         if re.MatchString(fname) {
110             matchFiles[fname] = true
111         }
112     }
113 
114     if len(matchFiles) >= mbf.TotalBlocks {
115         lastFile, _ := os.OpenFile(path.Join(tmpFolderPath, mbf.FileName), syscall.O_CREAT|syscall.O_WRONLY, 0777)
116         lockFile(lastFile)
117 
118         lastFile.Truncate(mbf.Size)
119 
120         for name := range matchFiles {
121             tmpPath := path.Join(tmpFolderPath, name)
122 
123             idxStr := name[strings.LastIndex(name, ".")+1:]
124             idx, _ := strconv.ParseInt(idxStr, 10, 32)
125 
126             fmt.Printf("Match file: %s index: %d 
", name, idx)
127 
128             data, _ := ioutil.ReadFile(tmpPath)
129 
130             lastFile.WriteAt(data, idx*mbf.BlockSize)
131 
132             os.Remove(tmpPath)
133         }
134         unlockFile(lastFile)
135     }
136 
137     return nil
138 }
139 
140 func indexHandle(w http.ResponseWriter, r *http.Request) {
141     tmp, _ := template.ParseFiles("./static/index.html")
142     tmp.Execute(w, "Index")
143 }
144 
145 func uploadHandle(w http.ResponseWriter, r *http.Request) {
146 
147     var mbf MultilBlockFile
148     mbf.FileName = r.FormValue("file_name")
149     mbf.Size, _ = strconv.ParseInt(r.FormValue("file_size"), 10, 64)
150     mbf.BlockSize, _ = strconv.ParseInt(r.FormValue("block_size"), 10, 64)
151     mbf.BreakError, _ = strconv.ParseBool(r.FormValue("break_error"))
152 
153     var i int64
154     i, _ = strconv.ParseInt(r.FormValue("total_blocks"), 10, 32)
155     mbf.TotalBlocks = int(i)
156 
157     i, _ = strconv.ParseInt(r.FormValue("index"), 10, 32)
158     mbf.Index = int(i)
159 
160     d, _, _ := r.FormFile("data")
161     mbf.Bufs, _ = ioutil.ReadAll(d)
162 
163     fmt.Printf(">>> Upload --------------------- 
")
164     fmt.Printf("File name: %s 
", mbf.FileName)
165     fmt.Printf("Size: %d 
", mbf.Size)
166     fmt.Printf("Block size: %d 
", mbf.BlockSize)
167     fmt.Printf("Total blocks: %d 
", mbf.TotalBlocks)
168     fmt.Printf("Index: %d 
", mbf.Index)
169     fmt.Println("Bufs len:", len(mbf.Bufs))
170 
171     multilBlockFile, _ := strconv.ParseBool(r.FormValue("multil_block"))
172 
173     var err error
174     if multilBlockFile {
175         err = multilBlocksSave(&mbf)
176     } else {
177         err = singleFileSave(&mbf)
178     }
179 
180     if !mbf.BreakError && err != nil {
181         w.WriteHeader(400)
182         fmt.Fprintf(w, fmt.Sprintf("%s", err))
183         return
184     }
185 
186     fmt.Fprintf(w, "ok")
187 }
188 
189 func main() {
190     println("Listen on 8080")
191 
192     http.HandleFunc("/", indexHandle)
193     http.HandleFunc("/upload", uploadHandle)
194 
195     log.Fatal(http.ListenAndServe(":8080", nil))
196 }
View Code

 目录截图,比较乱来

技术图片

 

 

 

以上是关于FormData/Go分片/分块文件上传的主要内容,如果未能解决你的问题,请参考以下文章

浏览器 大文件分片上传处理

Java大文件分片上传/多线程上传解决方案

b/s大文件上传解决方案支持分片断点上传

超大文件上传和断点续传的源代码

超大文件上传和断点续传的源代码

PHP 大文件上传进度条实现