Go upload file

简介

这篇文章主要介绍使用 Go 语言来实现客户端上传文件和服务端处理接收文件的功能。

1) Client 端上传文件:Uploading files
2) Server 端接收文件: Receving files 并且 Saving files

Client

作为 Client 端,读取本地文件,并上传到服务器,通常需要调用 Server 端提供的接口,向其发出 POST 请求。

Client 端的上传方式主要有两种:
一是 Request Body 就是整个文件内容,通过请求头(即 Header )中的 Content-Type 字段来指定文件类型。
二是用 multipart 表单方式来上传

Server

作为 Server 端,需要处理接收 Client 端上传过来的文件,并保存在服务器的某个目录中。
Server 通常需要提供一个接口供 Client 端访问,当接收到 Client 的调用请求,则需要解析这个请求,并把请求中上传过来的文件读取并保存到磁盘。

相应的,Server 端在处理 Client 端请求时也要区分两种方式

  • 如果 Client 端上传的是纯二进制流数据,那么直接读取 body 写入到本地文件中即可
  • 如果 Client 端是用 multipart 表单上传的文件,那么就解析表单,再把文件写入本地。

(1)第一种,Client 把文件内容放在整个 body 中来传送

client

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    // "time"
)

const (
    Url = "http://localhost:8080/upload"
)

// body is file content
func doUpload(filepath string) {
    file, err := os.Open(filepath)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    res, err := http.Post(Url, "binary/octet-stream", file) // 第二个参数用来指定 "Content-Type"
    // resp, err := http.Post(Url, "image/jpeg", file)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()
}

func main() {
    var filename string
    if len(os.Args) < 2 {
        filename = "/home/winkee/abc.txt"
    } else {
        filename = os.Args[1]
    }
    doUpload(filename)
}

server

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
)

func UploadHandler(w http.ResponseWriter, r *http.Request) {
        fmt.Println(r.Header["Content-Type"])
    
        // create a temporary file to hold the content
    f, err := os.OpenFile("received.tmp", os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer f.Close()

    n, err := io.Copy(f, r.Body)
    if err != nil {
        panic(err)
    }
        log.Printf("%d bytes are recieved.\n", n)
    // w.Write([]byte(fmt.Sprintf("%d bytes are recieved.\n", n)))
}

func main() {
    http.HandleFunc("/upload", UploadHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

(2)第二种,Client 通过 multipart 表单上传文件

Client 通过 multipart.Writer 的 CreateFormFile() 函数把本地文件写入 Form 中。并设置请求头的 Content-Type 为 writer.FormDataContentType(),然后发送请求。
需要注意的是:发送请求之前需要把 Writer 关闭。

package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func exitIfErr(err error) {
    if err != nil {
        panic(err)
    }
    return
}

func multipartUpload(destURL string, f io.Reader, fields map[string]string) (*http.Response, error) {
    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)
    fw, err := writer.CreateFormFile("file", fields["filename"])
    if err != nil {
        return nil, fmt.Errorf("CreateFormFile %v", err)
    }

    _, err = io.Copy(fw, f)
    if err != nil {
        return nil, fmt.Errorf("copying fileWriter %v", err)
    }

    for k, v := range fields {
        _ = writer.WriteField(k, v)
    }

    err = writer.Close() // close writer before POST request
    if err != nil {
        return nil, fmt.Errorf("writerClose: %v", err)
    }
    
    resp, err := http.Post(destURL, writer.FormDataContentType(), body)
    if err != nil {
        return nil, err
    }
    
    return resp, nil

    // req, err := http.NewRequest("POST", destURL, body)
    // if err != nil {
    //  return nil, err
    // }

    // req.Header.Set("Content-Type", writer.FormDataContentType())

    // if req.Close && req.Body != nil {
    //  defer req.Body.Close()
    // }

    // return http.DefaultClient.Do(req)
}

func main() {
    var filename string
    if len(os.Args) < 2 {
        filename = "/home/winkee/abc.txt"
    } else {
        filename = os.Args[1]
    }

    f, err := os.Open(filename)
    exitIfErr(err)
    defer f.Close()

    fields := map[string]string{
        "filename": filename,
    }
    res, err := multipartUpload("http://localhost:8080/uploadform", f, fields)
    exitIfErr(err)
    fmt.Println("res: ", res)
}

CreateFormFile() 函数原型:

// CreateFormFile is a convenience wrapper around CreatePart. It creates
// a new form-data header with the provided field name and file name.
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) {
    h := make(textproto.MIMEHeader)
    h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldname), escapeQuotes(filename)))
    h.Set("Content-Type", "application/octet-stream")
    return w.CreatePart(h)
}

第二个参数,实际上是时需要带上来的 filename 域所需的值(也就是文件名),
比如,客户端发送 POST 请求进行上传时,在 Body 中就会把 filename=FieldValue 写上,
而在发送 GET 请求进行下载时,则是在 url 中把 filename 拼上。

如果想要再上传文件的时候,同时传递一些其他的参数,那么就应该使用 writer.WriteField() 函数,如下:

for k, v := range fields {
    _ = writer.WriteField(k, v)
}

这样,形成的 request body 内容如下:

image.png

注意:
如果要指定上传的每个部分的Content-Type,则需要重写multipart.Writer的CreateFormField和CreateFormFile方法

func CreateFormFile(fieldname, filename, contentType string, w *multipart.Writer) (io.Writer, error) {
    h := make(textproto.MIMEHeader)
    h.Set("Content-Disposition",
        fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
            escapeQuotes(fieldname), escapeQuotes(filename)))
    h.Set("Content-Type", contentType)
    return w.CreatePart(h)
}

server

package main

import (
    "fmt"
    "io"
    "log"
    "path/filepath"
    "net/http"
    "os"
    "strings"
)

const (
    DefaultUploadDir = "/home/winkee"   
)

func ReceiveFormFile(w http.ResponseWriter, r *http.Request) {
    // if err = req.ParseMultipartForm(2 << 10); err != nil {  
    //    status = http.StatusInternalServerError  
    //    return  
    // }  
    
    // r.Method should be "POST"
    file, header, err := r.FormFile("file")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    nameParts := strings.Split(header.Filename, ".")
    filename := nameParts[1]
    savedPath := filepath.Join(DefaultUploadDir, filename)
    f, err := os.OpenFile(savedPath, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        panic(err)
    }
    defer f.Close()
    _, err = io.Copy(f, file)
    if err != nil {
        panic(err)
    }

    return
}

func main() {
    http.HandleFunc("/uploadform", ReceiveFormFile)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

这里的 header.Filename 需要注意,客户端上传时指定的 filename 可能是 ../../../../../test/hello.txt 这种格式,如果不做 sanitize 的话,保存的时候直接用这个 filename 显然会出问题。

server 的另外一种处理

func ReceiveFormFile(w http.ResponseWriter, r *http.Request) {
    const _24K = (1 << 10) * 24  // 24 MB
    if err = req.ParseMultipartForm(_24K); nil != err {  
        status = http.StatusInternalServerError  
        return  
    }  
    for _, fheaders := range req.MultipartForm.File {  
        for _, hdr := range fheaders {  
            // open uploaded  
            var infile multipart.File  
            if infile, err = hdr.Open(); nil != err {  
                status = http.StatusInternalServerError  
                return  
            }  
            // open destination  
            var outfile *os.File  
            if outfile, err = os.Create("./uploaded/" + hdr.Filename); nil != err {  
                status = http.StatusInternalServerError  
                return  
            }  
            // 32K buffer copy  
            var written int64  
            if written, err = io.Copy(outfile, infile); nil != err {  
                status = http.StatusInternalServerError  
                return  
            }  
            res.Write([]byte("uploaded file:" + hdr.Filename + ";length:" + strconv.Itoa(int(written))))  
        }  
    }  
}

解析

http 包中的 Request 结构体提供了 ParseForm() 和 ParseMultipartForm() 两个函数。
在理解它们之间的区别之前,先来看一下 Request 结构体的定义,如下:

type Request struct {   
    // Form contains the parsed form data, including both the URL
    // field's query parameters and the POST or PUT form data.
    // This field is only available after ParseForm is called.
    // The HTTP client ignores Form and uses Body instead.
    Form url.Values

    // PostForm contains the parsed form data from POST, PATCH,
    // or PUT body parameters.
    //
    // This field is only available after ParseForm is called.
    // The HTTP client ignores PostForm and uses Body instead.
    PostForm url.Values

    // MultipartForm is the parsed multipart form, including file uploads.
    // This field is only available after ParseMultipartForm is called.
    // The HTTP client ignores MultipartForm and uses Body instead.
    MultipartForm *multipart.Form
    ...
}

可见,Request 结构体中,定义了 3 个不同类型的 Form 相关的数据
Form 是一个通用的数据类型 url.Values,原型如下:

type Values map[string][]string

1) 对于 Form 和 PostForm 来说,一个是 GET url raw 请求中带的参数,另一个是在 POST body 中带的参数,格式都一样,比如 ?name=xxx&age=18 等。服务端在接收到请求时,通过调用 r.ParseForm()来获得客户端传上来的请求参数。
2) 对于这个 MultipartForm,是用户客户端上传文件时使用的,服务端收到客户端的请求时,通过调用 r.ParseMultipartForm() 函数来获得相应的文件内容。r.ParseMultipartForm() 在内部会调用 r.ParseForm() 来进行部分解析。

multipart.Form 则是在 url.Values 的基础上,再添加了一个包含了文件信息的数据类型,原型如下:

// Form is a parsed multipart form.
// Its File parts are stored either in memory or on disk,
// and are accessible via the *FileHeader's Open method.
// Its Value parts are stored as strings.
// Both are keyed by field name.
type Form struct {
    Value map[string][]string
    File  map[string][]*FileHeader
}

通常,服务端在收到客户端上传的文件请求时,先调用 r.ParseMultipartForm(maxMemorySize) ,然后调用 r.FormFile("uploadfile") 来获得其中的对应控件名的文件(注:multipartForm 可以包含多个文件)。
实际上, r.FormFile(") 内部也会调用 r.ParseMultipartForm(),因此,即使不先调用 r.ParseMultipartForm(), 而直接调用 r.FormFile("") 也是可以的。
但是,如果文件太大的话,可能会出错,因此最好还是先调用 r.ParseMultipartForm() 控制读入内存的大小。

默认的最大是 10 M,也就是:

maxFormSize = int64(10 << 20)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容