2017-04-03 21:32:13 +00:00
|
|
|
package main
|
|
|
|
|
2017-07-06 17:09:49 +00:00
|
|
|
//go:generate esc -o assets.go assets templates
|
2017-06-22 15:34:57 +00:00
|
|
|
|
2017-04-03 21:32:13 +00:00
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
2018-01-23 20:55:52 +00:00
|
|
|
"os/signal"
|
2017-04-03 21:32:13 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2018-01-23 20:55:52 +00:00
|
|
|
"syscall"
|
|
|
|
"time"
|
2017-04-03 21:32:13 +00:00
|
|
|
|
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
|
|
|
|
|
|
"github.com/gorilla/handlers"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/gorilla/sessions"
|
|
|
|
"github.com/justinas/alice"
|
|
|
|
)
|
|
|
|
|
2017-11-01 03:56:17 +00:00
|
|
|
// AppName Application name
|
2017-04-03 21:32:13 +00:00
|
|
|
const AppName = "gjvote"
|
2017-11-01 03:56:17 +00:00
|
|
|
|
|
|
|
// DbName Database name, which is <AppName>.db
|
2017-06-22 15:34:57 +00:00
|
|
|
const DbName = AppName + ".db"
|
2017-04-03 21:32:13 +00:00
|
|
|
|
|
|
|
// pageData is stuff that changes per request
|
|
|
|
type pageData struct {
|
|
|
|
Site *siteData
|
|
|
|
Title string
|
|
|
|
HideTitleImage bool
|
|
|
|
SubTitle string
|
|
|
|
Stylesheets []string
|
|
|
|
HeaderScripts []string
|
|
|
|
Scripts []string
|
|
|
|
FlashMessage string
|
|
|
|
FlashClass string
|
|
|
|
LoggedIn bool
|
|
|
|
Menu []menuItem
|
|
|
|
BottomMenu []menuItem
|
2017-06-08 17:20:43 +00:00
|
|
|
HideAdminMenu bool
|
2017-04-03 21:32:13 +00:00
|
|
|
session *pageSession
|
2017-04-21 18:17:18 +00:00
|
|
|
CurrentJam string
|
2017-06-30 19:35:36 +00:00
|
|
|
ClientId string
|
2017-06-08 17:20:43 +00:00
|
|
|
ClientIsAuth bool
|
2017-06-15 17:35:53 +00:00
|
|
|
ClientIsServer bool
|
2017-07-19 15:16:18 +00:00
|
|
|
AuthMode int
|
2017-04-03 21:32:13 +00:00
|
|
|
|
2017-06-22 15:34:57 +00:00
|
|
|
PublicMode int
|
2017-04-03 21:32:13 +00:00
|
|
|
TemplateData interface{}
|
|
|
|
}
|
|
|
|
|
|
|
|
type menuItem struct {
|
|
|
|
Label string
|
|
|
|
Location string
|
|
|
|
Icon string
|
|
|
|
}
|
|
|
|
|
2018-01-26 17:47:13 +00:00
|
|
|
var sessionStore *sessions.CookieStore
|
2017-04-03 21:32:13 +00:00
|
|
|
var r *mux.Router
|
2017-10-11 23:03:27 +00:00
|
|
|
var m *model
|
2017-04-03 21:32:13 +00:00
|
|
|
|
|
|
|
func main() {
|
2017-10-11 23:03:27 +00:00
|
|
|
var err error
|
|
|
|
if m, err = NewModel(); err != nil {
|
|
|
|
errorExit("Unable to initialize Model: " + err.Error())
|
|
|
|
}
|
|
|
|
|
2017-04-03 21:32:13 +00:00
|
|
|
loadConfig()
|
2017-10-21 22:47:01 +00:00
|
|
|
if err = m.site.SaveToDB(); err != nil {
|
2017-10-19 19:53:07 +00:00
|
|
|
errorExit("Unable to save site config to DB: " + err.Error())
|
|
|
|
}
|
2018-01-23 20:55:52 +00:00
|
|
|
|
|
|
|
// Save changes to the DB every 5 minutes
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
m.saveChanges()
|
|
|
|
time.Sleep(5 * time.Minute)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2017-04-03 21:32:13 +00:00
|
|
|
initialize()
|
|
|
|
|
2018-01-26 17:47:13 +00:00
|
|
|
// We should have a session secret by now, initialize the store
|
|
|
|
sessionStore = sessions.NewCookieStore([]byte(m.site.sessionSecret))
|
|
|
|
|
2017-04-03 21:32:13 +00:00
|
|
|
r = mux.NewRouter()
|
|
|
|
r.StrictSlash(true)
|
|
|
|
|
2017-10-11 23:03:27 +00:00
|
|
|
if m.site.DevMode {
|
2017-06-22 15:34:57 +00:00
|
|
|
fmt.Println("Operating in Development Mode")
|
|
|
|
}
|
2017-10-11 23:03:27 +00:00
|
|
|
r.PathPrefix("/assets/").Handler(http.FileServer(FS(m.site.DevMode)))
|
2017-04-03 21:32:13 +00:00
|
|
|
|
|
|
|
// Public Subrouter
|
|
|
|
pub := r.PathPrefix("/").Subrouter()
|
|
|
|
pub.HandleFunc("/", handleMain)
|
2017-06-30 19:35:36 +00:00
|
|
|
pub.HandleFunc("/vote", handlePublicSaveVote)
|
2017-07-14 12:28:09 +00:00
|
|
|
pub.HandleFunc("/image/{teamid}/{imageid}", handleImageRequest)
|
|
|
|
pub.HandleFunc("/thumbnail/{teamid}/{imageid}", handleThumbnailRequest)
|
2017-07-14 17:27:42 +00:00
|
|
|
pub.HandleFunc("/team/{id}", handleTeamMgmtRequest)
|
|
|
|
pub.HandleFunc("/team/{id}/{function}", handleTeamMgmtRequest)
|
|
|
|
pub.HandleFunc("/team/{id}/{function}/{subid}", handleTeamMgmtRequest)
|
2017-04-03 21:32:13 +00:00
|
|
|
|
|
|
|
// API Subrouter
|
|
|
|
//api := r.PathPrefix("/api").Subtrouter()
|
|
|
|
|
|
|
|
// Admin Subrouter
|
|
|
|
admin := r.PathPrefix("/admin").Subrouter()
|
|
|
|
admin.HandleFunc("/", handleAdmin)
|
|
|
|
admin.HandleFunc("/dologin", handleAdminDoLogin)
|
|
|
|
admin.HandleFunc("/dologout", handleAdminDoLogout)
|
|
|
|
admin.HandleFunc("/{category}", handleAdmin)
|
|
|
|
admin.HandleFunc("/{category}/{id}", handleAdmin)
|
|
|
|
admin.HandleFunc("/{category}/{id}/{function}", handleAdmin)
|
2017-06-21 22:09:26 +00:00
|
|
|
admin.HandleFunc("/{category}/{id}/{function}/{subid}", handleAdmin)
|
2017-04-03 21:32:13 +00:00
|
|
|
|
|
|
|
http.Handle("/", r)
|
|
|
|
|
|
|
|
chain := alice.New(loggingHandler).Then(r)
|
|
|
|
|
2018-01-23 20:55:52 +00:00
|
|
|
// Set up a channel to intercept Ctrl+C for graceful shutdowns
|
|
|
|
c := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
|
|
<-c
|
|
|
|
// Save the changes when the app quits
|
|
|
|
fmt.Println("\nFinishing up...")
|
|
|
|
m.saveChanges()
|
|
|
|
os.Exit(0)
|
|
|
|
}()
|
|
|
|
|
2017-10-21 22:47:01 +00:00
|
|
|
fmt.Printf("Listening on port %d\n", m.site.Port)
|
2018-06-18 13:49:15 +00:00
|
|
|
log.Fatal(http.ListenAndServe(m.site.Ip+":"+strconv.Itoa(m.site.Port), chain))
|
2017-04-03 21:32:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func loadConfig() {
|
2017-06-22 15:34:57 +00:00
|
|
|
if len(os.Args) > 1 {
|
|
|
|
for _, v := range os.Args {
|
|
|
|
key := v
|
|
|
|
val := ""
|
|
|
|
eqInd := strings.Index(v, "=")
|
|
|
|
if eqInd > 0 {
|
|
|
|
// It's a key/val argument
|
|
|
|
key = v[:eqInd]
|
|
|
|
val = v[eqInd+1:]
|
|
|
|
}
|
|
|
|
switch key {
|
|
|
|
case "-title":
|
2017-10-11 23:03:27 +00:00
|
|
|
m.site.Title = val
|
|
|
|
fmt.Print("Set site title: ", m.site.Title, "\n")
|
2018-06-18 13:49:15 +00:00
|
|
|
case "-ip":
|
|
|
|
m.site.Ip = val
|
|
|
|
fmt.Print("Set site IP: ", m.site.Ip, "\n")
|
2017-06-22 15:34:57 +00:00
|
|
|
case "-port":
|
|
|
|
var tryPort int
|
|
|
|
var err error
|
|
|
|
if tryPort, err = strconv.Atoi(val); err != nil {
|
|
|
|
fmt.Print("Invalid port given: ", val, " (Must be an integer)\n")
|
2017-10-11 23:03:27 +00:00
|
|
|
tryPort = m.site.Port
|
2017-06-22 15:34:57 +00:00
|
|
|
}
|
|
|
|
// TODO: Make sure a valid port number is given
|
2017-10-11 23:03:27 +00:00
|
|
|
m.site.Port = tryPort
|
2017-06-22 15:34:57 +00:00
|
|
|
case "-session-name":
|
2017-10-11 23:03:27 +00:00
|
|
|
m.site.SessionName = val
|
2017-06-22 15:34:57 +00:00
|
|
|
case "-server-dir":
|
|
|
|
// TODO: Probably check if the given directory is valid
|
2017-10-11 23:03:27 +00:00
|
|
|
m.site.ServerDir = val
|
2017-06-22 15:34:57 +00:00
|
|
|
case "-help", "-h", "-?":
|
|
|
|
printHelp()
|
|
|
|
done()
|
|
|
|
case "-dev":
|
2017-10-11 23:03:27 +00:00
|
|
|
m.site.DevMode = true
|
2017-06-22 15:34:57 +00:00
|
|
|
case "-reset-defaults":
|
|
|
|
resetToDefaults()
|
|
|
|
done()
|
|
|
|
}
|
|
|
|
}
|
2017-04-03 21:32:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func initialize() {
|
2017-10-11 23:03:27 +00:00
|
|
|
// Test if we have an admin user first
|
|
|
|
if !m.hasUser() {
|
|
|
|
// Nope, create one
|
2017-04-03 21:32:13 +00:00
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
fmt.Println("Create new Admin user")
|
|
|
|
fmt.Print("Email: ")
|
|
|
|
email, _ := reader.ReadString('\n')
|
|
|
|
email = strings.TrimSpace(email)
|
|
|
|
var pw1, pw2 []byte
|
|
|
|
for string(pw1) != string(pw2) || string(pw1) == "" {
|
|
|
|
fmt.Print("Password: ")
|
|
|
|
pw1, _ = terminal.ReadPassword(0)
|
|
|
|
fmt.Println("")
|
|
|
|
fmt.Print("Repeat Password: ")
|
|
|
|
pw2, _ = terminal.ReadPassword(0)
|
|
|
|
fmt.Println("")
|
|
|
|
if string(pw1) != string(pw2) {
|
|
|
|
fmt.Println("Entered Passwords don't match!")
|
|
|
|
}
|
|
|
|
}
|
2017-10-11 23:03:27 +00:00
|
|
|
assertError(m.updateUserPassword(email, string(pw1)))
|
2017-04-03 21:32:13 +00:00
|
|
|
}
|
2017-10-11 23:03:27 +00:00
|
|
|
|
|
|
|
// Now test if the 'current jam' is named
|
|
|
|
if m.jam.Name == "" {
|
2017-04-21 18:17:18 +00:00
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
fmt.Println("Create New Game Jam")
|
|
|
|
fmt.Print("GameJam Name: ")
|
|
|
|
gjName, _ := reader.ReadString('\n')
|
|
|
|
gjName = strings.TrimSpace(gjName)
|
2017-10-19 19:53:07 +00:00
|
|
|
m.jam.Name = gjName
|
2017-10-21 22:47:01 +00:00
|
|
|
assertError(m.jam.SaveToDB())
|
2017-04-21 18:17:18 +00:00
|
|
|
}
|
2017-06-08 17:20:43 +00:00
|
|
|
|
2017-10-11 23:03:27 +00:00
|
|
|
if m.jam.Name != "" {
|
2017-10-19 19:53:07 +00:00
|
|
|
fmt.Println("Current Jam Name: " + m.jam.Name)
|
2017-06-08 17:20:43 +00:00
|
|
|
} else {
|
2017-10-11 23:03:27 +00:00
|
|
|
fmt.Println("No Jam Name Specified")
|
2017-06-08 17:20:43 +00:00
|
|
|
}
|
2018-01-26 17:47:13 +00:00
|
|
|
|
|
|
|
if m.site.sessionSecret == "" {
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
fmt.Println("A good session secret is like a good password")
|
|
|
|
fmt.Print("Create New Session Secret: ")
|
|
|
|
sessSc, _ := reader.ReadString('\n')
|
|
|
|
sessSc = strings.TrimSpace(sessSc)
|
|
|
|
m.site.sessionSecret = sessSc
|
|
|
|
assertError(m.site.SaveToDB())
|
|
|
|
}
|
2017-04-03 21:32:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func loggingHandler(h http.Handler) http.Handler {
|
|
|
|
return handlers.LoggingHandler(os.Stdout, h)
|
|
|
|
}
|
|
|
|
|
2017-04-21 18:17:18 +00:00
|
|
|
func InitPageData(w http.ResponseWriter, req *http.Request) *pageData {
|
2017-10-21 22:47:01 +00:00
|
|
|
if m.site.DevMode {
|
2017-04-21 18:17:18 +00:00
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
}
|
|
|
|
p := new(pageData)
|
|
|
|
// Get session
|
|
|
|
var err error
|
|
|
|
var s *sessions.Session
|
2017-10-21 22:47:01 +00:00
|
|
|
if s, err = sessionStore.Get(req, m.site.SessionName); err != nil {
|
2018-01-29 21:45:58 +00:00
|
|
|
fmt.Println("Session error... Recreating.")
|
|
|
|
//http.Error(w, err.Error(), 500)
|
2017-04-21 18:17:18 +00:00
|
|
|
}
|
|
|
|
p.session = new(pageSession)
|
|
|
|
p.session.session = s
|
|
|
|
p.session.req = req
|
|
|
|
p.session.w = w
|
|
|
|
|
|
|
|
// First check if we're logged in
|
|
|
|
userEmail, _ := p.session.getStringValue("email")
|
|
|
|
// With a valid account
|
2017-10-19 19:53:07 +00:00
|
|
|
p.LoggedIn = m.isValidUserEmail(userEmail)
|
2017-04-21 18:17:18 +00:00
|
|
|
|
2017-10-21 22:47:01 +00:00
|
|
|
p.Site = m.site
|
2017-04-21 18:17:18 +00:00
|
|
|
p.SubTitle = "GameJam Voting"
|
|
|
|
p.Stylesheets = make([]string, 0, 0)
|
2017-06-15 19:09:07 +00:00
|
|
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css")
|
|
|
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/grids-responsive-min.css")
|
|
|
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/font-awesome/css/font-awesome.min.css")
|
2017-04-21 18:17:18 +00:00
|
|
|
p.Stylesheets = append(p.Stylesheets, "/assets/css/gjvote.css")
|
|
|
|
|
|
|
|
p.HeaderScripts = make([]string, 0, 0)
|
2017-06-15 19:09:07 +00:00
|
|
|
p.HeaderScripts = append(p.HeaderScripts, "/assets/vendor/js/snack-min.js")
|
2017-04-21 18:17:18 +00:00
|
|
|
|
|
|
|
p.Scripts = make([]string, 0, 0)
|
|
|
|
p.Scripts = append(p.Scripts, "/assets/js/gjvote.js")
|
|
|
|
|
|
|
|
p.FlashMessage, p.FlashClass = p.session.getFlashMessage()
|
|
|
|
if p.FlashClass == "" {
|
|
|
|
p.FlashClass = "hidden"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build the menu
|
|
|
|
if p.LoggedIn {
|
|
|
|
p.Menu = append(p.Menu, menuItem{"Admin", "/admin", "fa-key"})
|
|
|
|
p.Menu = append(p.Menu, menuItem{"Teams", "/admin/teams", "fa-users"})
|
|
|
|
p.Menu = append(p.Menu, menuItem{"Games", "/admin/games", "fa-gamepad"})
|
2017-06-22 15:34:57 +00:00
|
|
|
p.Menu = append(p.Menu, menuItem{"Votes", "/admin/votes", "fa-sticky-note"})
|
2017-10-03 14:01:32 +00:00
|
|
|
p.Menu = append(p.Menu, menuItem{"Archive", "/admin/archive", "fa-archive"})
|
2017-06-22 15:34:57 +00:00
|
|
|
p.Menu = append(p.Menu, menuItem{"Clients", "/admin/clients", "fa-desktop"})
|
2017-04-21 18:17:18 +00:00
|
|
|
|
|
|
|
p.BottomMenu = append(p.BottomMenu, menuItem{"Users", "/admin/users", "fa-user"})
|
|
|
|
p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/admin/dologout", "fa-sign-out"})
|
2017-06-08 17:20:43 +00:00
|
|
|
} else {
|
|
|
|
p.BottomMenu = append(p.BottomMenu, menuItem{"Admin", "/admin", "fa-sign-in"})
|
2017-04-21 18:17:18 +00:00
|
|
|
}
|
2017-06-08 17:20:43 +00:00
|
|
|
p.HideAdminMenu = true
|
2017-04-21 18:17:18 +00:00
|
|
|
|
2017-06-30 19:35:36 +00:00
|
|
|
p.ClientId = p.session.getClientId()
|
2017-10-21 22:47:01 +00:00
|
|
|
var cl *Client
|
|
|
|
if cl, err = m.GetClient(p.ClientId); err != nil {
|
|
|
|
// A new client
|
|
|
|
cl = NewClient(p.ClientId)
|
|
|
|
}
|
2017-10-02 23:06:35 +00:00
|
|
|
p.ClientIsAuth = cl.Auth
|
2017-06-15 17:35:53 +00:00
|
|
|
p.ClientIsServer = clientIsServer(req)
|
2017-06-08 17:20:43 +00:00
|
|
|
|
2017-06-22 15:34:57 +00:00
|
|
|
// Public Mode
|
2017-10-19 19:53:07 +00:00
|
|
|
p.PublicMode = m.site.GetPublicMode()
|
2017-07-19 15:16:18 +00:00
|
|
|
// Authentication Mode
|
2017-10-19 19:53:07 +00:00
|
|
|
p.AuthMode = m.site.GetAuthMode()
|
2017-06-22 15:34:57 +00:00
|
|
|
|
2017-04-21 18:17:18 +00:00
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
2017-04-03 21:32:13 +00:00
|
|
|
func (p *pageData) show(tmplName string, w http.ResponseWriter) error {
|
|
|
|
for _, tmpl := range []string{
|
|
|
|
"htmlheader.html",
|
|
|
|
"header.html",
|
|
|
|
tmplName,
|
|
|
|
"footer.html",
|
|
|
|
"htmlfooter.html",
|
|
|
|
} {
|
|
|
|
if err := outputTemplate(tmpl, p, w); err != nil {
|
|
|
|
fmt.Printf("%s\n", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// outputTemplate
|
|
|
|
// Spit out a template
|
|
|
|
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
|
2017-07-06 17:09:49 +00:00
|
|
|
n := "/templates/" + tmplName
|
2017-10-21 22:47:01 +00:00
|
|
|
l := template.Must(template.New("layout").Parse(FSMustString(m.site.DevMode, n)))
|
|
|
|
t := template.Must(l.Parse(FSMustString(m.site.DevMode, n)))
|
2017-07-06 17:09:49 +00:00
|
|
|
return t.Execute(w, tmplData)
|
2017-04-03 21:32:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// redirect can be used only for GET redirects
|
|
|
|
func redirect(url string, w http.ResponseWriter, req *http.Request) {
|
|
|
|
http.Redirect(w, req, url, 303)
|
|
|
|
}
|
|
|
|
|
2017-06-22 15:34:57 +00:00
|
|
|
func resetToDefaults() {
|
2017-10-11 23:03:27 +00:00
|
|
|
def := NewSiteData(m)
|
2017-06-22 15:34:57 +00:00
|
|
|
fmt.Println("Reset settings to defaults?")
|
2017-10-11 23:03:27 +00:00
|
|
|
fmt.Print(m.site.Title, " -> ", def.Title, "\n")
|
|
|
|
fmt.Print(m.site.Port, " -> ", def.Port, "\n")
|
|
|
|
fmt.Print(m.site.SessionName, " -> ", def.SessionName, "\n")
|
|
|
|
fmt.Print(m.site.ServerDir, " -> ", def.ServerDir, "\n")
|
2017-06-22 15:34:57 +00:00
|
|
|
fmt.Println("Are you sure? (y/N): ")
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
conf, _ := reader.ReadString('\n')
|
|
|
|
conf = strings.ToUpper(strings.TrimSpace(conf))
|
|
|
|
if strings.HasPrefix(conf, "Y") {
|
2017-10-19 19:53:07 +00:00
|
|
|
if err := def.SaveToDB(); err != nil {
|
|
|
|
errorExit("Error resetting to defaults: " + err.Error())
|
2017-06-22 15:34:57 +00:00
|
|
|
}
|
|
|
|
fmt.Println("Reset to defaults")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func printHelp() {
|
|
|
|
help := []string{
|
|
|
|
"Game Jam Voting Help",
|
|
|
|
" -help, -h, -? Print this message",
|
|
|
|
" -dev Development mode, load assets from file system",
|
|
|
|
" -port=<port num> Set the site port",
|
|
|
|
" -session-name=<session> Set the name of the session to be used",
|
|
|
|
" -server-dir=<directory> Set the server directory",
|
|
|
|
" This designates where the database will be saved",
|
|
|
|
" and where the app will look for files if you're",
|
|
|
|
" operating in 'development' mode (-dev)",
|
|
|
|
" -title=<title> Set the site title",
|
|
|
|
" -current-jam=<name> Change the name of the current jam",
|
|
|
|
" -reset-defaults Reset all configuration options to defaults",
|
|
|
|
"",
|
2017-04-03 21:32:13 +00:00
|
|
|
}
|
2017-06-22 15:34:57 +00:00
|
|
|
for _, v := range help {
|
|
|
|
fmt.Println(v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func done() {
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func errorExit(msg string) {
|
|
|
|
fmt.Println(msg)
|
|
|
|
os.Exit(1)
|
2017-04-03 21:32:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func assertError(err error) {
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|