spaceapid/persistence/persistentState.go
Bennett Wetters 11046cb501 feat: Allow setting state->open to unknown via DELETE
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
2026-10-09 00:30:26 +02:00

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)
}
}