package locationRecorder import ( "database/sql" "encoding/json" "fmt" "log/slog" "net/http" "os" "strconv" "time" "github.com/spf13/viper" ) var ( db *sql.DB ) const ( currentDBVersion = 2 ) type Location struct { Person string `json:"person"` DateTime string `json:"datetime"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` Altitude sql.NullFloat64 `json:"altitude,omitempty"` } type LocationContent struct { Person string `json:"person"` Latitude string `json:"latitude"` Longitude string `json:"longitude"` Altitude string `json:"altitude,omitempty"` } func Init() { initDb() } func HandleRecordLocation(w http.ResponseWriter, r *http.Request) { var location LocationContent decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() err := decoder.Decode(&location) if err != nil { slog.Warn(fmt.Sprintln("HandleRecordLocation Error decoding request body", err)) http.Error(w, err.Error(), http.StatusBadRequest) return } latiF64, _ := strconv.ParseFloat(location.Latitude, 64) longiF64, _ := strconv.ParseFloat(location.Longitude, 64) altiF64, _ := strconv.ParseFloat(location.Altitude, 64) InsertLocationNow(location.Person, latiF64, longiF64, altiF64) } func InsertLocation(person string, datetime time.Time, latitude float64, longitude float64, altitude float64) { _, err := db.Exec(`INSERT OR IGNORE INTO location (person, datetime, latitude, longitude, altitude) VALUES (?, ?, ?, ?, ?)`, person, datetime.UTC().Format(time.RFC3339), latitude, longitude, altitude) if err != nil { slog.Error(fmt.Sprintln("LocationRecorder.InsertLocation Error inserting location", err)) } } func InsertLocationNow(person string, latitude float64, longitude float64, altitude float64) { InsertLocation(person, time.Now(), latitude, longitude, altitude) } func initDb() { if !viper.InConfig("locationRecorder.dbPath") { slog.Info("LocationRecorderInit dbPath not found in config file, using default: location_recorder.db") viper.SetDefault("locationRecorder.dbPath", "location_recorder.db") } dbPath := viper.GetString("locationRecorder.dbPath") err := error(nil) db, err = sql.Open("sqlite", dbPath) if err != nil { slog.Error(fmt.Sprintln("LocationRecorderInit Error opening database", err)) os.Exit(1) } err = db.Ping() if err != nil { slog.Error(fmt.Sprintln("LocationRecorderInit Error pinging database", err)) os.Exit(1) } migrateDb() } func migrateDb() { var userVersion int err := db.QueryRow("PRAGMA user_version").Scan(&userVersion) if err != nil { slog.Error(fmt.Sprintln("LocationRecorderInit Error getting db user version", err)) os.Exit(1) } if userVersion == 0 { migrateDb0To1(&userVersion) } if userVersion == 1 { migrateDb1To2(&userVersion) } if userVersion != currentDBVersion { slog.Error(fmt.Sprintln("LocationRecorderInit Error unsupported database version", userVersion)) os.Exit(1) } } func migrateDb0To1(userVersion *int) { // this is actually create new db slog.Info("Creating location recorder database version 1..") _, err := db.Exec(`CREATE TABLE IF NOT EXISTS location ( person TEXT NOT NULL, datetime TEXT NOT NULL, latitude REAL NOT NULL, longitude REAL NOT NULL, altitude REAL, PRIMARY KEY (person, datetime))`) if err != nil { slog.Error(fmt.Sprintln("LocationRecorderInit DB0To1 Error creating table", err)) os.Exit(1) } _, err = db.Exec(`PRAGMA user_version = 1`) if err != nil { slog.Error(fmt.Sprintln("LocationRecorderInit DB0To1 Error setting user version to 1", err)) os.Exit(1) } *userVersion = 1 } func migrateDb1To2(userVersion *int) { // this will change the datetime format into Real RFC3339 slog.Info("Migrating location recorder database version 1 to 2..") dbTx, err := db.Begin() if err != nil { slog.Error(fmt.Sprintln("LocationRecorderInit DB1To2 Error beginning transaction", err)) os.Exit(1) } fail := func(err error, step string) { slog.Error(fmt.Sprintf("LocationRecorderInit DB1To2 Error %s: %s", step, err)) dbTx.Rollback() os.Exit(1) } _, err = dbTx.Exec(`ALTER TABLE location RENAME TO location_old`) if err != nil { fail(err, "renaming table") } _, err = dbTx.Exec(`CREATE TABLE IF NOT EXISTS location ( person TEXT NOT NULL, datetime TEXT NOT NULL, latitude REAL NOT NULL, longitude REAL NOT NULL, altitude REAL, PRIMARY KEY (person, datetime))`) if err != nil { fail(err, "creating new table") } row, err := dbTx.Query(`SELECT person, datetime, latitude, longitude, altitude FROM location_old`) if err != nil { fail(err, "selecting from old table") } defer row.Close() for row.Next() { var location Location err = row.Scan(&location.Person, &location.DateTime, &location.Latitude, &location.Longitude, &location.Altitude) if err != nil { fail(err, "scanning row") } dateTime, err := time.Parse("2006-01-02T15:04:05-0700", location.DateTime) if err != nil { fail(err, "parsing datetime") } _, err = dbTx.Exec(`INSERT INTO location (person, datetime, latitude, longitude, altitude) VALUES (?, ?, ?, ?, ?)`, location.Person, dateTime.UTC().Format(time.RFC3339), location.Latitude, location.Longitude, location.Altitude) if err != nil { fail(err, "inserting new row") } } _, err = dbTx.Exec(`DROP TABLE location_old`) if err != nil { fail(err, "dropping old table") } _, err = dbTx.Exec(`PRAGMA user_version = 2`) if err != nil { slog.Error(fmt.Sprintln("LocationRecorderInit Error setting user version to 2", err)) os.Exit(1) } dbTx.Commit() *userVersion = 2 }