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

  • 2020 年 3 月 26 日
  • 筆記

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 n", filePath)   56     fmt.Printf("File offset: %d n", 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 n", 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 --------------------- n")  164     fmt.Printf("File name: %s n", mbf.FileName)  165     fmt.Printf("Size: %d n", mbf.Size)  166     fmt.Printf("Block size: %d n", mbf.BlockSize)  167     fmt.Printf("Total blocks: %d n", mbf.TotalBlocks)  168     fmt.Printf("Index: %d n", 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