Adding everything needed to implement admin API, still needs testing,

though
This commit is contained in:
Donaldson 2021-12-30 17:00:14 -08:00
parent 7fc2eec35f
commit b4b867bed8
2 changed files with 402 additions and 73 deletions

View File

@ -58,26 +58,26 @@ type StateProvider interface {
PointsLog() award.List // GET /admin/state/points
AwardPoints(teamID string, cat string, points int) error // POST /admin/state/points
// AwardPoints(teamID string, cat string, points int, when int64) error // POST /admin/state/points
// Points(teamID string, cat string, points int) bool // Check if point entry exists // HEAD /admin/state/points/<teamID>/<cat>/<points>
// Points(teamID string, cat string, points int, when int64) bool // Check if point entry exists // HEAD /admin/state/points/<teamID>/<cat>/<points>/<when>
// RemovePoints(teamID string, cat string, points int) error // DELETE /admin/state/points/<teamID>/<cat>/<points>
// RemovePoints(teamID string, cat string, points int, when int64) error // DELETE /admin/state/points/<teamID>/<cat>/<points>/<when>
// SetPoints(award.List) error // PUT /admin/state/points
AwardPointsAtTime(teamID string, cat string, points int, when int64) error // POST /admin/state/points
PointExists(teamID string, cat string, points int) bool // Check if point entry exists // HEAD /admin/state/points/<teamID>/<cat>/<points>
PointExistsAtTime(teamID string, cat string, points int, when int64) bool // Check if point entry exists // HEAD /admin/state/points/<teamID>/<cat>/<points>/<when>
RemovePoints(teamID string, cat string, points int) error // DELETE /admin/state/points/<teamID>/<cat>/<points>
RemovePointsAtTime(teamID string, cat string, points int, when int64) error // DELETE /admin/state/points/<teamID>/<cat>/<points>/<when>
SetPoints(award.List) error // PUT /admin/state/points
// TeamIDs() []string // GET /admin/state/team_ids
// SetTeamIDs([] string) error // PUT /admin/state/team_ids
// AddTeamID(teamID string) error // POST /admin/state/team_ids/<teamID>
// RemoveTeamID(teamID string) error // DELETE /admin/state/team_ids/<teamID>
// TeamID(teamID string) bool // HEAD /admin/state/team_ids/<teamID>
TeamIDs() ([]string, error) // GET /admin/state/team_ids
SetTeamIDs([] string) error // PUT /admin/state/team_ids
AddTeamID(teamID string) error // POST /admin/state/team_ids/<teamID>
RemoveTeamID(teamID string) error // DELETE /admin/state/team_ids/<teamID>
TeamIDExists(teamID string) (bool, error) // HEAD /admin/state/team_ids/<teamID>
// TeamNames() (map[string]string, error) // GET /admin/state/teams
// SetTeamNames(map[string]string) error // PUT /admin/state/teams
TeamNames() map[string]string // GET /admin/state/teams
SetTeamNames(map[string]string) error // PUT /admin/state/teams
TeamName(teamID string) (string, error) // GET /admin/state/teams/id/<teamID>
// TeamIDFromName(teamName string) (string, error) // GET /admin/state/teams/name/<teamName>
TeamIDFromName(teamName string) (string, error) // GET /admin/state/teams/name/<teamName>
SetTeamName(teamID, teamName string) error // POST /admin/state/teams/id/<teamID>/<teamName>
// DeleteTeamName(teamID string) error // DELETE /admin/state/teams/id/<teamID>, /admin/state/teams/name/<teamName>
DeleteTeamName(teamID string) error // DELETE /admin/state/teams/id/<teamID>, /admin/state/teams/name/<teamName>
LogEvent(event, participantID, teamID, cat string, points int, extra ...string)
Maintainer

View File

@ -48,7 +48,13 @@ type State struct {
teamNames map[string]string
pointsLog award.List
messages string
lock sync.RWMutex
teamIDLock sync.RWMutex
teamIDFileLock sync.RWMutex
teamNameLock sync.RWMutex
teamNameFileLock sync.RWMutex
pointsLock sync.RWMutex
pointsLogFileLock sync.RWMutex // Sometimes, we need to fiddle with the file, while leaving the internal state alone
messageFileLock sync.RWMutex
}
// NewState returns a new State struct backed by the given Fs
@ -127,33 +133,167 @@ func (s *State) updateEnabled() {
}
}
/* ****************** Team ID functions ****************** */
func (s *State) TeamIDs() ([]string, error) {
var teamIDs []string
s.teamIDFileLock.RLock()
defer s.teamIDFileLock.RUnlock()
idsFile, err := s.Open("teamids.txt")
if err != nil {
return teamIDs, fmt.Errorf("team IDs file does not exist")
}
defer idsFile.Close()
scanner := bufio.NewScanner(idsFile)
for scanner.Scan() {
teamIDs = append(teamIDs, scanner.Text())
}
return teamIDs, nil
}
func (s *State) writeTeamIDs(teamIDs []string) error {
s.teamIDFileLock.Lock()
defer s.teamIDFileLock.Unlock()
if f, err := s.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil {
defer f.Close()
for _, teamID := range teamIDs {
fmt.Fprintln(f, string(teamID))
}
} else {
return err
}
return nil
}
func (s *State) SetTeamIDs(teamIDs []string) error {
s.teamIDLock.Lock()
defer s.teamIDLock.Unlock()
return s.writeTeamIDs(teamIDs)
}
func (s *State) AddTeamID(newTeamID string) error {
s.teamIDLock.Lock()
defer s.teamIDLock.Unlock()
teamIDs, err := s.TeamIDs()
if err != nil {
return err
}
for _, teamID := range teamIDs {
if newTeamID == teamID {
return fmt.Errorf("Team ID already exists")
}
}
teamIDs = append(teamIDs, newTeamID)
return s.writeTeamIDs(teamIDs)
}
func (s *State) RemoveTeamID(removeTeamID string) error {
s.teamIDLock.Lock()
defer s.teamIDLock.Unlock()
teamIDs, err := s.TeamIDs()
if err != nil {
return err
}
for _, teamID := range teamIDs {
if removeTeamID != teamID {
teamIDs = append(teamIDs, teamID)
}
}
return s.writeTeamIDs(teamIDs)
}
func (s *State) TeamIDExists(teamID string) (bool, error) {
s.teamIDLock.RLock()
defer s.teamIDLock.RUnlock()
teamIDs, err := s.TeamIDs()
if err != nil {
return false, err
}
for _, candidateTeamID := range teamIDs {
if teamID == candidateTeamID {
return true, nil
}
}
return false, nil
}
/* ********************* Team Name functions ********* */
// TeamName returns team name given a team ID.
func (s *State) TeamName(teamID string) (string, error) {
s.lock.RLock()
s.teamNameLock.RLock()
defer s.teamNameLock.RUnlock()
name, ok := s.teamNames[teamID]
s.lock.RUnlock()
if !ok {
return "", fmt.Errorf("unregistered team ID: %s", teamID)
}
return name, nil
}
func (s *State) TeamNames() map[string]string {
var teamNames map[string]string
s.teamNameFileLock.RLock()
defer s.teamNameFileLock.RUnlock()
teamsFs := afero.NewBasePathFs(s.Fs, "teams")
if dirents, err := afero.ReadDir(teamsFs, "."); err != nil {
log.Printf("Reading team ids: %v", err)
} else {
for _, dirent := range dirents {
teamID := dirent.Name()
if teamNameBytes, err := afero.ReadFile(teamsFs, teamID); err != nil {
log.Printf("Reading team %s: %v", teamID, err)
} else {
teamName := strings.TrimSpace(string(teamNameBytes))
teamNames[teamID] = teamName
}
}
}
return teamNames
}
// SetTeamName writes out team name.
// This can only be done once per team.
func (s *State) SetTeamName(teamID, teamName string) error {
idsFile, err := s.Open("teamids.txt")
teamIDs, err := s.TeamIDs()
if err != nil {
return fmt.Errorf("team IDs file does not exist")
return err
}
defer idsFile.Close()
found := false
scanner := bufio.NewScanner(idsFile)
for scanner.Scan() {
if scanner.Text() == teamID {
for _, validTeamID := range teamIDs {
if validTeamID == teamID {
found = true
break
}
}
if !found {
return fmt.Errorf("team ID not found in list of valid team IDs")
}
@ -175,32 +315,69 @@ func (s *State) SetTeamName(teamID, teamName string) error {
return nil
}
// PointsLog retrieves the current points log.
func (s *State) PointsLog() award.List {
s.lock.RLock()
ret := make(award.List, len(s.pointsLog))
copy(ret, s.pointsLog)
s.lock.RUnlock()
return ret
func (s *State) SetTeamNames(teams map[string]string) error {
return s.writeTeamNames(teams)
}
// Messages retrieves the current messages.
func (s *State) Messages() string {
s.lock.RLock() // It's not clear to me that this actually needs to happen
defer s.lock.RUnlock()
return s.messages
func (s *State) TeamIDFromName(teamName string) (string, error) {
for name, id := range s.TeamNames() {
if name == teamName {
return id, nil
}
}
return "", fmt.Errorf("team name not found")
}
// SetMessages sets the current message
func (s *State) SetMessages(message string) error {
s.lock.Lock()
defer s.lock.Unlock()
func (s *State) DeleteTeamName(teamID string) error {
newTeams := s.TeamNames()
err := afero.WriteFile(s, "messages.html", []byte(message), 0600)
_, ok := newTeams[teamID];
if ok {
delete(newTeams, teamID)
} else {
return fmt.Errorf("team not found")
}
return s.writeTeamNames(newTeams)
}
func (s *State) writeTeamNames(teams map[string]string) error {
s.teamNameFileLock.Lock()
defer s.teamNameFileLock.Unlock()
s.RemoveAll("teams")
s.Mkdir("teams", 0755)
// Write out all of the new team names
for teamID, teamName := range teams {
teamFilename := filepath.Join("teams", teamID)
teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
if err != nil {
return err
}
defer teamFile.Close()
log.Printf("Setting team name [%s] in file %s", teamName, teamFilename)
fmt.Fprintln(teamFile, teamName)
teamFile.Close()
}
s.refreshNow <- true
return err
return nil
}
/* **************** Point log functions ************ */
// PointsLog retrieves the current points log.
func (s *State) PointsLog() award.List {
s.pointsLock.RLock()
ret := make(award.List, len(s.pointsLog))
copy(ret, s.pointsLog)
s.pointsLock.RUnlock()
return ret
}
// AwardPoints gives points to teamID in category.
@ -213,6 +390,10 @@ func (s *State) AwardPoints(teamID, category string, points int) error {
return s.awardPointsAtTime(time.Now().Unix(), teamID, category, points)
}
func (s *State) AwardPointsAtTime(teamID, category string, points int, when int64) error {
return s.awardPointsAtTime(when, teamID, category, points)
}
func (s *State) awardPointsAtTime(when int64, teamID string, category string, points int) error {
a := award.T{
When: when,
@ -246,6 +427,119 @@ func (s *State) awardPointsAtTime(when int64, teamID string, category string, po
return nil
}
func (s *State) PointExists(teamID string, cat string, points int) bool {
for _, pointEntry := range s.pointsLog {
if (pointEntry.TeamID == teamID) && (pointEntry.Category == cat) && (pointEntry.Points == points) {
return true
}
}
return false
}
func (s *State) PointExistsAtTime(teamID string, cat string, points int, when int64) bool {
for _, pointEntry := range s.pointsLog {
if (pointEntry.TeamID == teamID) && (pointEntry.Category == cat) && (pointEntry.Points == points) && (pointEntry.When == when) {
return true
}
if (pointEntry.When > when) { // Since the points log is sorted, we can bail out earlier, if we see that current points are from later than our event
return false
}
}
return false
}
func (s *State) flushPointsLog(newPoints award.List) error {
s.pointsLogFileLock.Lock()
defer s.pointsLogFileLock.Unlock()
logf, err := s.OpenFile("points.log", os.O_CREATE|os.O_WRONLY, 0644)
defer logf.Close()
if err != nil {
return fmt.Errorf("Can't write to points log: ", err)
}
for _, pointEntry := range newPoints {
fmt.Fprintln(logf, pointEntry.String())
}
return nil
}
func (s *State) RemovePoints(teamID string, cat string, points int) error {
s.pointsLock.Lock()
defer s.pointsLock.Unlock()
var newPoints award.List
removed := false
for _, pointEntry := range s.pointsLog {
if (pointEntry.TeamID == teamID) && (pointEntry.Category == cat) && (pointEntry.Points == points) {
removed = true
} else {
newPoints = append(newPoints, pointEntry)
}
}
if (! removed) {
return fmt.Errorf("Unable to find matching point entry")
}
err := s.flushPointsLog(newPoints)
if err != nil {
return err
}
s.refreshNow <- true
return nil
}
func (s *State) RemovePointsAtTime(teamID string, cat string, points int, when int64) error {
s.pointsLock.Lock()
defer s.pointsLock.Unlock()
var newPoints award.List
removed := false
for _, pointEntry := range s.pointsLog {
if (pointEntry.TeamID == teamID) && (pointEntry.Category == cat) && (pointEntry.Points == points) && (pointEntry.When == when) {
removed = true
} else {
newPoints = append(newPoints, pointEntry)
}
}
if (! removed) {
return fmt.Errorf("Unable to find matching point entry")
}
err := s.flushPointsLog(newPoints)
if err != nil {
return err
}
s.refreshNow <- true
return nil
}
func (s *State) SetPoints(newPoints award.List) error {
err := s.flushPointsLog(newPoints)
if err != nil {
return err
}
s.refreshNow <- true
return nil
}
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
// removing each points.new/ file as it goes.
func (s *State) collectPoints() {
@ -268,20 +562,23 @@ func (s *State) collectPoints() {
}
duplicate := false
s.lock.RLock()
s.pointsLock.RLock()
for _, e := range s.pointsLog {
if awd.Equal(e) {
duplicate = true
break
}
}
s.lock.RUnlock()
s.pointsLock.RUnlock()
if duplicate {
log.Print("Skipping duplicate points: ", awd.String())
} else {
log.Print("Award: ", awd.String())
s.pointsLogFileLock.Lock()
defer s.pointsLogFileLock.Unlock()
logf, err := s.OpenFile("points.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Print("Can't append to points log: ", err)
@ -291,9 +588,9 @@ func (s *State) collectPoints() {
logf.Close()
// Stick this on the cache too
s.lock.Lock()
s.pointsLock.Lock()
defer s.pointsLock.Unlock()
s.pointsLog = append(s.pointsLog, awd)
s.lock.Unlock()
}
if err := s.Remove(filename); err != nil {
@ -302,24 +599,57 @@ func (s *State) collectPoints() {
}
}
/* ******************* Message functions *********** */
// Messages retrieves the current messages.
func (s *State) Messages() string {
return s.messages
}
// SetMessages sets the current message
func (s *State) SetMessages(message string) error {
s.messageFileLock.Lock()
defer s.messageFileLock.Unlock()
err := afero.WriteFile(s, "messages.html", []byte(message), 0600)
s.refreshNow <- true
return err
}
/* ***************** Other utilitity functions ******* */
func (s *State) maybeInitialize() {
// Are we supposed to re-initialize?
if _, err := s.Stat("initialized"); !os.IsNotExist(err) {
return
}
now := time.Now().UTC().Format(time.RFC3339)
log.Print("initialized file missing, re-initializing")
// Remove any extant control and state files
s.Remove("enabled")
s.Remove("hours.txt")
s.pointsLogFileLock.Lock()
s.Remove("points.log")
s.pointsLogFileLock.Unlock()
s.messageFileLock.Lock()
s.Remove("messages.html")
s.messageFileLock.Unlock()
s.Remove("mothd.log")
s.RemoveAll("points.tmp")
s.RemoveAll("points.new")
s.teamNameFileLock.Lock()
s.RemoveAll("teams")
s.Mkdir("teams", 0755)
s.teamNameFileLock.Unlock()
// Open log file
if err := s.reopenEventLog(); err != nil {
@ -330,20 +660,18 @@ func (s *State) maybeInitialize() {
// Make sure various subdirectories exist
s.Mkdir("points.tmp", 0755)
s.Mkdir("points.new", 0755)
s.Mkdir("teams", 0755)
// Preseed available team ids if file doesn't exist
if f, err := s.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil {
id := make([]byte, 8)
for i := 0; i < 100; i++ {
for i := range id {
char := rand.Intn(len(DistinguishableChars))
id[i] = DistinguishableChars[char]
}
fmt.Fprintln(f, string(id))
var teamIDs []string
id := make([]byte, 8)
for i := 0; i < 100; i++ {
for i := range id {
char := rand.Intn(len(DistinguishableChars))
id[i] = DistinguishableChars[char]
}
f.Close()
teamIDs = append(teamIDs, string(id))
}
s.SetTeamIDs(teamIDs)
// Create some files
if f, err := s.Create("initialized"); err == nil {
@ -373,10 +701,12 @@ func (s *State) maybeInitialize() {
f.Close()
}
s.messageFileLock.Lock()
if f, err := s.Create("messages.html"); err == nil {
fmt.Fprintln(f, "<!-- messages.html: put client broadcast messages here. -->")
f.Close()
}
s.messageFileLock.Unlock()
if f, err := s.Create("points.log"); err == nil {
f.Close()
@ -418,8 +748,11 @@ func (s *State) reopenEventLog() error {
}
func (s *State) updateCaches() {
s.lock.Lock()
defer s.lock.Unlock()
s.pointsLock.RLock()
defer s.pointsLock.RUnlock()
s.pointsLogFileLock.RLock()
defer s.pointsLogFileLock.RUnlock()
if f, err := s.Open("points.log"); err != nil {
log.Println(err)
@ -437,32 +770,28 @@ func (s *State) updateCaches() {
}
pointsLog = append(pointsLog, cur)
}
s.pointsLog = pointsLog
}
{
{
s.teamNameLock.Lock()
defer s.teamNameLock.Unlock()
// The compiler recognizes this as an optimization case
for k := range s.teamNames {
delete(s.teamNames, k)
}
teamsFs := afero.NewBasePathFs(s.Fs, "teams")
if dirents, err := afero.ReadDir(teamsFs, "."); err != nil {
log.Printf("Reading team ids: %v", err)
} else {
for _, dirent := range dirents {
teamID := dirent.Name()
if teamNameBytes, err := afero.ReadFile(teamsFs, teamID); err != nil {
log.Printf("Reading team %s: %v", teamID, err)
} else {
teamName := strings.TrimSpace(string(teamNameBytes))
s.teamNames[teamID] = teamName
}
}
for teamID, teamName := range s.TeamNames() {
s.teamNames[teamID] = teamName
}
}
s.messageFileLock.RLock()
defer s.messageFileLock.RUnlock()
if bMessages, err := afero.ReadFile(s, "messages.html"); err == nil {
s.messages = string(bMessages)
}