B.16. Multiple File Upload
Pada chapter ini, kita akan belajar 3 hal sekaligus yang mencakup poin-poin berikut:
- Cara untuk upload file secara asinkron
- Cara untuk handle upload banyak file sekaligus
- Cara handle upload file yang lebih hemat memori
Sebelumnya, pada chapter B.13. File Upload: ParseMultipartForm, pemrosesan file upload dilakukan lewat ParseMultipartForm, sedangkan pada chapter ini metode yang dipakai berbeda, yaitu MultipartReader.
Kelebihan dari MultipartReader adalah, file yang di upload tidak disimpan pada file temporary di lokal terlebih dahulu (tidak seperti ParseMultipartForm), melainkan data file bisa diambil langsung dari stream io.Reader.
Cara penerapan MultipartReader ini membutuhkan front-end untuk melakukan upload file secara asynchronous menggunakan objek FormData. Semua file yang akan di-upload diambil konten dan metadatanya menggunakan JavaScript untuk dimasukkan ke objek FormData. Setelahnya, objek tersebut dijadikan sebagai payload HTTP request.
B.16.1. Struktur Folder Proyek
Mari praktikkan, pertama siapkan proyek dengan struktur berikut.
chapter-B.16-ajax-multi-upload/
├── files/
├── main.go
└── view.html
B.16.2. Front End
Buka view.html, siapkan template dasar view. Dalam file ini terdapat satu buah inputan upload file yang mendukung multi-upload, dan satu buah tombol submit.
Untuk mengaktifkan kapabilitas multi upload, cukup tambahkan atribut multiple pada input file.
<!DOCTYPE html>
<html>
<head>
<title>Multiple Upload</title>
<script>
document.addEventListener("DOMContentLoaded", function () {
// javascript code goes here
});
</script>
</head>
<body>
<form id="user-form" method="post" action="/upload">
<input required multiple id="upload-file" type="file" />
<br />
<button id="btn-upload" type="submit">Upload!</button>
</form>
</body>
</html>
Override event submit pada form #user-form, handler event ini berisikan proses mulai pembentukan objek FormData dari file-file yang telah di-upload, hingga pengiriman HTTP request.
document.getElementById("user-form").addEventListener("submit", async function (e) {
e.preventDefault();
const form = e.target;
const files = document.getElementById("upload-file").files;
const formData = new FormData();
for (const file of files) {
formData.append("files", file);
}
try {
const res = await fetch(form.action, {
method: form.method,
body: formData,
});
const text = await res.text();
if (!res.ok) throw new Error(text);
alert(text);
form.reset();
} catch (err) {
alert("ERROR: " + err.message);
}
});
Objek input file memiliki properti .files yang berisi daftar semua file yang dipilih user. File-file tersebut diiterasi menggunakan for...of, setiap item dimasukkan ke dalam objek FormData dengan key "files".
Fungsi fetch() dipanggil dengan method dan body dari form. Ketika body berupa objek FormData, browser secara otomatis mengatur header Content-Type ke multipart/form-data beserta boundary yang diperlukan, sehingga tidak perlu di-set secara manual. Keyword async/await digunakan untuk menunggu respons secara asinkron.
B.16.3. Back-End
Ada 2 route handler yang harus dipersiapkan di back-end. Pertama adalah rute / untuk keperluan memunculkan form upload, dan rute /upload untuk pemrosesan upload file.
Buka file main.go, import package yang diperlukan, lalu deklarasikan dua rute tersebut.
package main
import (
"html/template"
"io"
"log"
"net/http"
"os"
"path/filepath"
)
func main() {
http.HandleFunc("/", handleIndex)
http.HandleFunc("/upload", handleUpload)
log.Println("server started at localhost:9000")
err := http.ListenAndServe(":9000", nil)
if err != nil {
log.Fatal(err)
}
}
Buat handler rute /, parsing template view view.html.
func handleIndex(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("view.html"))
if err := tmpl.Execute(w, nil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
Sebelumnya, pada chapter B.13. Form Upload File, metode yang digunakan untuk handle file upload adalah ParseMultipartForm. Cara kerjanya, file diproses dalam memori dengan alokasi tertentu, dan jika melebihi alokasi maka akan disimpan pada temporary file.
Metode tersebut kurang tepat guna jika digunakan untuk memproses file yang ukurannya besar (file size melebihi maxMemory) atau jumlah file-nya sangat banyak (memakan waktu, karena isi dari masing-masing file akan ditampung pada file temporary sebelum benar-benar di-copy ke file tujuan).
Solusi dari dua masalah yang telah disebutkan adalah menggunakan MultipartReader untuk handling file upload. Lewat metode ini, file destinasi isi di-copy langsung dari stream io.Reader tanpa butuh file temporary untuk perantara.
Kembali ke bagian perkodingan, siapkan fungsi handleUpload, isinya kode berikut.
func handleUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Only accept POST request", http.StatusBadRequest)
return
}
basePath, _ := os.Getwd()
reader, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ...
}
Bisa dilihat, method .MultipartReader() dipanggil dari objek request milik handler. Operasi tersebut menghasilkan dua nilai balik, *multipart.Reader dan error (jika ada).
Selanjutnya, lakukan perulangan terhadap objek reader. Setiap file yang di-upload diproses di masing-masing perulangan. Setelah looping berakhir, idealnya semua file sudah terproses dengan benar.
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
fileLocation := filepath.Join(basePath, "files", part.FileName())
dst, err := os.Create(fileLocation)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := io.Copy(dst, part); err != nil {
dst.Close()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dst.Close()
}
w.Write([]byte(`all files uploaded`))
Method .NextPart() mengembalikan 2 informasi, yaitu objek stream io.Reader (dari file yg di upload), dan error.
File destinasi disiapkan kemudian diisi dengan data dari stream file, menggunakan io.Copy().
Perlu diperhatikan,
dst.Close()dipanggil secara eksplisit (bukan menggunakandefer) karena kode ini berada di dalam perulangan. Penggunaandeferdi dalam loop akan menunda eksekusiClose()hingga fungsi selesai, bukan di akhir setiap iterasi. Akibatnya, semua file descriptor tetap terbuka selama proses upload berlangsung. Alternatif lain adalah membungkus isi loop dalam anonymous function:func() { defer dst.Close(); ... }()— dengan cara inideferbekerja di akhir tiap iterasi.
Jika reader.NextPart() mengembalikan error io.EOF, maka bisa disimpulkan semua file telah diproses, kemudian perulangan dihentikan.
OK, semua persiapan sudah cukup, selanjutnya masuk fase testing.
B.16.4. Testing
Buka browser, test program yang telah dibuat. Coba lakukan pengujian dengan beberapa buah file.

Cek apakah file sudah terupload.
