goembeddedwebserver

Serving a map of strings without 3rd party libraries using http.filesystem in Go (Golang)


I am just starting out with Go and am trying to learn how to build a simple web app without using 3rd party libraries / packages.

Using this post and this code as a guideline, I've hacked the following together:

package main

import (
    "bytes"
    "net/http"
    "os"
    "path"
    "time"
)

type StaticFS map[string]*staticFile

type staticFile struct {
    name string
    data []byte
    fs   StaticFS
}

func LoadAsset(name string, data string, fs StaticFS) *staticFile {
    return &staticFile{name: name,
        data: []byte(data),
        fs:   fs}
}

func (fs StaticFS) prepare(name string) (*staticFile, error) {
    f, present := fs[path.Clean(name)]
    if !present {
        return nil, os.ErrNotExist
    }
    return f, nil
}

func (fs StaticFS) Open(name string) (http.File, error) {
    f, err := fs.prepare(name)
    if err != nil {
        return nil, err
    }
    return f.File()
}

func (f *staticFile) File() (http.File, error) {
    type httpFile struct {
        *bytes.Reader
        *staticFile
    }
    return &httpFile{
        Reader:     bytes.NewReader(f.data),
        staticFile: f,
    }, nil
}

//implement the rest of os.FileInfo
func (f *staticFile) Close() error {
    return nil
}

func (f *staticFile) Stat() (os.FileInfo, error) {
    return f, nil
}

func (f *staticFile) Readdir(count int) ([]os.FileInfo, error) {
    return nil, nil
}

func (f *staticFile) Name() string {
    return f.name
}

func (f *staticFile) Size() int64 {
    return int64(len(f.data))
}

func (f *staticFile) Mode() os.FileMode {
    return 0
}

func (f *staticFile) ModTime() time.Time {
    return time.Time{}
}

func (f *staticFile) IsDir() bool {
    return false
}

func (f *staticFile) Sys() interface{} {
    return f
}

func main() {

    const HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<p>Hello World</p>
</main>
</body>
</html>
`

    const CSS = `
p {
    color:red;
    text-align:center;
} 
`
    ASSETS := make(StaticFS)
    ASSETS["index.html"] = LoadAsset("index.html", HTML, ASSETS)
    ASSETS["style.css"] = LoadAsset("style.css", CSS, ASSETS)
    http.Handle("/", http.FileServer(ASSETS))
    http.ListenAndServe(":8080", nil)
}

Which compiles fine, but doesn't actually produce any results other than 404 page not found..

What I want to achieve is having a package in my app that allows me to make a map, embed some static content such as css and js in it and then serve it with http.Handle - Without using 3rd party tools like go-bindata, rice or anything else.

Any help would be greatly appreciated..


Solution

  • Here is the main code we will need to look at, which comes from the source regarding http.FileServer:

    func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
        upath := r.URL.Path
        if !strings.HasPrefix(upath, "/") {
            upath = "/" + upath
            r.URL.Path = upath
        }
        serveFile(w, r, f.root, path.Clean(upath), true)
    }
    
    // name is '/'-separated, not filepath.Separator.
    func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
        const indexPage = "/index.html"
    
        // redirect .../index.html to .../
        // can't use Redirect() because that would make the path absolute,
        // which would be a problem running under StripPrefix
        if strings.HasSuffix(r.URL.Path, indexPage) {
            localRedirect(w, r, "./")
            return
        }
    
        f, err := fs.Open(name)
        if err != nil {
            msg, code := toHTTPError(err)
            Error(w, msg, code)
            return
        }
        defer f.Close()
    
        ...
    }
    

    In the ServeHTTP method, you will see a call to an unexported function.

    serveFile(w, r, f.root, path.Clean(upath), true)
    

    where upath is the request's URL path that is guaranteed to begin with "/".

    In serveFile, fs.Open(name) is called, where fs is the FileSystem you provided and name is the argument we passed as path.Clean(upath). Note that path.Clean is already being called, so you should not need to call this in your prepare method.

    The takeaway here is that you are storing your "file names" without a preceding "/", which would represent they are in the root of the filesystem.

    You can fix this two different ways.

    1.

    ASSETS["/index.html"] = LoadAsset("index.html", HTML, ASSETS)
    ASSETS["/style.css"] = LoadAsset("style.css", CSS, ASSETS)
    

    2.

    func (fs StaticFS) Open(name string) (http.File, error) {
        if strings.HasPrefix(name, "/") {
            name = name[1:]
        }
        f, err := fs.prepare(name)
        if err != nil {
            return nil, err
        }
        return f.File()
    }