Tutorial Series

This is Part 2 of a tutorial series. Also see:

Summary

In this chapter I want to split up the code a bit. So my goal is to reduce the main function to its minimum and to outsource different parts of the application to different topic related files. The separate files I chose will be templates, fileserver and log.

So to illustrate what my project folder looks like after finishing this post, look at my tree output:

❯ tree -L 3
.
├── build
│   └── goshs
├── go.mod
├── internal
│   ├── myhtml
│   │   └── template.go
│   ├── myhttp
│   │   └── fileserver.go
│   └── mylog
│       └── log.go
├── main.go
├── Makefile
└── README.md

And also please notice that I chose to continue by using go modules and a git repository. I will not handle what a go module is. If you need to read up the topic look at the official go blog post on that topic.

Makefile

I am a big fan of having a Makefile around. In this case it is not a big one:

.PHONY: build

build:
	@go build -o build
	@echo "[OK] App binary was created!"

run:
	@./build/goshs

All it does is to handle build and run by executing the go equivalents. I guess this is really self explanatory.

The Templates

To be more flexible with the templates to deliver to the client I made a package called myhtml under the path internal/myhtml/template.go.

It basically consist of three templates. I added a template to handle file permission issues as you will see further down the lines.

const dispTmpl = `
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Directory listing for {{.Path}}</title>
  </head>
  <body>
    <h1>Directory listing for {{.Path}}</h1>
    <hr />
	<ul>
	  {{range .Content}}
		<li><a href="/{{.URI}}">{{.Name}}</a></li>
	  {{ end }}
	</ul>
    <hr />
  </body>
</html>
`

const notFoundTmpl = `
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 404</p>
        <p>Message: File not found.</p>
        <p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>
    </body>
</html>
`

const noAccessTmpl = `
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 500</p>
        <p>Message: No permission to access the file.</p>
        <p>Error code explanation: HTTPStatus.PERMISSION_DENIED - You have no permission to access the file.</p>
    </body>
</html>
`ternal/myhtml/template.go" >}}
internal/myhtml/template.go

Also there is a function to retrieve the different templates depending on a string.

// GetTemplate will deliver the template depending on a 'name'
func GetTemplate(name string) string {
	switch name {
	case "display":
		return dispTmpl
	case "404":
		return notFoundTmpl
	case "500":
		return noAccessTmpl
	}
	return ""
}
internal/myhtml/template.go

So this is the template.go. It will be used in the fileserver.go when handling the http requests.

The Logging

I wanted to have a better looking logging and decided to use the log standard lib to handle the logging for me. I chose to handle the logging in a separate file in a package called mylog located at internal/mylog/log.go

package mylog

import (
	"log"
)

//LogRequest will log the request in a uniform way
func LogRequest(remoteAddr, method, url, proto, status string) {
	if status == "500" || status == "404" {
		log.Printf("ERROR: %s - - \"%s %s %s\" - %+v", remoteAddr, method, url, proto, status)
		return
	}
	log.Printf("INFO:  %s - - \"%s %s %s\" - %+v", remoteAddr, method, url, proto, status)
}

//LogMessage will log an arbitrary message to the console
func LogMessage(message string) {
	log.Println(message)
}
internal/mylog/log.go

There are two main functions. They are almost the same as in the initial run of this project. So the function LogRequest will recieve the parameters needed to log a message, like remote address or url. It will then print the log line. Also the function LogMessage can log arbitrary messages. As you might see we are missing the timestamp creation. This will be handled by the library log by default.

The resulting output will look like this:

2020/10/06 14:22:07 Serving HTTP on 0.0.0.0 port 8000 from /tmp
2020/10/06 14:22:10 INFO:  [::1]:52878 - - "GET / HTTP/1.1" - 200
2020/10/06 14:22:24 INFO:  [::1]:52878 - - "GET /.battery HTTP/1.1" - 200
2020/10/06 14:22:26 ERROR: [::1]:52878 - - "GET /foo.txt HTTP/1.1" - 404
2020/10/06 14:22:26 404:   File not found

The File Server

Well, this one is the biggest change. I wanted to have a very clear defined custom FileServer we are using to handle everything related to http.

First of all I moved my previously defined structs from main.go to the new package called myhttp located at internal/myhttp/fileserver.go. Also I defined a new struct to hold the FileServers information. As I included a little new feature I not only defined the port as a field, but the webroot as well. So later on we are able to serve content from another directory than the current working dir.

type directory struct {
	Path    string
	Content []item
}

type item struct {
	URI  string
	Name string
}

// FileServer holds the fileserver information
type FileServer struct {
	Port    int
	Webroot string
}
internal/myhttp/fileserver.go

Next up I extend the struct FileServer to have three functions: router, Start and ServeHTTP. Capitalized functions will be exposed to other packages and can be called. So router will only be internal, but Start and ServerHTTP will be accessible from our main function.

// router will hook up the webroot with our fileserver
func (fs *FileServer) router() {
	http.Handle("/", fs)
}

// Start will start the file server
func (fs *FileServer) Start() {
	// init router
	fs.router()

	// Print to console
	log.Printf("Serving HTTP on 0.0.0.0 port %+v from %+v\n", fs.Port, fs.Webroot)

	add := fmt.Sprintf(":%+v", fs.Port)
	log.Panic(http.ListenAndServe(add, nil))
}

// ServeHTTP will serve the response by leveraging our handler
func (fs *FileServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	defer func() {
		if err := recover(); err != nil {
			http.Error(w, fmt.Sprintf("%+v", err), http.StatusInternalServerError)
		}
	}()

	fs.handler(w, req)
}
internal/myhttp/fileserver.go

As you can see the router function just tells our http server to handle every request within the file server itself. The function http.Handle expects a http.Handler which our file server automatically provides with the function ServerHTTP. And the Start function starts the http server with the provided port and webroot. The ServeHTTP function will call our FileServers handler after handling possible errors.

Next up the heart of our code is the FileServers handler:

// handler is the function which actually handles dir or file retrieval
func (fs *FileServer) handler(w http.ResponseWriter, req *http.Request) {
	// Get url so you can extract Headline and title
	upath := req.URL.Path

	// Ignore default browser call to /favicon.ico
	if upath == "/favicon.ico" {
		return
	}

	// Define absolute path
	open := fs.Webroot + path.Clean(upath)

	// Check if you are in a dir
	file, err := os.Open(open)
	if os.IsNotExist(err) {
		// Handle as 404
		fs.handle404(w, req)
		return
	}
	if os.IsPermission(err) {
		// Handle as 500
		fs.handle500(w, req)
		return
	}
	if err != nil {
		// Handle general error
		log.Println(err)
		return
	}
	defer file.Close()

	// Log request
	mylog.LogRequest(req.RemoteAddr, req.Method, req.URL.Path, req.Proto, "200")

	// Switch and check if dir
	stat, _ := file.Stat()
	if stat.IsDir() {
		fs.processDir(w, req, file, upath)
	} else {
		fs.sendFile(w, file)
	}

}
internal/myhttp/fileserver.go

Basically not much changed. I decided to directly open the file/directory given instead of doing a stat first. Also I extended the error checking when accessing the file/directory by handling permission denied issues.

Then I use the new mylog package to do the logging. Afterwards I switch over the result of IsDir() and call one of the two new FileServer functions processDir or sendFile.

Those are:

func (fs *FileServer) processDir(w http.ResponseWriter, req *http.Request, file *os.File, relpath string) {
	// Read directory FileInfo
	fis, err := file.Readdir(-1)
	if err != nil {
		fs.handle404(w, req)
		return
	}

	// Create empty slice
	items := make([]item, 0, len(fis))
	// Iterate over FileInfo of dir
	for _, fi := range fis {
		// Set name and uri
		itemname := fi.Name()
		itemuri := url.PathEscape(path.Join(relpath, itemname))
		// Add / to name if dir
		if fi.IsDir() {
			itemname += "/"
		}
		// define item struct
		item := item{
			Name: itemname,
			URI:  itemuri,
		}
		// Add to items slice
		items = append(items, item)
	}

	// Sort slice all lowercase
	sort.Slice(items, func(i, j int) bool {
		return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
	})

	// Template parsing and writing to browser
	t := template.New("index")
	t.Parse(myhtml.GetTemplate("display"))
	d := &directory{Path: relpath, Content: items}
	t.Execute(w, d)
}

func (fs *FileServer) sendFile(w http.ResponseWriter, file *os.File) {
	// Write to browser
	io.Copy(w, file)
}
internal/myhttp/fileserver.go

Also here are the functions of FileServer to handle different errors:

func (fs *FileServer) handle404(w http.ResponseWriter, req *http.Request) {
	mylog.LogRequest(req.RemoteAddr, req.Method, req.URL.Path, req.Proto, "404")
	mylog.LogMessage("404:   File not found")
	t := template.New("404")
	t.Parse(myhtml.GetTemplate("404"))
	t.Execute(w, nil)
}

func (fs *FileServer) handle500(w http.ResponseWriter, req *http.Request) {
	mylog.LogRequest(req.RemoteAddr, req.Method, req.URL.Path, req.Proto, "500")
	mylog.LogMessage("500:   No permission to access the file")
	t := template.New("500")
	t.Parse(myhtml.GetTemplate("500"))
	t.Execute(w, nil)
}
internal/myhttp/fileserver.go

With everything setup like this the code is much more readable and understandable and more flexible for later modifications. You can implement new templates by extending the template.go for example. Or if you decide to implement an upload function you extend the fileserver.go file and start like this:

func (fs *FileServer) upload(w http.ResponseWriter, req *http.Request) {

}

The main.go

So to use our new SimpleHTTPServer I had to change the main.go, as well. And what is left in here is minimal.

var (
	port    = 8000
	webroot = "."
)

func init() {
	wd, _ := os.Getwd()

	// flags
	flag.IntVar(&port, "p", port, "The port")
	flag.StringVar(&webroot, "d", wd, "Web root directory")

	flag.Parse()
}

func main() {
	server := &myhttp.FileServer{Port: port, Webroot: webroot}
	server.Start()
}
main.go

First the defaults are defined. Then I use the standard library flag within the init() function to handle the arguments one can hand to our application. Also It produces a help page automatically:

❯ ./build/goshs -h
Usage of ./build/goshs:
  -d string
    	Web root directory (default "/tmp")
  -p int
    	The port (default 8000)

The init() function in main.go is called once before executing the main function. So there is no need to call it seperately.

Finally in the main() function we initiate our custom myhttp.FileServer and start it.

That is all there is for todays tutorial. If you have questions do not hestitate to ask in the comment section below. Also check out the full code at github.com/patrickhener/goshs - tag v.0.0.2.

Thanks for taking the time to read through and have a nice day.

Patrick