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

2
  • I don't see what's wrong after a quick glance. I recommend starting from the http.FileServer test. That code is known to work. Commented Sep 11, 2018 at 2: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.. Commented Sep 11, 2018 at 2:51

1 Answer 1

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()
}
Sign up to request clarification or add additional context in comments.

3 Comments

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 /.
@Gavin - thank you so much for your thorough answer; it works now :) Have a terrific day :)
@YongHaoHu - indeed, when trying it out with 'other.html' and adding the leading slash it did actually work. Thanks for your help as well :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.