Golang 內建提供 http.FileServer ,可以方便地透過 HTTP 存取檔案系統(file system),例如以下程式碼執行之後,就可以透過瀏覽器打開網址 http://localhost:8080 瀏覽 /usr/share/doc 資料夾內的檔案。

package main

import (
    "net/http"
)

func main() {
    http.ListenAndServe(":8080", http.FileServer(http.Dir("/usr/share/doc")))
}

也因為很方便,用 Golang 建構 web application 時,也可以利用 FileServer 幫忙處理靜態檔案(static files, 例如 CSS / JS / IMAGES)等,其範例如下:

package main

import (
    "net/http"
)

func main() {
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

上述範例先使用 http.FileServer 建立當前目錄下的 static 資料夾的 FileServer Handler ,然後再利用 http.Handle 將所有路徑請求是 /static/ 開頭的 HTTP request 先利用 http.StripPrefix 將路徑的 /static/ 字串去除後,轉交給 FileServer Handler 處理。

因此假設有一個 HTTP request 的路徑是 /static/index.js 的話,就會被換成 index.js 然後交給 FileServer Handler 處理,而 FileServer Handler 會試圖取得 ./static/index.js 的內容回應,如果檔案不存在的話,就會回應 404 NOT FOUND 。

這邊要注意的是 FileServer 是可以存取資料夾的,因此假設上述範例 HTTP request 的路徑是 /static/ 的話,就會變成直接列出當前目錄下的 ./static 資料夾所有的檔案(如果你使用的是 http.Dir("/") 那就會整個作業系統的檔案被看光光,十分危險,千萬別這麼做,母湯母湯)。

要解決可以存取資料夾的問題的最簡單方法,就是在每個資料夾底下新增一個 index.html 即可。

As a special case, the returned file server redirects any request ending in “/index.html” to the same path, without the final “index.html”.

這種解決辦法適用於只有資料夾結構十分單純的情況下,如果資料夾內有多個資料夾,或者資料夾的結構很複雜,使用這種方式就相當辛苦,此時可以考慮將 FileServer 再包裝一層,解決此一困擾。以下是包裝過 FileServer 範例:

package main

import (
    "log"
    "net/http"
)

type securedFileSystem struct {
    fs http.FileSystem
}

func (sfs securedFileSystem) Open(path string) (http.File, error) {
    f, err := sfs.fs.Open(path)
    if err != nil {
        return nil, err
    }

    s, err := f.Stat()
    if s.IsDir() {
        index := strings.TrimSuffix(path, "/") + "/index.html"
        if _, err := sfs.fs.Open(index); err != nil {
            return nil, err
        }
    }

    return f, nil
}

func main() {
    fs := http.FileServer(securedFileSystem{http.Dir("./static")})
    http.Handle("/static/", http.StripPrefix("/static/", fs))
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

上述範例最主要的關鍵部分在於實作 http.FileSystem 裡的 Open ,先讓 FileSytem 打開檔案,打開後判斷是否是資料夾,如果是資料夾,進一步看是否有 index.html 存在於檔案中,如果有的話就回傳 http.File ,沒有的話就回傳 err 。利用再包裝的方式達到阻擋存取資料夾的目的。

以上為本篇 http.FileServer 的介紹。

References

https://golang.org/pkg/net/http/#FileSystem

https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings