Added database backup/restore.

This commit is contained in:
Patrick Fairbank
2014-06-07 02:02:12 -07:00
parent f7c3a4f682
commit 247b4f05eb
8 changed files with 296 additions and 58 deletions

12
main.go
View File

@@ -9,18 +9,24 @@ import (
"time"
)
const eventDbPath = "./event.db"
var db *Database
var eventSettings *EventSettings
func main() {
rand.Seed(time.Now().UnixNano())
initDb()
ServeWebInterface()
}
func initDb() {
var err error
db, err = OpenDatabase("test.db")
db, err = OpenDatabase(eventDbPath)
checkErr(err)
eventSettings, err = db.GetEventSettings()
checkErr(err)
ServeWebInterface()
}
func checkErr(err error) {

View File

@@ -6,10 +6,16 @@
package main
import (
"fmt"
"html/template"
"io"
"io/ioutil"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
)
// Shows the event settings editing page.
@@ -19,11 +25,6 @@ func SettingsGetHandler(w http.ResponseWriter, r *http.Request) {
// Saves the event settings.
func SettingsPostHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
handleWebErr(w, err)
return
}
eventSettings.Name = r.PostFormValue("name")
eventSettings.Code = r.PostFormValue("code")
match, _ := regexp.MatchString("^#([0-9A-Fa-f]{3}){1,2}$", r.PostFormValue("displayBackgroundColor"))
@@ -41,12 +42,94 @@ func SettingsPostHandler(w http.ResponseWriter, r *http.Request) {
eventSettings.SelectionRound1Order = r.PostFormValue("selectionRound1Order")
eventSettings.SelectionRound2Order = r.PostFormValue("selectionRound2Order")
eventSettings.SelectionRound3Order = r.PostFormValue("selectionRound3Order")
err = db.SaveEventSettings(eventSettings)
err := db.SaveEventSettings(eventSettings)
if err != nil {
handleWebErr(w, err)
return
}
renderSettings(w, r, "")
http.Redirect(w, r, "/setup/settings", 302)
}
// Sends a copy of the event database file to the client as a download.
func SaveDbHandler(w http.ResponseWriter, r *http.Request) {
dbFile, err := os.Open(db.path)
defer dbFile.Close()
if err != nil {
handleWebErr(w, err)
return
}
filename := fmt.Sprintf("%s-%s.db", strings.Replace(eventSettings.Name, " ", "_", -1),
time.Now().Format("20060102150405"))
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
http.ServeContent(w, r, "", time.Now(), dbFile)
}
// Accepts an event database file as an upload and loads it.
func RestoreDbHandler(w http.ResponseWriter, r *http.Request) {
file, _, err := r.FormFile("databaseFile")
if err != nil {
renderSettings(w, r, "No database backup file was specified.")
return
}
// Write the file to a temporary location on disk and verify that it can be opened as a database.
tempFile, err := ioutil.TempFile(".", "uploaded-db-")
if err != nil {
handleWebErr(w, err)
return
}
defer tempFile.Close()
tempFilePath := tempFile.Name()
defer os.Remove(tempFilePath)
_, err = io.Copy(tempFile, file)
if err != nil {
handleWebErr(w, err)
return
}
tempFile.Close()
tempDb, err := OpenDatabase(tempFilePath)
if err != nil {
renderSettings(w, r, "Could not read uploaded database backup file. Please verify that it a valid "+
"database file.")
return
}
tempDb.Close()
// Replace the current database with the new one.
db.Close()
err = os.Rename(tempFilePath, eventDbPath)
if err != nil {
handleWebErr(w, err)
return
}
initDb()
http.Redirect(w, r, "/setup/settings", 302)
}
// Deletes all data except for the team list.
func ClearDbHandler(w http.ResponseWriter, r *http.Request) {
err := db.TruncateMatches()
if err != nil {
handleWebErr(w, err)
return
}
err = db.TruncateMatchResults()
if err != nil {
handleWebErr(w, err)
return
}
err = db.TruncateRankings()
if err != nil {
handleWebErr(w, err)
return
}
err = db.TruncateAllianceTeams()
if err != nil {
handleWebErr(w, err)
return
}
http.Redirect(w, r, "/setup/settings", 302)
}
func renderSettings(w http.ResponseWriter, r *http.Request, errorMessage string) {

View File

@@ -4,7 +4,12 @@
package main
import (
"bytes"
"github.com/stretchr/testify/assert"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
)
@@ -28,7 +33,8 @@ func TestSetupSettings(t *testing.T) {
// Change the settings and check the response.
recorder = postHttpResponse("/setup/settings", "name=Chezy Champs&code=CC&displayBackgroundColor=#ff00ff&"+
"numElimAlliances=16")
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/settings")
assert.Contains(t, recorder.Body.String(), "Chezy Champs")
assert.Contains(t, recorder.Body.String(), "CC")
assert.Contains(t, recorder.Body.String(), "#ff00ff")
@@ -52,3 +58,88 @@ func TestSetupSettingsInvalidValues(t *testing.T) {
recorder = postHttpResponse("/setup/settings", "numAlliances=1&displayBackgroundColor=#000")
assert.Contains(t, recorder.Body.String(), "must be between 2 and 16")
}
func TestSetupSettingsClearDb(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
db.CreateTeam(new(Team))
db.CreateMatch(&Match{Type: "qualification"})
db.CreateMatchResult(new(MatchResult))
db.CreateRanking(new(Ranking))
db.CreateAllianceTeam(new(AllianceTeam))
recorder := postHttpResponse("/setup/db/clear", "")
assert.Equal(t, 302, recorder.Code)
teams, _ := db.GetAllTeams()
assert.NotEmpty(t, teams)
matches, _ := db.GetMatchesByType("qualification")
assert.Empty(t, matches)
rankings, _ := db.GetAllRankings()
assert.Empty(t, rankings)
db.CalculateRankings()
assert.Empty(t, rankings)
alliances, _ := db.GetAllAlliances()
assert.Empty(t, alliances)
}
func TestSetupSettingsBackupRestoreDb(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
// Modify a parameter so that we know when the database has been restored.
eventSettings.Name = "Chezy Champs"
db.SaveEventSettings(eventSettings)
// Back up the database.
recorder := getHttpResponse("/setup/db/save")
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "application/octet-stream", recorder.HeaderMap["Content-Type"][0])
backupBody := recorder.Body
// Wipe the database to reset the defaults.
clearDb()
defer clearDb()
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
assert.NotEqual(t, "Chezy Champs", eventSettings.Name)
// Check restoring with a missing file.
recorder = postHttpResponse("/setup/db/restore", "")
assert.Contains(t, recorder.Body.String(), "No database backup file was specified")
assert.NotEqual(t, "Chezy Champs", eventSettings.Name)
// Check restoring with a corrupt file.
recorder = postFileHttpResponse("/setup/db/restore", "databaseFile", bytes.NewBufferString("invalid"))
assert.Contains(t, recorder.Body.String(), "Could not read uploaded database backup file")
assert.NotEqual(t, "Chezy Champs", eventSettings.Name)
// Check restoring with the backup retrieved before.
recorder = postFileHttpResponse("/setup/db/restore", "databaseFile", backupBody)
assert.Equal(t, "Chezy Champs", eventSettings.Name)
}
func postFileHttpResponse(path string, paramName string, file *bytes.Buffer) *httptest.ResponseRecorder {
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile(paramName, "file.ext")
io.Copy(part, file)
writer.Close()
recorder := httptest.NewRecorder()
req, _ := http.NewRequest("POST", path, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
newHandler().ServeHTTP(recorder, req)
return recorder
}

View File

@@ -34,11 +34,6 @@ func TeamsPostHandler(w http.ResponseWriter, r *http.Request) {
return
}
err := r.ParseForm()
if err != nil {
handleWebErr(w, err)
return
}
var teamNumbers []int
for _, teamNumberString := range strings.Split(r.PostFormValue("teamNumbers"), "\r\n") {
teamNumber, err := strconv.Atoi(teamNumberString)
@@ -59,7 +54,7 @@ func TeamsPostHandler(w http.ResponseWriter, r *http.Request) {
return
}
}
renderTeams(w, r, false)
http.Redirect(w, r, "/setup/teams", 302)
}
// Clears the team list.
@@ -74,7 +69,7 @@ func TeamsClearHandler(w http.ResponseWriter, r *http.Request) {
handleWebErr(w, err)
return
}
renderTeams(w, r, false)
http.Redirect(w, r, "/setup/teams", 302)
}
// Shows the page to edit a team's fields.
@@ -121,11 +116,6 @@ func TeamEditPostHandler(w http.ResponseWriter, r *http.Request) {
return
}
err = r.ParseForm()
if err != nil {
handleWebErr(w, err)
return
}
team.Name = r.PostFormValue("name")
team.Nickname = r.PostFormValue("nickname")
team.City = r.PostFormValue("city")
@@ -138,7 +128,7 @@ func TeamEditPostHandler(w http.ResponseWriter, r *http.Request) {
handleWebErr(w, err)
return
}
renderTeams(w, r, false)
http.Redirect(w, r, "/setup/teams", 302)
}
// Removes a team from the team list.
@@ -164,7 +154,7 @@ func TeamDeletePostHandler(w http.ResponseWriter, r *http.Request) {
handleWebErr(w, err)
return
}
renderTeams(w, r, false)
http.Redirect(w, r, "/setup/teams", 302)
}
func renderTeams(w http.ResponseWriter, r *http.Request, showErrorMessage bool) {

View File

@@ -37,14 +37,16 @@ func TestSetupTeams(t *testing.T) {
// Add some teams.
recorder = postHttpResponse("/setup/teams", "teamNumbers=254\r\nnotateam\r\n1114\r\n")
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/teams")
assert.Contains(t, recorder.Body.String(), "2 teams")
assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs")
assert.Contains(t, recorder.Body.String(), "1114")
// Add another team.
recorder = postHttpResponse("/setup/teams", "teamNumbers=33")
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/teams")
assert.Contains(t, recorder.Body.String(), "3 teams")
assert.Contains(t, recorder.Body.String(), "33")
@@ -53,17 +55,20 @@ func TestSetupTeams(t *testing.T) {
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs")
recorder = postHttpResponse("/setup/teams/254/edit", "nickname=Teh Chezy Pofs")
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/teams")
assert.Contains(t, recorder.Body.String(), "Teh Chezy Pofs")
// Delete a team.
recorder = postHttpResponse("/setup/teams/1114/delete", "")
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/teams")
assert.Contains(t, recorder.Body.String(), "2 teams")
// Test clearing all teams.
recorder = postHttpResponse("/setup/teams/clear", "")
assert.Equal(t, 200, recorder.Code)
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/teams")
assert.Contains(t, recorder.Body.String(), "0 teams")
}
@@ -98,6 +103,8 @@ func TestSetupTeamsDisallowModification(t *testing.T) {
// Allow editing a team.
recorder = postHttpResponse("/setup/teams/254/edit", "nickname=Teh Chezy Pofs")
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/teams")
assert.NotContains(t, recorder.Body.String(), "can't modify")
assert.Contains(t, recorder.Body.String(), "1 teams")
assert.Contains(t, recorder.Body.String(), "Teh Chezy Pofs")

View File

@@ -1,17 +1,17 @@
{{define "title"}}Settings{{end}}
{{define "body"}}
<div class="row">
<div class="col-lg-6 col-lg-offset-3">
{{if .ErrorMessage}}
<div class="alert alert-dismissable alert-danger">
<button type="button" class="close" data-dismiss="alert">×</button>
{{.ErrorMessage}}
</div>
{{end}}
<div class="col-lg-6 col-lg-offset-1">
<div class="well">
<form class="form-horizontal" action="/setup/settings" method="POST">
<fieldset>
<legend>Event Settings</legend>
{{if .ErrorMessage}}
<div class="alert alert-dismissable alert-danger">
<button type="button" class="close" data-dismiss="alert">×</button>
{{.ErrorMessage}}
</div>
{{end}}
<div class="form-group">
<label for="textArea" class="col-lg-5 control-label">Name</label>
<div class="col-lg-7">
@@ -113,6 +113,63 @@
</form>
</div>
</div>
<div class="col-lg-4">
<div class="well">
<legend>Database</legend>
<p>
<a href="/setup/db/save"><button class="btn btn-info">Save Copy of Database</button></a>
</p>
<p>
<button class="btn btn-primary"onclick="$('#uploadDatabase').modal('show'); return false;">
Load Database from Backup
</button>
</p>
<p>
<button class="btn btn-primary"onclick="$('#confirmClearData').modal('show'); return false;">
Clear All Match Data
</button>
</p>
</div>
</div>
</div>
<div id="uploadDatabase" class="modal" style="top: 20%;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Choose Backup File</h4>
</div>
<form class="form-horizontal" action="/setup/db/restore" enctype="multipart/form-data" method="POST">
<div class="modal-body">
<p>Select the database file to load from. <b>This will overwrite any existing data.</b></p>
<input type="file" name="databaseFile">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Load Database from Backup</button>
</div>
</form>
</div>
</div>
</div>
<div id="confirmClearData" class="modal" style="top: 20%;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Confirm</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to clear all match, ranking, and alliance selection data?</p>
</div>
<div class="modal-footer">
<form class="form-horizontal" action="/setup/db/clear" method="POST">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Clear All Match Data</button>
</form>
</div>
</div>
</div>
</div>
{{end}}
{{define "script"}}

View File

@@ -1,11 +1,11 @@
{{define "title"}}Team List{{end}}
{{define "body"}}
{{if .ShowErrorMessage}}
<div class="alert alert-dismissable alert-danger">
<button type="button" class="close" data-dismiss="alert">×</button>
You can't modify the team list once the qualification schedule has been generated. If you need to change the
team list, clear all other data first on the Settings page.
</div>
<div class="alert alert-dismissable alert-danger">
<button type="button" class="close" data-dismiss="alert">×</button>
You can't modify the team list once the qualification schedule has been generated. If you need to change
the team list, clear all other data first on the Settings page.
</div>
{{end}}
<div class="row">
<div class="col-lg-2">
@@ -67,24 +67,25 @@
</table>
<b>{{len .Teams}} teams</b>
</div>
<div id="confirmClearTeams" class="modal" style="top: 20%;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Confirm</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to clear the team list?</p>
</div>
<div class="modal-footer">
<form class="form-horizontal" action="/setup/teams/clear" method="POST">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Clear Team List</button>
</form>
</div>
</div>
<div id="confirmClearTeams" class="modal" style="top: 20%;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Confirm</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to clear the team list?</p>
</div>
<div class="modal-footer">
<form class="form-horizontal" action="/setup/teams/clear" method="POST">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Clear Team List</button>
</form>
</div>
</div>
</div>
</div>
{{end}}
{{define "script"}}{{end}}

3
web.go
View File

@@ -65,6 +65,9 @@ func newHandler() http.Handler {
router := mux.NewRouter()
router.HandleFunc("/setup/settings", SettingsGetHandler).Methods("GET")
router.HandleFunc("/setup/settings", SettingsPostHandler).Methods("POST")
router.HandleFunc("/setup/db/save", SaveDbHandler).Methods("GET")
router.HandleFunc("/setup/db/restore", RestoreDbHandler).Methods("POST")
router.HandleFunc("/setup/db/clear", ClearDbHandler).Methods("POST")
router.HandleFunc("/setup/teams", TeamsGetHandler).Methods("GET")
router.HandleFunc("/setup/teams", TeamsPostHandler).Methods("POST")
router.HandleFunc("/setup/teams/clear", TeamsClearHandler).Methods("POST")