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 seperti gambar berikut.

Dasar Pemrograman Golang - Project structure

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

B.17.2. Front End

Kali ini di bagian front end kita tidak menggunakan jQuery, cukup javascript saja tanpa library.

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 sendiri nantinya didapat dari API call ke back end. Di sini sebuah AJAX diperlukan untuk pengambilan data.

Selanjutnya, siapkan sebuah fungsi dengan nama Yo atau bisa lainnya, fungsi ini berisikan closure renderData(), getAllListFiles(), dan method init(). Buat instance object baru dari Yo, lalu akses method init(), tempatkan dalam event window.onload.

function Yo() {
    var self = this;
    var $ul = document.getElementById("list-files");

    var renderData = function (res) {
        // do stuff
    };

    var getAllListFiles = function () {
        // do stuff
    };

    self.init = function () {
        getAllListFiles();
    };
};

window.onload = function () {
    new Yo().init();
};

Closure renderData() bertugas untuk melakukan rendering data JSON ke HTML. Berikut adalah isi dari fungsi ini.

var renderData = function (res) {
    res.forEach(function (each) {
        var $li = document.createElement("li");
        var $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";
    });
};

Sedangkan closure getAllListFiles(), memiliki tugas untuk request ke back end, mengambil data list semua file. Request dilakukan dalam bentuk AJAX, nilai baliknya adalah data JSON. Setelah data sudah didapatkan, fungsi renderData() dipanggil.

var getAllListFiles = function () {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "/list-files");
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4 && xhr.status == 200) {
            var json = JSON.parse(xhr.responseText);
            renderData(json);
        }
    };
    xhr.send();
};

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 "fmt"
import "net/http"
import "html/template"
import "path/filepath"
import "io"
import "encoding/json"
import "os"

type M map[string]interface{}

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

    fmt.Println("server started at localhost:9000")
    http.ListenAndServe(":9000", nil)
}

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 oleh AJAX dari front end.

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

    err := filepath.Walk(filesLocation, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if info.IsDir() {
            return nil
        }

        files = append(files, M{"filename": info.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 di gabung 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.Walk 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.WalkFunc), kita bisa mengambil informasi tiap item satu-per satu.

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", 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