I'm having a problem with my Go project where one route handles CSS fine and another route's CSS is broken. The CSS used to work on both pages, but now it isn't loading for /login.html.
I know that I'm properly stripping the prefix for the /static/ folder because it's working in once place and not another. I also directly copied and pasted the header code from the working page to the not-working page (being careful to use the correct css file).
Negroni is showing that the application is making the call to the correct location:
999.3µs | localhost:8080 | GET /static/css/splash.css
The correctly working html file index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Pando</title>
<link rel="stylesheet" href="/static/css/index.css" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Bitter|Nunito:400,700" rel="stylesheet">
</head>
<body>
<div id="sidebar">
<p id="logo"><img src="/static/img/logo.svg" height="14px">Pando</p>
<span id="all-files" class="selected">All Files</span>
<p id="shared-collections">Shared Collections<img src="/static/img/gear.svg" /></p>
<div class="collections">
<span>Collection 1</span>
<span>Collection 2</span>
</div>
<p id="my-collections">My Collections<img src="/static/img/gear.svg" /></p>
<div class="collections">
<span>Collection 1</span>
<span>Collection 2</span>
</div>
</div>
<div id="header">
<input type="button" id="upload-launch-button" value="Upload" onclick="showUploadDialog()"></button>
<form id="search">
<input type="search" placeholder="Search..">
<input type="button"><img src="/static/img/search.svg"></button>
</form>
<div id="user">
<img src="/static/img/user.svg">{{.User}}<a href="/logout">(Log out)</a>
</div>
</div>
<!-- <span id="filter">Latest Files</span> -->
<div id="results">
{{range .Files}}
<div class="img-container">
<img src="/files/{{.Name}}" id="file-{{.PK}}">
<div class="hover-actions">
<a href="/files/{{.Name}}" download><img src="/static/img/download.svg"></a>
<img src="/static/img/edit.svg">
<img src="/static/img/delete.svg" onclick="deleteFile('/files/{{.Name}}', {{.PK}})">
</div>
</div>
{{end}}
</div>
<div class="dialog" id="upload-dialog">
<div class="dialog-name">Upload</div>
<form id="upload" enctype="multipart/form-data" action="/upload" method="post">
<input type="file" id="selectedFile" name="file" /> <!--multiple later display none-->
<input id="upload-button" type="submit" value="Upload" onclick="hideUploadDialog()" />
</form>
</div>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script type="text/javascript" src="/static/js/script.js"></script>
</body>
</html>
Login.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Pando</title>
<link rel="stylesheet" href="/static/css/splash.css" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Bitter|Nunito:400,700" rel="stylesheet">
</head>
<body>
<section class="section-a">
<div id="logo"><img src="/static/img/logo.svg">Pando</div>
<p id="welcome">Join Pando.</p>
<div id="buttoncont">
<a href="/static/html/index.html"><span id="enter" class="button">Enter</span></a>
</div>
</section>
<section class="section-b">
<form id="login-form">
<div>
<label>Email</label><input type="email" name="username" required>
</div>
<div>
<label>Password</label><input type="password" name="password" required>
</div>
<div>
<input type="submit" value="Register" name="register">
<input type="submit" value="Log In" name="login">
</div>
<div id="error">{{.Error}}</div>
</form>
</section>
</body>
</html>
The complete go file:
package main
import (
"database/sql"
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"time"
"net/http"
"gopkg.in/gorp.v1"
_ "github.com/go-sql-driver/mysql"
"encoding/json"
"golang.org/x/crypto/bcrypt"
sessions "github.com/goincremental/negroni-sessions"
"github.com/goincremental/negroni-sessions/cookiestore"
gmux "github.com/gorilla/mux"
"github.com/urfave/negroni"
)
/*File struct
PK primary key
Name is the original name of the file; new file location at /files/{name}
File Type is the extension of the file; valid file types are image formats, PDF, .AI, .PSD, and MS Word docs
Upload Date is a static value indicating when the file was uploaded
Last Modified records if any changes are made to the file while it's on the server
User is the uploading user
Eventually I will probably want to refactor this so that I can allow for files with the same name to coexist. Not sure how to do that right now elegantly.
*/
type File struct {
PK int64 `db:"pk"`
Name string `db:"name"`
FileType string `db:"type"`
UploadDate string `db:"uploadtime"`
LastModified string `db:"modtime"`
User string `db:"user"`
}
// Tag struct
type Tag struct {
PK int64 `db:"pk"`
FilePK int64 `db:"filepk"`
Name string `db:"name"`
}
// Collection struct
type Collection struct {
PK int64 `db:"pk"`
Name string `db:"name"`
ContentName string `db:"contentname"`
ContentType string `db:"type"`
}
// User struct
type User struct {
Username string `db:"username"`
Secret []byte `db:"secret"`
}
// Page struct
type Page struct {
Files []File
Filter string
User string
}
// LoginPage struct
type LoginPage struct {
Error string
}
// UploadPage struct
type UploadPage struct {
Error string
}
var db *sql.DB
var dbmap *gorp.DbMap
func main() {
initDb()
index := template.Must(template.ParseFiles("html/index.html"))
login := template.Must(template.ParseFiles("html/login.html"))
upload := template.Must(template.ParseFiles("html/upload.html"))
mux := gmux.NewRouter()
defer db.Close()
mux.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
mux.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir("css"))))
mux.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir("img"))))
mux.PathPrefix("/files/").Handler(http.StripPrefix("/files/", http.FileServer(http.Dir("files"))))
// Login
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
var p LoginPage
if r.FormValue("register") != "" {
secret, _ := bcrypt.GenerateFromPassword([]byte(r.FormValue("password")), bcrypt.DefaultCost)
user := User{r.FormValue("username"), secret}
if err := dbmap.Insert(&user); err != nil {
p.Error = err.Error()
} else {
sessions.GetSession(r).Set("User", user.Username)
http.Redirect(w, r, "/", http.StatusFound)
return
}
} else if r.FormValue("login") != "" {
user, err := dbmap.Get(User{}, r.FormValue("username"))
if err != nil {
p.Error = err.Error()
} else if user == nil {
p.Error = "No user account exists for the username " + r.FormValue("username")
} else {
u := user.(*User)
if err = bcrypt.CompareHashAndPassword(u.Secret, []byte(r.FormValue("password"))); err != nil {
p.Error = err.Error()
} else {
sessions.GetSession(r).Set("User", u.Username)
http.Redirect(w, r, "/", http.StatusFound)
return
}
}
}
if err := login.ExecuteTemplate(w, "login.html", p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
// Upload
mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
var p UploadPage
// Checks filesize against max upload size (10MB)
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// reads file
fileType := r.PostFormValue("type")
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// checks the filetype against expected mime types
mimetype := http.DetectContentType(fileBytes)
if mimetype != "image/jpeg" && mimetype != "image/jpg" &&
mimetype != "image/gif" && mimetype != "image/png" &&
mimetype != "application/pdf" && mimetype != "image/vnd.adobe.photoshop" && mimetype != "application/illustrator" && mimetype != "image/vnd.microsoft.icon" &&
mimetype != "application/msword" && mimetype != "application/x-photoshop" && mimetype != "application/photoshop" && mimetype != "application/psd" {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
filename := header.Filename
newPath := filepath.Join("files/", filename)
fmt.Printf("FileType: %s, File: %s\n", fileType, newPath)
t := time.Now().String()
currentTime, _ := time.Parse(time.Stamp, t)
// Creates a File struct-type object out of the file information from
f := File{
PK: -1,
Name: filename,
FileType: fileType,
UploadDate: currentTime.String(),
LastModified: currentTime.String(),
User: getStringFromSession(r, "User"),
}
// Inserts the file information into the database
if err = dbmap.Insert(&f); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
newFile, err := os.Create(newPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer newFile.Close()
if _, err := newFile.Write(fileBytes); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// w.Write([]byte("SUCCESS"))
if err := upload.ExecuteTemplate(w, "upload.html", p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}).Methods("POST")
// Sort
mux.HandleFunc("/files", func(w http.ResponseWriter, r *http.Request) {
var b []File
if !getFileCollection(&b, r.FormValue("sortBy"), getStringFromSession(r, "Filter"), getStringFromSession(r, "User"), w) {
return
}
sessions.GetSession(r).Set("sortBy", r.FormValue("sortBy"))
if err := json.NewEncoder(w).Encode(b); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}).Methods("GET").Queries("sortBy", "{sortBy:title|author|classification}")
// Default page
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
p := Page{Files: []File{}, Filter: getStringFromSession(r, "Filter"), User: getStringFromSession(r, "User")}
if !getFileCollection(&p.Files, getStringFromSession(r, "SortBy"), getStringFromSession(r, "Filter"), p.User, w) {
return
}
if err := index.ExecuteTemplate(w, "index.html", p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}).Methods("GET")
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
sessions.GetSession(r).Set("User", nil)
sessions.GetSession(r).Set("Filter", nil)
http.Redirect(w, r, "/login", http.StatusFound)
})
// Deletes file from database; currently not working :(
mux.HandleFunc("/files/{name}", func(w http.ResponseWriter, r *http.Request) {
pk, _ := strconv.ParseInt(gmux.Vars(r)["pk"], 10, 64)
fmt.Printf("pk is %d", pk)
var f File
if err := dbmap.SelectOne(&f, "select * from files where pk=?", pk); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
if _, err := dbmap.Delete(&f); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}).Methods("DELETE")
// Session management
n := negroni.Classic()
n.Use(sessions.Sessions("pando", cookiestore.New([]byte("hubert88"))))
n.Use(negroni.HandlerFunc(verifyDatabase))
n.Use(negroni.HandlerFunc(verifyUser))
n.UseHandler(mux)
n.Run(":8080")
} // end main
// Opens the database connection to SQL and creates tables if they don't exist
func initDb() {
db, _ = sql.Open("mysql", "root:secret@tcp(127.0.0.1:3306)/pando")
dbmap = &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}
// creates tables, specifies the fields on the struct that map to table primary keys
dbmap.AddTableWithName(File{}, "files").SetKeys(true, "pk")
dbmap.AddTableWithName(Tag{}, "tags").SetKeys(true, "pk")
dbmap.AddTableWithName(Collection{}, "collections").SetKeys(true, "pk")
dbmap.AddTableWithName(User{}, "users").SetKeys(false, "username")
dbmap.CreateTablesIfNotExists()
}
// Checks to make sure the database is alive
func verifyDatabase(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if err := db.Ping(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
next(w, r)
}
func getStringFromSession(r *http.Request, key string) string {
var strVal string
if val := sessions.GetSession(r).Get(key); val != nil {
strVal = val.(string)
}
return strVal
}
func verifyUser(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if r.URL.Path == "/login" {
next(w, r)
return
}
if username := getStringFromSession(r, "User"); username != "" {
if user, _ := dbmap.Get(User{}, username); user != nil {
next(w, r)
return
}
}
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
}
I'm at my wits end trying to debug this, and all the search results I get only talk about stripping prefixes (which I'm already doing).
In the Networking tab of my browser, the CSS and image files are returning a 307 Temporary Redirect error.
Running colminator's curl command gave this output:
HTTP/1.1 307 Temporary Redirect
Content-Type: text/html; charset=utf-8
Location: /login
Date: Sun, 05 May 2019 22:16:17 GMT
Content-Length: 42
This is how I'm handling my static files.
mux.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
As the comments suggest another handler is intercepting your static file route. Try simplifying your routes. Reduce:
mux.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
mux.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir("css"))))
mux.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir("img"))))
mux.PathPrefix("/files/").Handler(http.StripPrefix("/files/", http.FileServer(http.Dir("files"))))
to a single route:
mux.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
and move all your static files under the static
directory - updating any html/css paths accordingly.