1

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..

nomadist
  • 13
  • 1
  • 4
  • I don't see what's wrong after a quick glance. I recommend starting from [the http.FileServer test](https://github.com/golang/go/blob/0f72e79856d246af85c449f9e5a357ba751cd234/src/net/http/fs_test.go#L599-L662). That code is known to work. – Charlie Tumahai Sep 11 '18 at 02:48
  • Thanks for the quick reply :) It compiles fine but the result is invariably "404 not found"... I'll have a look at the fileserver test thanks; although i think the problem is likely to be in the map - to filesystem part.. – nomadist Sep 11 '18 at 02:51

1 Answers1

2

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()
}
Gavin
  • 4,365
  • 1
  • 18
  • 27
  • In fact, I can figure out this by add debug message. One question, `"/index.html"` not works as expected because browser treat `index.html` as `/`. – chris Sep 11 '18 at 04:46
  • @Gavin - thank you so much for your thorough answer; it works now :) Have a terrific day :) – nomadist Sep 11 '18 at 05:25
  • @YongHaoHu - indeed, when trying it out with 'other.html' and adding the leading slash it did actually work. Thanks for your help as well :) – nomadist Sep 11 '18 at 05:26