Skip to content

Commit d8b7fce

Browse files
committed
feat: ⚡ add multipart file upload
add multipart file upload
0 parents  commit d8b7fce

File tree

9 files changed

+331
-0
lines changed

9 files changed

+331
-0
lines changed

.github/workflows/go.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Go
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
branches:
9+
- master
10+
jobs:
11+
build_and_test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Setup go-task
15+
uses: pnorton5432/setup-task@v1
16+
with:
17+
task-version: 3.29.1
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
- name: Setup Go
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version: 'stable'
24+
check-latest: true
25+
- name: Task Build
26+
run: task build

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+
20+
# Go workspace file
21+
go.work
22+
go.work.sum
23+
24+
# env file
25+
.env
26+
bin
27+
uploads

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# golang-multipart-upload-sample
2+
3+
This repository is for demo how to implement multipart file upload on golang
4+
5+
## what is multipart upload?
6+
7+
A way to send large files by breaking them into smaller parts and encoding them into a single HTTP request.
8+
9+
## Multipart upload vs regular upload
10+
11+
* Encoding: Multipart MIME format vs plain binary/text
12+
* File size: Unlimited (chunks) vs request size limits.
13+
* Error handling: Retry chunks vs re-upload everything.
14+
* Flexibility: Supports extra metadata easily

Taskfile.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
version: '3'
2+
3+
tasks:
4+
default:
5+
cmds:
6+
- echo "This is task cmd"
7+
silent: true
8+
9+
build-server:
10+
cmds:
11+
- CGO_ENABLED=0 GOOS=linux go build -o bin/server cmd/server/main.go
12+
silent: true
13+
run-server:
14+
cmds:
15+
- ./bin/server
16+
deps:
17+
- build-server
18+
silent: true
19+
build-client:
20+
cmds:
21+
- CGO_ENABLED=0 GOOS=linux go build -o bin/client cmd/client/main.go
22+
silent: true
23+
run-client:
24+
cmds:
25+
- ./bin/client
26+
deps:
27+
- build-client
28+
silent: true
29+
coverage:
30+
cmds:
31+
- go test -v -cover ./...
32+
silent: true
33+
test:
34+
cmds:
35+
- go test -v ./...
36+
silent: true
37+

client/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>File Upload</title>
7+
</head>
8+
<body>
9+
<h1>Upload a File</h1>
10+
<form action="http://localhost:8080/upload" method="post">
11+
<!-- File Input-->
12+
<label for="file">Choose a file</label>
13+
<input type="file" id="file" name="file" required>
14+
<br><br>
15+
<!-- Submit Button -->
16+
<button type="submit">Upload</button>
17+
</form>
18+
<br>
19+
<br>
20+
<h1>Upload a File - Multipart</h1>
21+
<form action="http://localhost:8080/upload_multipart" method="post" enctype="multipart/form-data">
22+
<!-- File Input-->
23+
<label for="file">Choose a file</label>
24+
<input type="file" id="file" name="file" required>
25+
<br><br>
26+
<!-- Submit Button -->
27+
<button type="submit">Upload</button>
28+
</form>
29+
</body>
30+
</html>

cmd/client/main.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"log"
9+
"mime/multipart"
10+
"net/http"
11+
"os"
12+
)
13+
14+
func main() {
15+
// Define a command-line flag for the file path
16+
filePath := flag.String("file", "", "Path to the file upload (required)")
17+
flag.Parse()
18+
19+
// Validate the file path
20+
if *filePath == "" {
21+
log.Fatal("Error: file path is required. Use the -file flag to specify the file")
22+
}
23+
// Open the file
24+
file, err := os.Open(*filePath)
25+
if err != nil {
26+
log.Fatalf("Error opening file: %v", err)
27+
}
28+
defer file.Close()
29+
30+
// Create a buffer to store the request body
31+
var buf bytes.Buffer
32+
// Create a new multipart writer with the buffer
33+
w := multipart.NewWriter(&buf)
34+
35+
// Create a new form field for the file
36+
fw, err := w.CreateFormFile("file", file.Name())
37+
if err != nil {
38+
log.Fatalf("Error creating form file: %v", err)
39+
}
40+
// Copy the contents of the file to the form field
41+
if _, err := io.Copy(fw, file); err != nil {
42+
log.Fatalf("Error copying file content %v", err)
43+
}
44+
45+
// Close the multipart writer to finalize the request
46+
if err := w.Close(); err != nil {
47+
log.Fatalf("Error closing multipart writer: %v", err)
48+
}
49+
50+
// Create the HTTP request
51+
req, err := http.NewRequest(http.MethodPost, "http://localhost:8080/upload_multipart", &buf)
52+
if err != nil {
53+
log.Fatalf("error creating request: %v", err)
54+
}
55+
req.Header.Set("Content-Type", w.FormDataContentType())
56+
57+
// Send the request
58+
client := &http.Client{}
59+
resp, err := client.Do(req)
60+
if err != nil {
61+
log.Fatalf("error sending request: %v", err)
62+
}
63+
defer resp.Body.Close()
64+
65+
// Print the response
66+
fmt.Printf("Response status: %s\n", resp.Status)
67+
}

cmd/server/main.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"net/http"
6+
7+
"github.com/leetcode-golang-classroom/golang-multipart-upload-sample/internal/controller"
8+
)
9+
10+
func HelloHandler(w http.ResponseWriter, r *http.Request) {
11+
w.Write([]byte("Hello, World!"))
12+
}
13+
14+
func main() {
15+
addr := ":8080"
16+
17+
mux := http.NewServeMux()
18+
mux.HandleFunc("GET /", HelloHandler)
19+
mux.HandleFunc("POST /upload", controller.FileUpload)
20+
mux.HandleFunc("POST /upload_multipart", controller.FileUploadMultipart)
21+
22+
log.Printf("server is listening at %s", addr)
23+
log.Fatal(http.ListenAndServe(addr, mux))
24+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/leetcode-golang-classroom/golang-multipart-upload-sample
2+
3+
go 1.22.4

internal/controller/file.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package controller
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"os"
8+
"path/filepath"
9+
)
10+
11+
func FileUpload(w http.ResponseWriter, r *http.Request) {
12+
// Limit the size of the incoming request body to prevent abuse (e.g. 100MB)
13+
const maxUploadSize = 100 << 20 // 100 MB
14+
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
15+
16+
// Parse the form data
17+
if err := r.ParseForm(); err != nil {
18+
http.Error(w, "Error parsing form", http.StatusBadRequest)
19+
return
20+
}
21+
22+
// Get the filename
23+
filename := r.FormValue("file")
24+
if filename == "" {
25+
http.Error(w, "Filename query parameter is required", http.StatusBadRequest)
26+
return
27+
}
28+
29+
// Define the destination path
30+
uploadDir := "./uploads"
31+
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
32+
http.Error(w, "Failed to create upload directory: "+err.Error(), http.StatusInternalServerError)
33+
return
34+
}
35+
36+
destPath := filepath.Join(uploadDir, filename)
37+
// Create the destination file
38+
destFile, err := os.Create(destPath)
39+
if err != nil {
40+
http.Error(w, "failed to create destination file: "+err.Error(), http.StatusInternalServerError)
41+
return
42+
}
43+
defer destFile.Close()
44+
45+
// Copy the raw body to the destination file
46+
if _, err := io.Copy(destFile, r.Body); err != nil {
47+
http.Error(w, "failed to save file:"+err.Error(), http.StatusInternalServerError)
48+
return
49+
}
50+
// Respond with a success message
51+
w.WriteHeader(http.StatusOK)
52+
fmt.Fprintf(w, "File uploaded successfully: %s\n", filename)
53+
}
54+
55+
func FileUploadMultipart(w http.ResponseWriter, r *http.Request) {
56+
// Limit the size of the incoming request body to prevent abuse (e.g. 100MB)
57+
const maxUploadSize = 100 << 20 // 100 MB
58+
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
59+
60+
// Parse the multipart form
61+
if err := r.ParseMultipartForm(10 << 20); err != nil {
62+
http.Error(w, "failed to parse multipart form: "+err.Error(), http.StatusBadRequest)
63+
return
64+
}
65+
66+
// Retrieve the file from the form data
67+
file, fileHeader, err := r.FormFile("file")
68+
if err != nil {
69+
http.Error(w, "unable to retrieve file from form:"+err.Error(), http.StatusBadRequest)
70+
return
71+
}
72+
defer file.Close()
73+
74+
fmt.Printf("Uploaded File: %+v\n", fileHeader.Filename)
75+
fmt.Printf("File Size: %+v\n", fileHeader.Size)
76+
fmt.Printf("MIME Header: %+v\n", fileHeader.Header)
77+
78+
// Define the destination path
79+
uploadDir := "./uploads"
80+
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
81+
http.Error(w, "failed to create upload directory: "+err.Error(), http.StatusInternalServerError)
82+
return
83+
}
84+
destPath := filepath.Join(uploadDir, fileHeader.Filename)
85+
86+
// Create the destination file
87+
destFile, err := os.Create(destPath)
88+
if err != nil {
89+
http.Error(w, "failed to create destination file: "+err.Error(), http.StatusInternalServerError)
90+
return
91+
}
92+
defer destFile.Close()
93+
94+
// Copy the file's content to the destination file
95+
if _, err := io.Copy(destFile, file); err != nil {
96+
http.Error(w, "Failed to save file:"+err.Error(), http.StatusInternalServerError)
97+
return
98+
}
99+
100+
// Respond with a success message
101+
w.WriteHeader(http.StatusOK)
102+
fmt.Fprintf(w, "File uploaded successfully: %s\n", fileHeader.Filename)
103+
}

0 commit comments

Comments
 (0)