The schema defines the absence of state->open to represent the unknown state. Previously it was not possible to explicitly set this state via the HTTP endpoints. types.SpaceAPIState.Open is now an enum which is either unknown, closed, or open. Custom IsZero() and MarshalJSON() methods on the type ensure marshaling the value yields a boolean for open/closed and an empty byte slice for unknown (resulting in the field being absent from the final response). The handlers.StateOpenDELETE() function has been added to expose this behavior. Switching to encoding/json/v2 required raising the minimum Go version in go.mod to 1.27. Closes: CCCHH/spaceapid!37 Fixes: CCCHH/spaceapid#38
132 lines
3.9 KiB
Go
132 lines
3.9 KiB
Go
package persistence
|
|
|
|
import (
|
|
"encoding/json/jsontext"
|
|
"encoding/json/v2"
|
|
"errors"
|
|
"log"
|
|
"os"
|
|
|
|
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
|
)
|
|
|
|
const (
|
|
persistentStateFile = "spaceapid-state.json"
|
|
persistentStatePath = persistentStateDir + persistentStateFile
|
|
)
|
|
|
|
// MergeOldState merges a given SpaceAPIResponse with the state saved at the time of last program exit and then deletes
|
|
// the file containing the old state.
|
|
func MergeOldState(response *types.SpaceAPIResponse) {
|
|
var (
|
|
err error
|
|
oldState []byte
|
|
persistedState types.PersistentState
|
|
)
|
|
|
|
log.Println("Merging old state from", persistentStatePath, "...")
|
|
|
|
// Check if state.json is present
|
|
_, err = os.Stat(persistentStatePath)
|
|
if err != nil {
|
|
log.Println("Old state json not accessible at", persistentStatePath, ", skipping merge... error:", err)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return
|
|
}
|
|
goto removeOld
|
|
}
|
|
|
|
// Read file and load persisted state
|
|
oldState, err = os.ReadFile(persistentStatePath)
|
|
if err != nil {
|
|
log.Println("Error reading old state from", persistentStatePath, ", skipping merge... error:", err)
|
|
goto removeOld
|
|
}
|
|
err = json.Unmarshal(oldState, &persistedState)
|
|
if err != nil {
|
|
log.Println(persistentStatePath, "doesn't seem to contain valid data... error:", err)
|
|
goto removeOld
|
|
}
|
|
|
|
// Merge state
|
|
response.State.FromPersistentSpaceState(persistedState)
|
|
|
|
// Merge sensors
|
|
for key, environmentSensors := range persistedState.Sensors {
|
|
for _, sensor := range environmentSensors {
|
|
// Order or amount of sensors might have changed, so check sensors already present in response
|
|
// and then look for matching values in persisted state
|
|
for i := range response.Sensors[key] {
|
|
if rs := &response.Sensors[key][i]; rs.Location == sensor.Location && rs.Name == sensor.Name {
|
|
rs.Value = sensor.Value
|
|
rs.LastChange = sensor.LastChange
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete old state json
|
|
removeOld:
|
|
err = os.RemoveAll(persistentStatePath)
|
|
if err != nil {
|
|
log.Println("Failed to remove", persistentStatePath, ", continuing... error:", err)
|
|
}
|
|
}
|
|
|
|
func SaveCurrentState(response types.SpaceAPIResponse) {
|
|
// Create state directory if not present
|
|
err := os.MkdirAll(persistentStateDir, 0750)
|
|
if err != nil {
|
|
log.Fatalln("Failed creating", persistentStateDir, ", aborting... error:", err)
|
|
}
|
|
|
|
// Open persistent state file
|
|
file, err := os.OpenFile(persistentStatePath, os.O_WRONLY|os.O_CREATE, 0644)
|
|
if err != nil {
|
|
log.Fatalln("Failed opening", persistentStatePath, "while trying to save current state... error:", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Create persistent state
|
|
var (
|
|
isOpen bool
|
|
isUnknown bool
|
|
)
|
|
switch response.State.Open {
|
|
case types.SpaceAPIStateOpenIsUnknown:
|
|
isUnknown = true
|
|
case types.SpaceAPIStateOpenIsClosed:
|
|
break
|
|
case types.SpaceAPIStateOpenIsOpen:
|
|
isOpen = true
|
|
}
|
|
persistentState := types.PersistentState{
|
|
State: types.PersistentSpaceState{
|
|
Open: isOpen, IsUnknown: isUnknown,
|
|
LastChange: response.State.LastChange, Message: response.State.Message,
|
|
},
|
|
Sensors: make(map[string][]types.PersistentEnvironmentSensor),
|
|
}
|
|
// Save sensor state
|
|
for key, environmentSensors := range response.Sensors {
|
|
persistentState.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors))
|
|
for i, sensor := range environmentSensors {
|
|
persistentState.Sensors[key][i].Value = sensor.Value
|
|
persistentState.Sensors[key][i].Location = sensor.Location
|
|
persistentState.Sensors[key][i].Name = sensor.Name
|
|
persistentState.Sensors[key][i].LastChange = sensor.LastChange
|
|
}
|
|
}
|
|
|
|
// Serialize persistent state
|
|
marshal, err := json.Marshal(persistentState, json.JoinOptions(jsontext.Multiline(true), jsontext.SpaceAfterComma(true)))
|
|
if err != nil {
|
|
log.Fatalln("Failed serializing persistent state... error:", err)
|
|
}
|
|
|
|
// Write to file
|
|
_, err = file.Write(marshal)
|
|
if err != nil {
|
|
log.Fatalln("Failed writing persistent state to file", file.Name(), "... error:", err)
|
|
}
|
|
}
|