[goshs] Part #4 - Eyecandy, anyone?
Tutorial Series
This is Part 4 of a tutorial series. Also see:
- Part#1 - My take on SimpleHTTPServer in go
- Part#2 - Trying to achieve code quality
- Part#3 - I can haz featurez?
- Part#4 - Eyecandy, anyone?
Summary
In this chapter I want to implement a bit of eyecandy. I was inspired by a beautiful project called updog by sc0tfree. His solution is almost the same but written in python. I decided to take his html construct and transform it to goshs
rewriting it to fit my needs.
Embedding Files
Well, to be honest I dislike the fact that I will have to use a third-party library for this purpose, but when it comes to this topic parcello
is doing its job pretty well. Also there is this proposal which might get integrated into go 16.1
soonish. So I might come back to this topic and migrate to a standard library later on.
Static folder
To get parcello working I created a new folder called static
. And within this folder I defined a package.go
with this content:
package static
//go:generate go run github.com/phogolabs/parcello/cmd/parcello -r -i *.go
The package is basically empty but there is a comment to tell go generate
to run the command parcello -r -i *.go
everytime. We will see what this does in a second.
Referencing embedded files
Also you will need to import this package where you plan to use parcello embedded files. So in internal/myhttp/fileserver.go
we make sure to import parcello and our static package as so:
"github.com/phogolabs/parcello"
// This will import for bundling with parcello
_ "github.com/patrickhener/goshs/static"
So what is going on here, you ask? The _
imports the package but omits the compiler error when not using an imported package (which we actually do not).
Workflow optimization
It is mandatory that you run go generate
before you go build
for this to work, as mentioned above. So to have a better workflow I rewrote my Makefile
:
.PHONY: build
generate:
@echo "[*] Minifying css and js"
@find static/ -type f -name "*.js" ! -name "*.min.*" -exec echo {} \; -exec uglifyjs -o {}.min.js {} \;
@find static/ -type f -name "*.css" ! -name "*.min.*" -exec echo {} \; -exec uglifycss --output {}.min.css {} \;
@echo "[OK] Done minifying things"
@echo "[*] Embedding via parcello"
@PARCELLO_RESOURCE_DIR=./static go generate ./...
@echo "[OK] Done bundeling things"
build: clean generate
@echo "[*] go mod dowload"
@go mod download
@echo "[*] Building for linux"
@GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dist/linux_amd64/goshs
@GOOS=linux GOARCH=386 go build -ldflags="-s -w" -o dist/linux_386/goshs
@echo "[*] Building for windows"
@GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o dist/windows_amd64/goshs.exe
@GOOS=windows GOARCH=386 go build -ldflags="-s -w" -o dist/windows_386/goshs.exe
@echo "[*] Building for mac"
@GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o dist/darwin_amd64/goshs
@echo "[*] Building for arm"
@GOOS=linux GOARCH=arm GOARM=5 go build -ldflags="-s -w" -o dist/arm_5/goshs
@GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o dist/arm_6/goshs
@GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w" -o dist/arm_7/goshs
@GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o dist/arm64_8/goshs
@echo "[OK] App binary was created!"
run:
@go run main.go
install:
@go install ./...
@echo "[OK] Application was installed to go binary directory!"
clean:
@rm -rf ./dist
@echo "[OK] Cleaned up!"
There is now going on a lot. Let’s talk about the generate
part. Besides minifying css and js (you will need to install the corresponding tools yourself), I am running this command: @PARCELLO_RESOURCE_DIR=./static go generate ./...
. This will tell go to recursively embed all files in the static directory into the build process.
Also you can see I enhanced my build method a bit to build executables for different operating systems.
When running make generate
it does look like this (I already have a lot of static files in my folder):
❯ make generate
[*] Minifying css and js
static/js/main.js
static/css/style.css
[OK] Done minifying things
[*] Embedding via parcello
Compressing 'css/bootstrap.min.css'
Compressing 'css/bootstrap.min.css.map'
Compressing 'css/style.css'
Compressing 'css/style.css.min.css'
Compressing 'fonts/FiraCode-VF.woff'
Compressing 'fonts/FiraCode-VF.woff2'
Compressing 'images/favicon.gif'
Compressing 'images/goshs-logo.png'
Compressing 'js/jquery-3.5.1.min.js'
Compressing 'js/main.js'
Compressing 'js/main.js.min.js'
Compressing 'templates/404.html'
Compressing 'templates/500.html'
Compressing 'templates/index.html'
Compressing 'vendor/datatable/jquery.dataTables.min.css'
Compressing 'vendor/datatable/jquery.dataTables.min.js'
Compressing 'vendor/fontawesome-5.15.1/css/all.min.css'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-brands-400.eot'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-brands-400.svg'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-brands-400.ttf'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-brands-400.woff'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-brands-400.woff2'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-regular-400.eot'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-regular-400.svg'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-regular-400.ttf'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-regular-400.woff'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-regular-400.woff2'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-solid-900.eot'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-solid-900.svg'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-solid-900.ttf'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-solid-900.woff'
Compressing 'vendor/fontawesome-5.15.1/webfonts/fa-solid-900.woff2'
Compressing 'vendor/images/sort_asc.png'
Compressing 'vendor/images/sort_asc_disabled.png'
Compressing 'vendor/images/sort_both.png'
Compressing 'vendor/images/sort_desc.png'
Compressing 'vendor/images/sort_desc_disabled.png'
Embedding 37 resource(s) at 'resource.go'
[OK] Done bundeling things
This process will result in a file resource.go
in our static directory:
// Code generated by parcello; DO NOT EDIT.
// Package static contains embedded resources
package static
import "github.com/phogolabs/parcello"
func init() {
parcello.AddResource([]byte{
80, 75, 3, 4, 20, 0, 8, 0, 8, 0, 243, 72, 77, 81, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 9, 0, 99, 115, 115, 47,
98, 111, 111, 116, 115, 116, 114, 97, 112, 46, 109, 105, 110,
46, 99, 115, 115, 85, 84, 5, 0, 1, 90, 110, 133, 95, 236,
189, 109, 143, 227, 56, 146, 32, 252, 253, 126, 133, 55, 11,
133, 234, 154, 150, 220, 146, 108, 217, 78, 39, 186, 177,
187, 131, 93, 220, 2, 221, 243, 97, 231, 14, 56, 160, 175,
[ ... ommited ...]
})
}
Basically our static content is transfered to a big byte slice and can be called and handled by the parcello package later on. We will now redesign our template and open it via parcello instead of referencing it as a constant.
New folder structure
[...omitted...]
├── internal
│ ├── myca
│ │ └── ca.go
│ ├── myhttp
│ │ └── fileserver.go
│ ├── mylog
│ │ └── log.go
│ └── myutils
│ └── utils.go
[...omitted...]
└── static
├── css
│ ├── bootstrap.min.css
│ ├── bootstrap.min.css.map
│ ├── style.css
│ └── style.css.min.css
├── fonts
│ ├── FiraCode-VF.woff
│ └── FiraCode-VF.woff2
├── images
│ ├── favicon.gif
│ └── goshs-logo.png
├── js
│ ├── jquery-3.5.1.min.js
│ ├── main.js
│ └── main.js.min.js
├── package.go
├── resource.go
├── templates
│ ├── 404.html
│ ├── 500.html
│ └── index.html
└── vendor
├── datatable
├── fontawesome-5.15.1
└── images
So these are the two folders which are most important to goshs
. And as you can see I transfered the templates to the static folder and removed the myhtml
package completely.
I will not go into detail which third-party javascript libraries and stylesheets I am using as this is not the main topic of this blog post. But I will describe how I integrated them with goshs
to work smoothly.
Error handling templates
As the templates for error handling do not have dynamic content I just copied over the html markup to the corresponding files 404.html
and 500.html
. It is still on my roadmap to design an even better error page with dynamic content displaying the actual error to the user.
Utilities package
I implemented a utility package which has one function by now:
package myutils
import "fmt"
// ByteCountDecimal generates human readable file sizes and returns a string
func ByteCountDecimal(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}
To be honest, I found this one when searching for a way to express a human readable file size. I am quite new to go myself and do still not understand what exactly is going on here. So if you want to be a really smarta** write in the comments down below what this function exactly does. For all I know it converts the byte size (returned by os.Stat().Size()) to a humand readable string. And this is the string I will use later on to display the size of a file to the user.
fileserver.go
Well, this file was undergoing a big change for my new design to work. The point is, when embedding the static files like javascript and css you need to serve this to your html page for them to work. As goshs
serves the directory content to the user I cannot just use the root path to reference the css, right? What would happen if my html markup would do something like this?:
<html>
<head>
<style rel="stylesheet" href="/css/style.css">
</head>
<body>
...
</body>
</html>
The webserver would look into the folder we are in and try to find the css folder in there. So as long as the user does not have a css folder with a style.css in it there would be an error, right?
So I came up with a little trick to solve this issue.
The special folder
I decided to use a “special” folder to serve my static content from. So I chose a not so random sha256 hashsum to be the folders name.
echo "NameOfFirstChildAndNameOfSecondChild" | sha256sum
This is how I constructed a hashsum of the value 425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c
and this will be our special folder or more likely our special path as you will see now:
// 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)
}
}()
// Check if special path, then serve static
// Realise that you will not be able to deliver content of folder
// called 425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c on disk
if strings.Contains(req.URL.Path, "425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c") {
fs.static(w, req)
} else {
// serve files / dirs or upload
switch req.Method {
case "GET":
fs.handler(w, req)
case "POST":
fs.upload(w, req)
}
}
}
So in addition to distinguish between a GET and a POST request for handling the upload function as described in “Part#3 - I can haz featurez?” we now have to check if the request path will contain our “special” path and then handle the request with an additional handler serving our embedded static content.
For this purpose I coded the handler fs.static(w, req)
like so:
// static will give static content for style and function
func (fs *FileServer) static(w http.ResponseWriter, req *http.Request) {
// Check which file to serve
upath := req.URL.Path
staticPath := strings.SplitAfterN(upath, "/", 3)[2]
// Load file with parcello
staticFile, err := parcello.Open(staticPath)
if err != nil {
log.Printf("ERROR: static file: %+v cannot be loaded: %+v", staticPath, err)
}
// Read file
staticContent, err := ioutil.ReadAll(staticFile)
if err != nil {
log.Printf("ERROR: static file: %+v cannot be read: %+v", staticPath, err)
}
// Get mimetype from extension
extSlice := strings.Split(staticPath, ".")
ext := "." + extSlice[len(extSlice)-1]
contentType := mime.TypeByExtension(ext)
// Set mimetype and deliver to browser
w.Header().Add("Content-Type", contentType)
w.Write(staticContent)
}
First of all I split the path and extract everything after the special string. This is to determine which file in which folder to serve within the static directory. So for example if the user request is /425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/css/style.css staticPath
will be just css/style.css. And this is what we need next to open the File using parcello.Open()
. This function of the package parcello will open the file like os
would do with the difference that it will open it from the resources.go
we generate with go generate
.
Next up I read in the file content resulting in []bytes. Then I determine the content type by looking at the extension. I am using mime.TypeByExtension()
which will return the mimetype as string.
Finally I set the “Content-Type” Header in our response to the browser and then write the file content to the response.
This way I can serve every possible static file to the browser. So basically everytime the browser calles something in our “special” path it will just be served from our static folder. Cool, right?
But there are obvious trade-offs, you say? Well, that is perfectly correct. If a user has a folder with the name of our special path on disk, then goshs
will not be able to serve its content, as it will confuse the folder with our special path. To prevent that from happening I handled this situation before it can occur. I decided to not add a folder with the name of our special path to the dir listing displayed to the user like so:
func (fs *FileServer) processDir(w http.ResponseWriter, req *http.Request, file *os.File, relpath string) {
[... ommited ...]
// Add / to name if dir
if fi.IsDir() {
// Check if special path exists as dir on disk and do not add
if fi.Name() == "425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c" {
continue
}
itemname += "/"
item.IsDir = true
}
// define item struct
item.Name = itemname
[... ommited ...]
So when iterating over the content of a folder in our processDir()
function I will just not add a folder called like our special path to the items slice we are generating. So even if there is a folder with this name it will just not be displayed and therefore nothing can be confused. This was the best solution I could think of.
Attributes, we need attributes!

As you can see from the screenshot of the final design we will need a few new fields in our item struct and in general. Those are: Size, LastModified, goshsVersion. Also what cannot be seen from the screenshot is that we also need to split our path we got into relpath and abspath for the template to work.
So from this (v0.0.3):
type directory struct {
Path string
Content []item
}
type item struct {
URI string
Name string
}
we need to change to this (v0.0.4):
const goshsVersion string = "0.0.4"
type goshs struct {
Version string
}
type directory struct {
RelPath string
AbsPath string
IsSubdirectory bool
Back string
Content []item
Goshs goshs
}
type item struct {
URI string
Name string
IsDir bool
DisplaySize string
SortSize int64
DisplayLastModified string
SortLastModified time.Time
}
And to fill in the values to this new attributes we will now look at the all new processDir
function, shall we?
func (fs *FileServer) processDir(w http.ResponseWriter, req *http.Request, file *os.File, relpath string) {
// Read directory FileInfo
[...unchanged...]
** Section 1: Fill slice **
// Create empty slice
items := make([]item, 0, len(fis))
// Iterate over FileInfo of dir
for _, fi := range fis {
var item = item{}
// Set name and uri
itemname := fi.Name()
itemuri := url.PathEscape(path.Join(relpath, itemname))
itemsize := myutils.ByteCountDecimal(fi.Size())
itemsortsize := fi.Size()
itemmod := fi.ModTime().Format("Mon Jan _2 15:04:05 2006")
itemsortmod := fi.ModTime()
// Add / to name if dir
if fi.IsDir() {
// Check if special path exists as dir on disk and do not add
if fi.Name() == "425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c" {
continue
}
itemname += "/"
item.IsDir = true
}
// define item struct
item.Name = itemname
item.URI = itemuri
item.DisplaySize = itemsize
item.SortSize = itemsortsize
item.DisplayLastModified = itemmod
item.SortLastModified = itemsortmod
// Add to items slice
items = append(items, item)
}
// Sort slice all lowercase
[... unchanged ...]
** Section 2: Template parsing **
// Template parsing and writing to browser
indexFile, err := parcello.Open("templates/index.html")
if err != nil {
log.Printf("Error opening embedded file: %+v", err)
}
fileContent, err := ioutil.ReadAll(indexFile)
if err != nil {
log.Printf("Error opening embedded file: %+v", err)
}
// Construct directory for template
d := &directory{
RelPath: relpath,
AbsPath: path.Join(fs.Webroot, relpath),
Content: items,
Goshs: goshs{
Version: goshsVersion,
},
}
if relpath != "/" {
d.IsSubdirectory = true
pathSlice := strings.Split(relpath, "/")
if len(pathSlice) > 2 {
pathSlice = pathSlice[1 : len(pathSlice)-1]
var backString string = ""
for _, part := range pathSlice {
backString += "/" + part
}
d.Back = backString
} else {
d.Back = "/"
}
} else {
d.IsSubdirectory = false
}
t := template.New("index")
t.Parse(string(fileContent))
if err := t.Execute(w, d); err != nil {
log.Printf("ERROR: Error parsing template: %+v", err)
}
}
Okay, I know. This is a lot to take in. So I left markers in the code to break that down in sections.
Section 1: Fill slice
In this part of the new processDir
function I am filling the items slice with the content of the directory. I do this like i did before but now I am filling in more information.
First I create an empty slice as before and then I iterate over the content of the folder. For each item I will define its Name, URI, Size (using our utils function to represent a human readable size), sort size (which is just the byte size to be able to sort), last modification time (as formatted string) and last modification time as timestamp (to be able to sort correct, too). This is this part:
// Create empty slice
items := make([]item, 0, len(fis))
// Iterate over FileInfo of dir
for _, fi := range fis {
var item = item{}
// Set name and uri
itemname := fi.Name()
itemuri := url.PathEscape(path.Join(relpath, itemname))
itemsize := myutils.ByteCountDecimal(fi.Size())
itemsortsize := fi.Size()
itemmod := fi.ModTime().Format("Mon Jan _2 15:04:05 2006")
itemsortmod := fi.ModTime()
Now comes the part with the special folder. I will skip this. But I have to mention that I added a item.IsDir = true
to the item, if it is a directory. This is important for the template later on.
Then I define the item with all the data and add it to the slice like this:
// define item struct
item.Name = itemname
item.URI = itemuri
item.DisplaySize = itemsize
item.SortSize = itemsortsize
item.DisplayLastModified = itemmod
item.SortLastModified = itemsortmod
// Add to items slice
items = append(items, item)
So now the item is in our slice with all the juicy new information, nice!
Section 2: Template parsing
In this part of the new processDir
function we now need to parse the new template and provide all the new information.
So first of all the new template is opened with parcello
as we did it in the static handler before like so:
// Template parsing and writing to browser
indexFile, err := parcello.Open("templates/index.html")
if err != nil {
log.Printf("Error opening embedded file: %+v", err)
}
fileContent, err := ioutil.ReadAll(indexFile)
if err != nil {
log.Printf("Error opening embedded file: %+v", err)
}
Now instead of feeding a constant string to the template engine we feed an actual embedded html file to it. Isn’t this convenient?
Now lets construct the directory struct we feed to our template:
// Construct directory for template
d := &directory{
RelPath: relpath,
AbsPath: path.Join(fs.Webroot, relpath),
Content: items,
Goshs: goshs{
Version: goshsVersion,
},
}
First we hand in the information we already have. Those are the relative path, the absolute path which is were we are on the disk, our items slice and finally the static goshs
version. So far so good.
To be able to jump back one layer without hitting the back button of the browser we implement a value called Back
to be able to link to it like so:
if relpath != "/" {
d.IsSubdirectory = true
pathSlice := strings.Split(relpath, "/")
if len(pathSlice) > 2 {
pathSlice = pathSlice[1 : len(pathSlice)-1]
var backString string = ""
for _, part := range pathSlice {
backString += "/" + part
}
d.Back = backString
} else {
d.Back = "/"
}
} else {
d.IsSubdirectory = false
}
If we are not in the exact root of our file server we are in a subDirectory
therefore set it to true. Then we do a bit of string splitting and transformation to construct the relative path we were in before.
This means that if we are in /tmp/layer1/layer2/layer3/layer4
our backString now holds /tmp/layer1/layer2/layer3
. We assign this to d.Back
to be able to hand it to our template.
If we are at root level (/) we set subDirectory
to false and there will not be a value for Back
.
Finally we execute our template like so:
t := template.New("index")
t.Parse(string(fileContent))
if err := t.Execute(w, d); err != nil {
log.Printf("ERROR: Error parsing template: %+v", err)
}
Instead of the constant we now parse the content of embedded the index file and execute the template.
This applies to the handle404
and handle500
function, as well. For example:
[...ommited...]
file, err := parcello.Open("templates/404.html")
[...ommited...]
t := template.New("404")
t.Parse(string(fileContent))
t.Execute(w, nil)
Index template
Well, speaking of which, we will now look at the important parts of the template. I will break it down and explain what is happening.
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>goshs - {{.AbsPath}}</title>
<!-- stylesheets -->
<link rel="icon" type="image/gif" href="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/images/favicon.gif" />
<link rel="stylesheet" href="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/css/bootstrap.min.css" />
<link rel="stylesheet" href="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/css/style.css.min.css" />
<link rel="stylesheet" href="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/vendor/fontawesome-5.15.1/css/all.min.css" />
<link rel="stylesheet" href="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/vendor/datatable/jquery.dataTables.min.css" />
</head>
In the head section you can see how I reference all the styles we need to apply to make goshs
look like it does from our special path. Again, all credits go to sc0tfree and updog as I took his template as basis for this.
<body>
<div class="conn">
<!-- Header -->
<header id="header" class="d-flex align_item_center">
<div onclick="document.location='/'" class="logo_p">
<img src="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/images/goshs-logo.png" alt="goshs" />
</div>
<div class="heading_title_p">
<h2>Directory: {{.AbsPath}}</h2>
</div>
</header>
In the body there is a header section to display the logo and the directory we are in. Here (and in the title) I use the absolute path instead of the relative path.
<!-- ----- Upload Form ----- -->
<div class="inputUploadP">
{{ if (eq .RelPath "/") }}
<form method="post" action="/upload" enctype="multipart/form-data" class="uploadForm">
{{ else }}
<form method="post" action="{{.RelPath}}/upload" enctype="multipart/form-data" class="uploadForm">
{{ end }}
<!-- -- Upload File -- -->
<div class="uploadFile_P">
<input type="file" name="files" id="files" class="uploadFile" data-multiple-caption="{count} files selected" multiple/>
<label for="files">
<i class="fa fa-upload"></i>
<span>Choose file(s) ...</span>
</label>
</div>
<!-- -- Upload Btn -- -->
<p class="uploadBtn_P">
<button type="submit" class="uploadBtn btn btn-primary">
Upload
</button>
</p>
</form>
</div>
Then there is the upload form consisting of a upload file dialog and a submit button. If you look closely you can already guess what our bonus section will be. But I will not point it out right now 😎.
I still do distinguis between the root path and any sub path to make the upload form work. Here we could have used {{.isSubdirectory}}
, too.
{{if .IsSubdirectory}}
<section class="backBtn_p">
<a href="{{ .Back }}">
<i class="fas fa-level-up-alt"></i>
<span>Back</span>
</a>
</section>
{{end}}
Next up if we are in a subdirectory I will display a back button to bring us back to the path which will be in {{ .Back }}
. See how it all connects?
<!-- Table -->
<section class="table_p table-responsive">
<table id="tableData" class="table table-hover compact">
<thead>
<tr>
<th width="4%"><!--Type (Directory or File)--></th>
<th>Name</th>
<th>Size</th>
<th>Last Modified</th>
</tr>
</thead>
<tbody>
{{range .Content}}
<tr>
<td> <!-- Icon -->
{{ if .IsDir }}
<button class="file_ic"><i class="far fa-folder"></i></button><!-- Directory icon -->
{{ else }}
<button class="file_ic"><i class="far fa-file"></i></button><!-- File icon -->
{{ end }}
</td>
<td> <!-- Name -->
<a href="/{{.URI}}">{{.Name}}</a>
</td>
<td data-order="{{.SortSize}}"> <!-- File size -->
{{ if .IsDir }}
--
{{ else }}
{{.DisplaySize}}
{{ end }}
</td>
<td data-order="{{.SortLastModified}}"> <!-- File last modified -->
{{ .DisplayLastModified }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</section>
In this code part I will construct the table holding the information. I will again do a {{range .Content}}
to loop over our items slice.
If {{ .IsDir }}
is true the icon will be a folder icon. Otherwise it will be a file icon.
Then there is the name with the link to the URI. And then there is the column for size and last modified.
I use a third-party javascript library to be able to sort the table later on. So for the data-order attribute I will fill in the {{.SortX}}
value, whereas for displaying information I will use the {{.DisplayX}}
value. This way I display human readable data but am able to sort correctly.
<footer>
<p>
goshs v{{ .Goshs.Version }}
</p>
</footer>
</div> <!-- end class conn-->
<!-- Scripts -->
<script src="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/js/jquery-3.5.1.min.js"></script>
<script src="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/vendor/datatable/jquery.dataTables.min.js"></script>
<script src="/425bda8487e36deccb30dd24be590b8744e3a28a8bb5a57d9b3fcd24ae09ad3c/js/main.js.min.js"></script>
</body>
Finally there is a small footer showing the goshs
version and the scripts I need to include for everything to work.
Third-party libraries I use
- Bootstrap
- jQuery
- Datatables
- Font Awesome
- Fira Code as font
Conclusion
Compiling all this will give us a beautiful goshs
design with a lot of functionality, right?

Datatables will also add a search field and the total count of the items automatically.
As always you can view the complete code for this part at the github repository as tag v0.0.4 and I am happy to get some feedback, github stars, issues (if any), feature requests and such.
Bonus: Multiple files upload
So as promised here is the bonus. Some of you may have already seen the multiple
flag within our file upload form. So we need to implement that in our upload handler:
// upload handles the POST request to upload files
func (fs *FileServer) upload(w http.ResponseWriter, req *http.Request) {
[... ommited url and target path ...]
// Parse request
if err := req.ParseMultipartForm(10 << 20); err != nil {
log.Printf("Error parsing multipart request: %+v", err)
return
}
// Get ref to the parsed multipart form
m := req.MultipartForm
// Get the File Headers
files := m.File["files"]
for i := range files {
file, err := files[i].Open()
defer file.Close()
if err != nil {
log.Printf("Error retrieving the file: %+v\n", err)
}
// Construct absolute savepath
savepath := fmt.Sprintf("%s%s/%s", fs.Webroot, target, files[i].Filename)
log.Printf("DEBUG: savepath is supposed to be: %+v", savepath)
// Create file to write to
if _, err := os.Create(savepath); err != nil {
log.Println("ERROR: Not able to create file on disk")
fs.handle500(w, req)
}
// Read file from post body
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
log.Println("ERROR: Not able to read file from request")
fs.handle500(w, req)
}
// Write file to disk
if err := ioutil.WriteFile(savepath, fileBytes, os.ModePerm); err != nil {
log.Println("ERROR: Not able to write file to disk")
fs.handle500(w, req)
}
}
[... ommited logging and redirecting ...]
}
So to make this work we need to wrap our file save procedure into a range loop which will loop over all the provided files. So instead of just parsing one file we need to parse the fultipart Form a little differently.
This req.ParseMultipartForm(10 << 20)
and this m := req.MultipartForm
in combination with this files := m.File["files"]
will give us a handler to all files provided by the form.
So we then are able to loop over files
where i
will be the representation of every single file provided.
Finally we just treat the single file as before by constructing a savepath, creating its existance and reading the actual content and then finally writing it to the disk.
This does not differ from the previous procedure.
Thanks for taking the time to read. Comment down below. Have a nice day.
Patrick