如何处理同时具有多个输入和多个文件的请求

huangapple go评论78阅读模式
英文:

How to process a request that has multiple inputs and multiple files at the same time

问题

构建一个后端Go服务器,可以接收具有多个输入字段的表单,其中3个字段具有多个文件输入。我搜索了一下,发现如果要使这样的工作正常,不应该使用典型的方式:

if err := r.ParseMultipartForm(32 << 20); err != nil {
    fmt.Println(err)
}
// 获取文件头的引用
files := r.MultipartForm.File["coverArt"]

而应该使用以下方式:

mr, err := r.MultipartReader()
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
}

标准的表单数据:
1)姓名
2)电子邮件
3)封面艺术照片(多个文件)
4)个人资料照片(多个文件)
5)2个音频文件(2首歌曲)
6)2个视频(个人介绍,人声独唱录音)

HTML表单:

<form method="post" enctype="multipart/form-data" action="/upload">
    <input type="text" name="name">
    <input type="text" name="email">
    <input name="coverArt" type="file" multiple />
    <input name="profile" type="file" multiple />
    <input type="file" name="songs" multiple />
    <input type="file" name="videos" multiple/>

    <button type="submit">上传文件</button>
</form>

Go代码:

func FilePOST(w http.ResponseWriter, r *http.Request) error {
    fmt.Println("File Upload Endpoint Hit")
    mr, err := r.MultipartReader()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }

    for {
        part, err := mr.NextPart()

        // 没有更多的部分,结束循环
        if err == io.EOF {
            break
        }

        // 出现错误
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }

        // 封面艺术照片部分
        if part.FormName() == "coverArt" {
            name := part.FileName()
            outfile, err := os.Create("uploads/" + name)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
            defer outfile.Close()

            _, err = io.Copy(outfile, part)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
        }

        // 个人资料照片部分
        if part.FormName() == "profile" {
            name := part.FileName()
            outfile, err := os.Create("uploads/" + name)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
            defer outfile.Close()

            _, err = io.Copy(outfile, part)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
        }

        // 音频文件部分
        if part.FormName() == "songs" {
            name := part.FileName()
            outfile, err := os.Create("uploads/" + name)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
            defer outfile.Close()

            _, err = io.Copy(outfile, part)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
        }

        // 视频文件部分
        if part.FormName() == "videos" {
            name := part.FileName()
            outfile, err := os.Create("uploads/" + name)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
            defer outfile.Close()

            _, err = io.Copy(outfile, part)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
        }
    }

    fmt.Println("done")
    return nil
}

Go服务器错误:
go run main.go
now serving at the following location www.localhost:3000
File Upload Endpoint Hit
INFO[0009] POST /upload elapsed="680.422μs" host= method=POST path=/upload query=
2021/07/14 15:58:32 http: panic serving [::1]:62924: runtime error: invalid memory address or nil pointer dereference

英文:

Building a backend go server that can take a form with multiple inputs and 3 of them have multiple file inputs. I searched and it states that if you want to make something like this work you don't want to use the typical

if err := r.ParseMultipartForm(32 &lt;&lt; 20); err != nil {
fmt.Println(err)
}
// get a reference to the fileHeaders
files := r.MultipartForm.File[&quot;coverArt&quot;]

and instead you should use

mr, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}

Standard form-data:

  1. Name
  2. Email
  3. Cover art photos (multiple files)
  4. Profile photos (multiple files)
  5. 2 Audio files (2 songs)
  6. 2 Videos (personal intro, recording of person in a cappella)

HTML Form

&lt;form method=&quot;post&quot; enctype=&quot;multipart/form-data&quot; action=&quot;/upload&quot;&gt;
&lt;input type=&quot;text&quot; name=&quot;name&quot;&gt;
&lt;input type=&quot;text&quot; name=&quot;email&quot;&gt;
&lt;input name=&quot;coverArt&quot; type=&quot;file&quot;  multiple /&gt;
&lt;input name=&quot;profile&quot; type=&quot;file&quot;  multiple /&gt;
&lt;input type=&quot;file&quot; name=&quot;songs&quot;  multiple /&gt;
&lt;input type=&quot;file&quot; name=&quot;videos&quot;  multiple/&gt;
&lt;button type=&quot;submit&quot;&gt;Upload File&lt;/button&gt;
&lt;/form&gt;

Go Code:

func FilePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println(&quot;File Upload Endpoint Hit&quot;)
mr, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
for {
part, err := mr.NextPart()
// This is OK, no more parts
if err == io.EOF {
break
}
// Some error
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)			
}
// CoverArt &#39;files&#39; part
if part.FormName() == &quot;coverArt&quot; {
name := part.FileName()
outfile, err := os.Create(&quot;uploads/&quot; + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
}
// Profile Pic &#39;files&#39; part
if part.FormName() == &quot;profile&quot; {
name := part.FileName()
outfile, err := os.Create(&quot;uploads/&quot; + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
}
// Songs &#39;files&#39; part
if part.FormName() == &quot;songs&quot; {
name := part.FileName()
outfile, err := os.Create(&quot;uploads/&quot; + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
}
// Video &#39;files&#39; part
if part.FormName() == &quot;videos&quot; {
name := part.FileName()
outfile, err := os.Create(&quot;uploads/&quot; + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
}
}
fmt.Println(&quot;done&quot;)
return nil
}

Go Server Error:
go run main.go [15:58:21]
now serving at the following location www.localhost:3000
File Upload Endpoint Hit
INFO[0009] POST /upload elapsed="680.422µs" host= method=POST path=/upload query=
2021/07/14 15:58:32 http: panic serving [::1]:62924: runtime error: invalid memory address or nil pointer dereference

答案1

得分: 1

很难猜测你的代码在哪里出错。可能的原因是当错误发生时,你的程序继续执行。例如,如果文件创建失败,outfile.Close() 将会出现 panic,因为 outfile 是 nil。

这两种方法都支持单个字段的多个文件。它们的区别在于它们如何处理内存。流式版本从网络中读取小部分数据,并在调用 io.Copy 时将其写入文件。另一种变体在调用 ParseMultiForm() 时将所有数据加载到内存中,因此它需要与要传输的文件大小一样多的内存。下面你将找到这两种变体的工作示例。

流式版本:

func storeFile(part *multipart.Part) error {
	name := part.FileName()
	outfile, err := os.Create("uploads/" + name)
	if err != nil {
		return err
	}
	defer outfile.Close()
	_, err = io.Copy(outfile, part)
	if err != nil {
		return err
	}
	return nil
}

func filePOST(w http.ResponseWriter, r *http.Request) error {
	fmt.Println("File Upload Endpoint Hit")
	mr, err := r.MultipartReader()
	if err != nil {
		return err
	}
	for {
		part, err := mr.NextPart()

		// This is OK, no more parts
		switch {
		case errors.Is(err, io.EOF):
			fmt.Println("done")
			return nil
		case err != nil:
			// Some error
			return err
		default:
			switch part.FormName() {
			case "coverArt", "profile", "songs", "videos":
				if err := storeFile(part); err != nil {
					return err
				}
			}
		}
	}
}

func main() {
	http.HandleFunc("/upload", func(writer http.ResponseWriter, request *http.Request) {
		err := filePOST(writer, request)
		if err != nil {
			http.Error(writer, err.Error(), http.StatusInternalServerError)
			log.Println("Error", err)
		}
	})
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

另一个使用 ParseMultipartForm 的版本,它将数据读入内存。

func storeFile(part *multipart.FileHeader) error {
name := part.Filename
infile, err := part.Open()
if err != nil {
return err
}
defer infile.Close()
outfile, err := os.Create("uploads/" + name)
if err != nil {
return err
}
defer outfile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return err
}
return nil
}
func FilePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println("File Upload Endpoint Hit")
if err := r.ParseMultipartForm(2 << 24); err != nil {
return err
}
for _, fileType := range []string{"coverArt", "profile", "songs", "videos"} {
uploadedFiles, exists := r.MultipartForm.File[fileType]
if !exists {
continue
}
for _, file := range uploadedFiles {
if err := storeFile(file); err != nil {
return err
}
}
}
return nil
}
func main() {
http.HandleFunc("/upload", func(writer http.ResponseWriter, request *http.Request) {
err := FilePOST(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
log.Println("Error", err)
}
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
英文:

It is hard to guess where your code panics. Probably the reason is that your program continue to execute when error occurs. For example if creation of file fails, outfile.Close() will panic as the outfile is nil.

Both approaches support multiple files for single field. The difference is in how they handle memory. The streaming version reads small portions of data from the network and writes it to a file when you call io.Copy. The other variant loads all the data into memory when you call ParseMultiForm(), so it requires as much memory as the size of the files you want to transfer. Below you will find working examples for both variants.

Streaming variant:

func storeFile(part *multipart.Part) error {
	name := part.FileName()
	outfile, err := os.Create(&quot;uploads/&quot; + name)
	if err != nil {
		return err
	}
	defer outfile.Close()
	_, err = io.Copy(outfile, part)
	if err != nil {
		return err
	}
	return nil
}

func filePOST(w http.ResponseWriter, r *http.Request) error {
	fmt.Println(&quot;File Upload Endpoint Hit&quot;)
	mr, err := r.MultipartReader()
	if err != nil {
		return err
	}
	for {
		part, err := mr.NextPart()

		// This is OK, no more parts
		switch {
		case errors.Is(err, io.EOF):
			fmt.Println(&quot;done&quot;)
			return nil
		case err != nil:
			// Some error
			return err
		default:
			switch part.FormName() {
			case &quot;coverArt&quot;, &quot;profile&quot;, &quot;songs&quot;, &quot;videos&quot;:
				if err := storeFile(part); err != nil {
					return err
				}
			}
		}
	}
}

func main() {
	http.HandleFunc(&quot;/upload&quot;, func(writer http.ResponseWriter, request *http.Request) {
		err := filePOST(writer, request)
		if err != nil {
			http.Error(writer, err.Error(), http.StatusInternalServerError)
			log.Println(&quot;Error&quot;, err)
		}
	})
	if err := http.ListenAndServe(&quot;:8080&quot;, nil); err != nil {
		log.Fatal(err)
	}
}

And version with ParseMultipartForm, which reads data to memory.

func storeFile(part *multipart.FileHeader) error {
name := part.Filename
infile, err := part.Open()
if err != nil {
return err
}
defer infile.Close()
outfile, err := os.Create(&quot;uploads/&quot; + name)
if err != nil {
return err
}
defer outfile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return err
}
return nil
}
func FilePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println(&quot;File Upload Endpoint Hit&quot;)
if err := r.ParseMultipartForm(2 &lt;&lt; 24); err != nil {
return err
}
for _, fileType := range []string{&quot;coverArt&quot;, &quot;profile&quot;, &quot;songs&quot;, &quot;videos&quot;} {
uploadedFiles, exists := r.MultipartForm.File[fileType]
if !exists {
continue
}
for _, file := range uploadedFiles {
if err := storeFile(file); err != nil {
return err
}
}
}
return nil
}
func main() {
http.HandleFunc(&quot;/upload&quot;, func(writer http.ResponseWriter, request *http.Request) {
err := FilePOST(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
log.Println(&quot;Error&quot;, err)
}
})
if err := http.ListenAndServe(&quot;:8080&quot;, nil); err != nil {
log.Fatal(err)
}
}

huangapple
  • 本文由 发表于 2021年7月15日 04:03:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/68384426.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定