B.17. Download File

Sebelumnya kita telah belajar cara untuk handle upload file, kali ini kita akan belajar bagaimana cara membuat HTTP handler yang menghasilkan response berbentuk download file.

Sebenarnya download file bisa dengan mudah di-implementasikan menggunakan teknik routing static file, lalu langsung mengakses url assets di browser. Namun outcome dari teknik ini sangat tergantung default konfigurasi browser. Tiap browser memiliki behaviour berbeda, ada yang akan merespon dengan membuka file di tab, ada juga yang merespon dengan men-download file tersebut.

Dengan penerapan teknik yang dibahas pada chapter ini, file bisa dipastikan di-download oleh browser sewaktu diakses.

B.17.1. Struktur Folder Proyek

OK, pertama siapkan terlebih dahulu proyek dengan struktur berikut.

chapter-B.17-download-file/
├── files/
├── main.go
└── view.html

File yang berada di folder files adalah dummy file. Silakan gunakan file apapun dengan jumlah berapapun untuk keperluan praktik ini.

B.17.2. Front End

Kali ini di bagian front end kita tidak menggunakan library tambahan, cukup JavaScript native.

Pertama siapkan dahulu template nya, isi file view.html dengan kode berikut.

<!DOCTYPE html>
<html>
    <head>
        <title>Download file</title>
        <script>
            // javascript code goes here
        </script>
    </head>
    <body>
        <ul id="list-files"></ul>
    </body>
</html>

Tag <ul /> nantinya diisi dengan data list file yang ada dalam folder files. Data list file didapat dari HTTP request ke back end. Setelah data diterima, fungsi renderData() dipanggil untuk merender hasilnya ke HTML.

Siapkan event listener DOMContentLoaded, di dalamnya definisikan fungsi renderData() dan lakukan HTTP request ke /list-files.

document.addEventListener("DOMContentLoaded", async function () {
    const ul = document.getElementById("list-files");

    const renderData = function (files) {
        // do stuff
    };

    try {
        const res = await fetch("/list-files");
        if (!res.ok) throw new Error(await res.text());
        const files = await res.json();
        renderData(files);
    } catch (err) {
        alert("ERROR: " + err.message);
    }
});

Fungsi renderData() bertugas untuk melakukan rendering data JSON ke HTML. Berikut adalah isinya.

const renderData = function (files) {
    files.forEach(function (each) {
        const li = document.createElement("li");
        const a = document.createElement("a");

        li.innerText = "download ";
        li.appendChild(a);
        ul.appendChild(li);

        a.href = "/download?path=" + encodeURI(each.path);
        a.innerText = each.filename;
        a.target = "_blank";
    });
};

Setiap item file dirender sebagai elemen <li> berisi link <a> yang mengarah ke endpoint /download?path=.... Klik link tersebut akan memicu download file di browser.

HTTP request ke /list-files dilakukan menggunakan fetch(). Karena fetch() mengembalikan Promise, keyword async/await digunakan untuk menunggu respons. Respons di-parse sebagai JSON menggunakan res.json(), hasilnya diteruskan ke renderData().

B.17.3. Back End

Pindah ke bagian back end. Siapkan beberapa hal pada main.go, import package, siapkan fungsi main, dan buat beberapa rute.

package main

import (
    "encoding/json"
    "fmt"
    "html/template"
    "io"
    "log"
    "net/http"
    "os"
    "path/filepath"
)

type M map[string]interface{}

func main() {
    http.HandleFunc("/", handleIndex)
    http.HandleFunc("/list-files", handleListFiles)
    http.HandleFunc("/download", handleDownload)

    log.Println("server started at localhost:9000")
    err := http.ListenAndServe(":9000", nil)
    if err != nil {
        log.Fatal(err)
    }
}

Buat handler untuk rute /.

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)
    }
}

Lalu siapkan juga route handler /list-files. Isi dari handler ini adalah membaca semua file yang ada pada folder files untuk kemudian dikembalikan sebagai output berupa JSON. Endpoint ini akan diakses dari front end.

func handleListFiles(w http.ResponseWriter, r *http.Request) {
    files := []M{}
    basePath, _ := os.Getwd()
    filesLocation := filepath.Join(basePath, "files")

    err := filepath.WalkDir(filesLocation, func(path string, d os.DirEntry, err error) error {
        if err != nil {
            return err
        }

        if d.IsDir() {
            return nil
        }

        files = append(files, M{"filename": d.Name(), "path": path})
        return nil
    })
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    res, err := json.Marshal(files)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(res)
}

Fungsi os.Getwd() mengembalikan informasi absolute path di mana aplikasi di-eksekusi. Path tersebut kemudian digabung dengan folder bernama files lewat fungsi filepath.Join.

Fungsi filepath.Join akan menggabungkan item-item dengan path separator sesuai dengan sistem operasi di mana program dijalankan. \ untuk Windows dan / untuk Linux/Unix.

Fungsi filepath.WalkDir berguna untuk operasi list isi folder, apa yang ada di dalamnya (baik itu file maupun folder) akan diiterasi. Dengan memanfaatkan callback parameter kedua fungsi ini (yang bertipe filepath.WalkDirFunc), kita bisa mengambil informasi tiap item satu-per satu.

Sejak Go 1.16, filepath.WalkDir direkomendasikan sebagai pengganti filepath.Walk. Fungsi ini lebih efisien karena callback-nya menerima os.DirEntry (bukan os.FileInfo), sehingga tidak perlu melakukan os.Stat tambahan untuk setiap entry.

Selanjutnya siapkan handler untuk /download. Implementasi teknik download pada dasarnya sama pada semua bahasa pemrograman, yaitu dengan memainkan isi dari header Content-Disposition pada HTTP response.

func handleDownload(w http.ResponseWriter, r *http.Request) {
    if err := r.ParseForm(); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    path := r.FormValue("path")
    f, err := os.Open(path)
    if f != nil {
        defer f.Close()
    }
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    contentDisposition := fmt.Sprintf("attachment; filename=%s", filepath.Base(f.Name()))
    w.Header().Set("Content-Disposition", contentDisposition)

    if _, err := io.Copy(w, f); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

Content-Disposition adalah salah satu ekstensi MIME protocol, berguna untuk menginformasikan browser bagaimana mereka harus berinteraksi dengan output API endpoint.

Ada banyak jenis value content-disposition, salah satunya adalah attachment. Pada kode di atas, header Content-Disposition: attachment; filename=filename.json menghasilkan output response berupa attachment atau file, yang kemudian akan di-download oleh browser.

Objek file yang direpresentasikan variabel f, isinya di-copy ke objek response lewat statement io.Copy(w, f).

B.17.4. Testing

Jalankan program, akses rute /. List semua file dalam folder files muncul di sana. Klik salah satu file untuk men-download-nya.

Dasar Pemrograman Golang - Download file