mirror of
https://github.com/Cernobor/oko-server.git
synced 2025-02-24 08:27:17 +00:00
App version management.
* DB migration adds app_versions table for managing known app versions. * /ping reads app version from User-Agent header and responds with latest version if the app is OKO and its version is older than the latest one stored. * Added endpoint /app-versions which * lists all known versions via GET * adds/updates a version via POST #3 Fix #4
This commit is contained in:
parent
c9377b04fc
commit
7c8a430f49
6
go.mod
6
go.mod
@ -4,14 +4,15 @@ go 1.19
|
||||
|
||||
require (
|
||||
crawshaw.io/sqlite v0.3.2
|
||||
github.com/consbio/mbtileserver v0.8.1
|
||||
github.com/consbio/mbtileserver v0.8.2
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/paulmach/go.geojson v1.4.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/brendan-ward/mbtiles-go v0.0.0-20211210015813-553bc514bbdf // indirect
|
||||
github.com/brendan-ward/mbtiles-go v0.1.1-0.20220129145719-67a7dabdbaab // indirect
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
@ -22,6 +23,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mssola/user_agent v0.5.3
|
||||
github.com/ugorji/go/codec v1.2.6 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539
|
||||
|
11
go.sum
11
go.sum
@ -68,8 +68,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/brendan-ward/mbtiles-go v0.0.0-20211210015813-553bc514bbdf h1:RT/qOIiM0yuaELiwbyli7g6bHFARWlU8B27Ff1r8K6g=
|
||||
github.com/brendan-ward/mbtiles-go v0.0.0-20211210015813-553bc514bbdf/go.mod h1:rlKkU0/sjOysMB+dvVI+b90UyzmD5alQK7KlhJplVrg=
|
||||
github.com/brendan-ward/mbtiles-go v0.1.1-0.20220129145719-67a7dabdbaab h1:KC2TqbUmfkIrTwVpkH1ysJiZicPoSyb6chH3JxKHqBM=
|
||||
github.com/brendan-ward/mbtiles-go v0.1.1-0.20220129145719-67a7dabdbaab/go.mod h1:rlKkU0/sjOysMB+dvVI+b90UyzmD5alQK7KlhJplVrg=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
@ -92,8 +92,9 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/consbio/mbtileserver v0.8.1 h1:Q1x3Vf4Wbb92uNQ9ZAIn2bkS+nzMbcnH2qidq81lxOs=
|
||||
github.com/consbio/mbtileserver v0.8.1/go.mod h1:efp5wvQOhIhElvcXrFhXUw+k43dwn9EfdACvXBwHy7o=
|
||||
github.com/consbio/mbtileserver v0.8.2 h1:iXOBkCp4r/7Z7wC4sw6dYkFAJnuyVpQMu0dbT+RHSxE=
|
||||
github.com/consbio/mbtileserver v0.8.2/go.mod h1:lVBCXeAL6OzooRXWbQ/MZFGFtwm/0A1fkeNTf9T4gzA=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
@ -317,6 +318,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mssola/user_agent v0.5.3 h1:lBRPML9mdFuIZgI2cmlQ+atbpJdLdeVl2IDodjBR578=
|
||||
github.com/mssola/user_agent v0.5.3/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
geojson "github.com/paulmach/go.geojson"
|
||||
)
|
||||
|
||||
@ -40,6 +41,11 @@ type BuildInfo struct {
|
||||
|
||||
// transport objects
|
||||
|
||||
type AppVersionInfo struct {
|
||||
Version semver.Version `json:"version"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type Coords struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
|
@ -5,6 +5,7 @@ const (
|
||||
URIBuildInfo = "/build-info"
|
||||
URIHardFail = "/hard-fail"
|
||||
URISoftFail = "/soft-fail"
|
||||
URIAppVersions = "/app-versions"
|
||||
URIReinit = "/reinit"
|
||||
URIMapPack = "/mappack"
|
||||
URIHandshake = "/handshake"
|
||||
@ -16,4 +17,6 @@ const (
|
||||
URITileserverRoot = "/tileserver"
|
||||
URITileserver = URITileserverRoot + "/*x"
|
||||
URITileTemplate = URITileserverRoot + "/map/tiles/{z}/{x}/{y}.pbf"
|
||||
|
||||
AppName = "OKO"
|
||||
)
|
||||
|
@ -13,14 +13,22 @@ import (
|
||||
|
||||
"cernobor.cz/oko-server/errs"
|
||||
"cernobor.cz/oko-server/models"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mssola/user_agent"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func internalError(gc *gin.Context, err error) {
|
||||
gc.Error(err)
|
||||
gc.String(http.StatusInternalServerError, "%v", err)
|
||||
}
|
||||
|
||||
func badRequest(gc *gin.Context, err error) {
|
||||
gc.Error(err)
|
||||
gc.String(http.StatusBadRequest, "%v", err)
|
||||
}
|
||||
|
||||
func (s *Server) setupRouter() *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.New()
|
||||
@ -77,10 +85,23 @@ func (s *Server) setupRouter() *gin.Engine {
|
||||
}
|
||||
})
|
||||
|
||||
// tileserver
|
||||
router.GET(URITileserver, gin.WrapH(s.tileserverSvSet.Handler()))
|
||||
|
||||
/*** API ***/
|
||||
router.GET(URIPing, s.handleGETPing)
|
||||
router.POST(URIHandshake, s.handlePOSTHandshake)
|
||||
router.GET(URIData, s.handleGETData)
|
||||
router.POST(URIData, s.handlePOSTData)
|
||||
router.GET(URIDataPeople, s.handleGETDataPeople)
|
||||
router.GET(URIDataFeatures, s.handleGETDataFeatures)
|
||||
router.GET(URIDataFeaturesPhoto, s.handleGETDataFeaturesPhoto)
|
||||
router.GET(URIDataProposals, s.handleGETDataProposals)
|
||||
|
||||
// resources
|
||||
router.GET(URIMapPack, s.handleGETTilepack)
|
||||
|
||||
// utility/debug paths
|
||||
router.GET(URIPing, func(gc *gin.Context) {
|
||||
gc.Status(http.StatusNoContent)
|
||||
})
|
||||
router.GET(URIBuildInfo, func(gc *gin.Context) {
|
||||
gc.JSON(http.StatusOK, models.BuildInfo{
|
||||
VersionHash: s.config.VersionHash,
|
||||
@ -93,26 +114,72 @@ func (s *Server) setupRouter() *gin.Engine {
|
||||
router.GET(URISoftFail, func(gc *gin.Context) {
|
||||
gc.JSON(http.StatusOK, map[string]string{"error": "artificial fail"})
|
||||
})
|
||||
router.GET(URIAppVersions, s.handleGETAppVersions)
|
||||
router.POST(URIAppVersions, s.handlePOSTAppVersions)
|
||||
router.POST(URIReinit, s.handlePOSTReset)
|
||||
|
||||
// resources
|
||||
router.GET(URIMapPack, s.handleGETTilepack)
|
||||
|
||||
// API
|
||||
router.POST(URIHandshake, s.handlePOSTHandshake)
|
||||
router.GET(URIData, s.handleGETData)
|
||||
router.POST(URIData, s.handlePOSTData)
|
||||
router.GET(URIDataPeople, s.handleGETDataPeople)
|
||||
router.GET(URIDataFeatures, s.handleGETDataFeatures)
|
||||
router.GET(URIDataFeaturesPhoto, s.handleGETDataFeaturesPhoto)
|
||||
router.GET(URIDataProposals, s.handleGETDataProposals)
|
||||
|
||||
// tileserver
|
||||
router.GET(URITileserver, gin.WrapH(s.tileserverSvSet.Handler()))
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func extractAppVersion(gc *gin.Context) (*semver.Version, error) {
|
||||
ua := user_agent.New(gc.Request.UserAgent())
|
||||
n, v := ua.Browser()
|
||||
if n != AppName {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
version, err := semver.NewVersion(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed version in User-Agent header: %w", err)
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleGETPing(gc *gin.Context) {
|
||||
version, err := extractAppVersion(gc)
|
||||
if err != nil {
|
||||
badRequest(gc, err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := s.getLatestVersion(version)
|
||||
if err != nil {
|
||||
internalError(gc, err)
|
||||
return
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
gc.Status(http.StatusNoContent)
|
||||
} else {
|
||||
gc.JSON(http.StatusOK, res)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleGETAppVersions(gc *gin.Context) {
|
||||
versions, err := s.getAppVersions()
|
||||
if err != nil {
|
||||
internalError(gc, err)
|
||||
return
|
||||
}
|
||||
gc.JSON(http.StatusOK, versions)
|
||||
}
|
||||
|
||||
func (s *Server) handlePOSTAppVersions(gc *gin.Context) {
|
||||
var versionInfo models.AppVersionInfo
|
||||
err := gc.ShouldBindJSON(&versionInfo)
|
||||
if err != nil {
|
||||
badRequest(gc, fmt.Errorf("malformed version info: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = s.putAppVersion(&versionInfo)
|
||||
if err != nil {
|
||||
internalError(gc, err)
|
||||
return
|
||||
}
|
||||
gc.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handlePOSTReset(gc *gin.Context) {
|
||||
err := s.initDB(true)
|
||||
if err != nil {
|
||||
@ -130,7 +197,7 @@ func (s *Server) handlePOSTHandshake(gc *gin.Context) {
|
||||
var hs models.HandshakeChallenge
|
||||
err := gc.ShouldBindJSON(&hs)
|
||||
if err != nil {
|
||||
gc.String(http.StatusBadRequest, fmt.Sprintf("malformed handshake challenge: %v", err))
|
||||
badRequest(gc, fmt.Errorf("malformed handshake challenge: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -205,7 +272,7 @@ func (s *Server) handlePOSTData(gc *gin.Context) {
|
||||
case "multipart/form-data":
|
||||
s.handlePOSTDataMultipart(gc)
|
||||
default:
|
||||
gc.String(http.StatusBadRequest, "unsupported Content-Type")
|
||||
badRequest(gc, fmt.Errorf("unsupported Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,17 +280,17 @@ func (s *Server) handlePOSTDataJSON(gc *gin.Context) {
|
||||
var data models.Update
|
||||
err := gc.ShouldBindJSON(&data)
|
||||
if err != nil {
|
||||
gc.String(http.StatusBadRequest, fmt.Sprintf("malformed data: %v", err))
|
||||
badRequest(gc, fmt.Errorf("malformed data: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !isUniqueFeatureID(data.Create) {
|
||||
gc.String(http.StatusBadRequest, "created features do not have unique IDs")
|
||||
badRequest(gc, fmt.Errorf("created features do not have unique IDs"))
|
||||
return
|
||||
}
|
||||
|
||||
if data.CreatedPhotos != nil || data.AddPhotos != nil {
|
||||
gc.String(http.StatusBadRequest, "created_photos and/or add_photos present, but Content-Type is application/json")
|
||||
badRequest(gc, fmt.Errorf("created_photos and/or add_photos present, but Content-Type is application/json"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -238,7 +305,7 @@ func (s *Server) handlePOSTDataJSON(gc *gin.Context) {
|
||||
func (s *Server) handlePOSTDataMultipart(gc *gin.Context) {
|
||||
form, err := gc.MultipartForm()
|
||||
if err != nil {
|
||||
gc.String(http.StatusBadRequest, "malformed multipart/form-data content")
|
||||
badRequest(gc, fmt.Errorf("malformed multipart/form-data content"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -246,11 +313,11 @@ func (s *Server) handlePOSTDataMultipart(gc *gin.Context) {
|
||||
if !ok {
|
||||
dataFile, ok := form.File["data"]
|
||||
if !ok {
|
||||
gc.String(http.StatusBadRequest, "value 'data' is missing from the content")
|
||||
badRequest(gc, fmt.Errorf("value 'data' is missing from the content"))
|
||||
return
|
||||
}
|
||||
if len(dataFile) != 1 {
|
||||
gc.String(http.StatusBadRequest, "value 'data' does not contain exactly 1 item")
|
||||
badRequest(gc, fmt.Errorf("value 'data' does not contain exactly 1 item"))
|
||||
return
|
||||
}
|
||||
df, err := dataFile[0].Open()
|
||||
@ -266,26 +333,26 @@ func (s *Server) handlePOSTDataMultipart(gc *gin.Context) {
|
||||
dataStr = []string{string(dataBytes)}
|
||||
}
|
||||
if len(dataStr) != 1 {
|
||||
gc.String(http.StatusBadRequest, "value 'data' does not contain exactly 1 item")
|
||||
badRequest(gc, fmt.Errorf("value 'data' does not contain exactly 1 item"))
|
||||
return
|
||||
}
|
||||
|
||||
var data models.Update
|
||||
err = json.Unmarshal([]byte(dataStr[0]), &data)
|
||||
if err != nil {
|
||||
gc.String(http.StatusBadRequest, "malformed 'data' value: %v", err)
|
||||
badRequest(gc, fmt.Errorf("malformed 'data' value: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !isUniqueFeatureID(data.Create) {
|
||||
gc.String(http.StatusBadRequest, "created features do not have unique IDs")
|
||||
badRequest(gc, fmt.Errorf("created features do not have unique IDs"))
|
||||
return
|
||||
}
|
||||
|
||||
photos := make(map[string]models.Photo, len(form.File))
|
||||
for name, fh := range form.File {
|
||||
if len(fh) != 1 {
|
||||
gc.String(http.StatusBadRequest, "file item %s does not contain exactly 1 file", name)
|
||||
badRequest(gc, fmt.Errorf("file item %s does not contain exactly 1 file", name))
|
||||
return
|
||||
}
|
||||
var photo models.Photo
|
||||
@ -303,7 +370,7 @@ func (s *Server) handlePOSTDataMultipart(gc *gin.Context) {
|
||||
if err != nil {
|
||||
var e *errs.ErrUnsupportedContentType
|
||||
if errors.As(err, &e) {
|
||||
gc.String(http.StatusBadRequest, e.Error())
|
||||
badRequest(gc, e)
|
||||
return
|
||||
}
|
||||
internalError(gc, fmt.Errorf("failed to update data: %w", err))
|
||||
@ -334,12 +401,12 @@ func (s *Server) handleGETDataFeatures(gc *gin.Context) {
|
||||
func (s *Server) handleGETDataFeaturesPhoto(gc *gin.Context) {
|
||||
reqFeatureID, err := strconv.Atoi(gc.Param("feature"))
|
||||
if err != nil {
|
||||
gc.String(http.StatusBadRequest, "malformed feature ID")
|
||||
badRequest(gc, fmt.Errorf("malformed feature ID"))
|
||||
return
|
||||
}
|
||||
reqPhotoID, err := strconv.Atoi(gc.Param("photo"))
|
||||
if err != nil {
|
||||
gc.String(http.StatusBadRequest, "malformed photo ID")
|
||||
badRequest(gc, fmt.Errorf("malformed photo ID"))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,10 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"cernobor.cz/oko-server/errs"
|
||||
@ -18,6 +22,7 @@ import (
|
||||
"crawshaw.io/sqlite"
|
||||
"crawshaw.io/sqlite/sqlitex"
|
||||
mbsh "github.com/consbio/mbtileserver/handlers"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
geojson "github.com/paulmach/go.geojson"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -346,9 +351,9 @@ func (s *Server) migrateDb(conn *sqlite.Conn) error {
|
||||
}
|
||||
|
||||
err = sqlitex.Exec(conn, fmt.Sprintf("PRAGMA user_version = %d", migration.version), nil)
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set user_version in db: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = sqlitex.Exec(conn, "PRAGMA user_version", func(stmt *sqlite.Stmt) error {
|
||||
version = stmt.ColumnInt(0)
|
||||
@ -356,7 +361,7 @@ func (s *Server) migrateDb(conn *sqlite.Conn) error {
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user_version: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.log.Infof("Migrated db to version: %d", version)
|
||||
return nil
|
||||
@ -366,6 +371,61 @@ func (s *Server) migrateDb(conn *sqlite.Conn) error {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) getLatestVersion(v *semver.Version) (*models.AppVersionInfo, error) {
|
||||
conn := s.getDbConn()
|
||||
defer s.dbpool.Put(conn)
|
||||
|
||||
var latest *models.AppVersionInfo
|
||||
versions, err := s.getAppVersions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve app versions from db: %w", err)
|
||||
}
|
||||
|
||||
for _, ver := range versions {
|
||||
if (v == nil || v.LessThan(ver.Version)) && (latest == nil || latest.Version.LessThan(ver.Version)) {
|
||||
latest = ver
|
||||
}
|
||||
}
|
||||
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func (s *Server) getAppVersions() ([]*models.AppVersionInfo, error) {
|
||||
conn := s.getDbConn()
|
||||
defer s.dbpool.Put(conn)
|
||||
|
||||
versions := []*models.AppVersionInfo{}
|
||||
err := sqlitex.Exec(conn, "select version, address from app_versions", func(stmt *sqlite.Stmt) error {
|
||||
verStr := stmt.ColumnText(0)
|
||||
addr := stmt.ColumnText(1)
|
||||
|
||||
ver, err := semver.NewVersion(verStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse version: %w", err)
|
||||
}
|
||||
|
||||
versions = append(versions, &models.AppVersionInfo{
|
||||
Version: *ver,
|
||||
Address: addr,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to insert/retrieve user from db: %w", err)
|
||||
}
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (s *Server) putAppVersion(versionInfo *models.AppVersionInfo) error {
|
||||
conn := s.getDbConn()
|
||||
defer s.dbpool.Put(conn)
|
||||
|
||||
err := sqlitex.Exec(conn, "insert into app_versions(version, address) values(?, ?) on conflict(version) do update set address = excluded.address", nil, versionInfo.Version.String(), versionInfo.Address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert app version into db: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
4
server/sql_schema/V3_app_versions.sql
Normal file
4
server/sql_schema/V3_app_versions.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE app_versions (
|
||||
version text NOT NULL PRIMARY KEY,
|
||||
address text NOT NULL
|
||||
);
|
Loading…
Reference in New Issue
Block a user