mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-04-05 00:43:45 -04:00
324 lines
9.8 KiB
Go
324 lines
9.8 KiB
Go
package pb
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
|
|
"github.com/pocketbase/pocketbase"
|
|
"github.com/pocketbase/pocketbase/apis"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/pocketbase/pocketbase/plugins/ghupdate"
|
|
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
|
"github.com/robfig/cron/v3"
|
|
"github.com/seriousm4x/upsnap/cronjobs"
|
|
"github.com/seriousm4x/upsnap/logger"
|
|
_ "github.com/seriousm4x/upsnap/migrations"
|
|
)
|
|
|
|
var Version = "(untracked)"
|
|
|
|
func StartPocketBase(distDirFS fs.FS) {
|
|
// set data dir
|
|
// use "./pb_data" if it's in the same dir as upsnap binary
|
|
// else use os.UserConfigDir() / upsnap
|
|
var dataDir string
|
|
baseDir, err := os.Getwd()
|
|
if err != nil {
|
|
logger.Error.Fatalln(err)
|
|
}
|
|
pb_data := path.Join(baseDir, "pb_data")
|
|
if _, err = os.Stat(pb_data); err == nil {
|
|
dataDir = pb_data
|
|
} else if os.IsNotExist(err) {
|
|
userConfigDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
logger.Error.Fatalln(err)
|
|
}
|
|
upsnap_data := path.Join(userConfigDir, "upsnap")
|
|
if _, err = os.Stat(upsnap_data); err == nil {
|
|
dataDir = upsnap_data
|
|
} else if os.IsNotExist(err) {
|
|
if err := os.MkdirAll(upsnap_data, 0700); err != nil {
|
|
logger.Error.Fatalln(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// create app
|
|
app := pocketbase.NewWithConfig(pocketbase.Config{
|
|
DefaultDataDir: dataDir,
|
|
})
|
|
app.RootCmd.Short = "UpSnap CLI"
|
|
app.RootCmd.Version = Version
|
|
|
|
// auto migrate db
|
|
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
|
|
Automigrate: true,
|
|
})
|
|
|
|
// GitHub selfupdate
|
|
ghupdate.MustRegister(app, app.RootCmd, ghupdate.Config{
|
|
Owner: "seriousm4x",
|
|
Repo: "UpSnap",
|
|
ArchiveExecutable: "upsnap",
|
|
})
|
|
|
|
// event hooks
|
|
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
se.Router.GET("/{path...}", apis.Static(distDirFS, true))
|
|
|
|
se.Router.GET("/api/upsnap/wake/{id}", HandlerWake).Bind(RequireUpSnapPermission())
|
|
se.Router.GET("/api/upsnap/wakegroup/{id}", HandlerWakeGroup).Bind(RequireUpSnapPermission())
|
|
se.Router.GET("/api/upsnap/sleep/{id}", HandlerSleep).Bind(RequireUpSnapPermission())
|
|
se.Router.GET("/api/upsnap/reboot/{id}", HandlerReboot).Bind(RequireUpSnapPermission())
|
|
se.Router.GET("/api/upsnap/shutdown/{id}", HandlerShutdown).Bind(RequireUpSnapPermission())
|
|
se.Router.GET("/api/upsnap/scan", HandlerScan).Bind(apis.RequireSuperuserAuth())
|
|
se.Router.POST("/api/upsnap/init-superuser", HandlerInitSuperuser) // https://github.com/pocketbase/pocketbase/discussions/6198
|
|
se.Router.POST("/api/upsnap/validate-cron", HandlerValidateCron)
|
|
se.Router.GET("/api/upsnap/manifest.webmanifest", HandlerWebsiteManifest)
|
|
|
|
if err := importSettings(app); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := resetDeviceStates(app); err != nil {
|
|
return err
|
|
}
|
|
|
|
cronjobs.SetPingJobs(app)
|
|
cronjobs.StartPing()
|
|
cronjobs.SetWakeShutdownJobs(app)
|
|
cronjobs.StartWakeShutdown()
|
|
|
|
// restart ping cronjobs or wake/shutdown cronjobs on model update
|
|
// add event hook before starting server.
|
|
// using this outside App.OnBeforeServe() would not work
|
|
app.OnModelAfterUpdateSuccess("settings_private", "devices").BindFunc(func(e *core.ModelEvent) error {
|
|
if e.Model.TableName() == "settings_private" {
|
|
cronjobs.SetPingJobs(app)
|
|
} else if e.Model.TableName() == "devices" {
|
|
// only restart wake/shutdown cronjobs if new model's cron changed
|
|
record := e.Model.(*core.Record)
|
|
newRecord := record.Fresh()
|
|
oldRecord := record.Original()
|
|
|
|
newWakeCron := newRecord.GetString("wake_cron")
|
|
newWakeCmd := newRecord.GetString("wake_cmd")
|
|
newWakeCronEnabled := newRecord.GetBool("wake_cron_enabled")
|
|
newShutdownCron := newRecord.GetString("shutdown_cron")
|
|
newShutdownCmd := newRecord.GetString("shutdown_cmd")
|
|
newShutdownCronEnabled := newRecord.GetBool("shutdown_cron_enabled")
|
|
|
|
oldWakeCron := oldRecord.GetString("wake_cron")
|
|
oldWakeCmd := oldRecord.GetString("wake_cmd")
|
|
oldWakeCronEnabled := oldRecord.GetBool("wake_cron_enabled")
|
|
oldShutdownCron := oldRecord.GetString("shutdown_cron")
|
|
oldShutdownCmd := oldRecord.GetString("shutdown_cmd")
|
|
oldShutdownCronEnabled := oldRecord.GetBool("shutdown_cron_enabled")
|
|
|
|
if newWakeCron != oldWakeCron ||
|
|
newWakeCmd != oldWakeCmd ||
|
|
newWakeCronEnabled != oldWakeCronEnabled ||
|
|
newShutdownCron != oldShutdownCron ||
|
|
newShutdownCronEnabled != oldShutdownCronEnabled ||
|
|
newShutdownCmd != oldShutdownCmd {
|
|
cronjobs.SetWakeShutdownJobs(app)
|
|
}
|
|
}
|
|
return e.Next()
|
|
})
|
|
return se.Next()
|
|
})
|
|
|
|
app.OnModelAfterCreateSuccess().BindFunc(func(e *core.ModelEvent) error {
|
|
if e.Model.TableName() == "_superusers" {
|
|
if err := setSetupCompleted(e.App); err != nil {
|
|
logger.Error.Println(err)
|
|
return err
|
|
}
|
|
return e.Next()
|
|
} else if e.Model.TableName() == "devices" {
|
|
// when a device is created, give the user all rights to the device he just created
|
|
deviceRec := e.Model.(*core.Record)
|
|
userId := deviceRec.GetString("created_by")
|
|
|
|
var permissionRec *core.Record
|
|
permissionRec, err := app.FindFirstRecordByFilter("permissions",
|
|
fmt.Sprintf("user.id = '%s'", userId))
|
|
if err != nil && err.Error() != "sql: no rows in result set" {
|
|
logger.Error.Println(err)
|
|
return err
|
|
} else if permissionRec != nil {
|
|
permissionRec.Set("read", append(permissionRec.GetStringSlice("read"), deviceRec.Id))
|
|
permissionRec.Set("update", append(permissionRec.GetStringSlice("update"), deviceRec.Id))
|
|
permissionRec.Set("delete", append(permissionRec.GetStringSlice("delete"), deviceRec.Id))
|
|
permissionRec.Set("power", append(permissionRec.GetStringSlice("power"), deviceRec.Id))
|
|
if err := app.Save(permissionRec); err != nil {
|
|
logger.Error.Println(err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return e.Next()
|
|
})
|
|
|
|
app.OnModelAfterDeleteSuccess().BindFunc(func(e *core.ModelEvent) error {
|
|
if e.Model.TableName() == "_superusers" {
|
|
if err := setSetupCompleted(e.App); err != nil {
|
|
logger.Error.Println(err)
|
|
return err
|
|
}
|
|
}
|
|
return e.Next()
|
|
})
|
|
|
|
// prevent new superuser bahavior introduced in pocketbase 0.23
|
|
app.OnRecordCreate(core.CollectionNameSuperusers).BindFunc(func(e *core.RecordEvent) error {
|
|
if e.Record.Email() == core.DefaultInstallerEmail {
|
|
return errors.New("skip default PocketBase installer")
|
|
}
|
|
return e.Next()
|
|
})
|
|
|
|
app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {
|
|
cronjobs.StopAll()
|
|
return e.Next()
|
|
})
|
|
|
|
// check for custom http listen address in env var, else use default
|
|
httpListen := os.Getenv("UPSNAP_HTTP_LISTEN")
|
|
if httpListen != "" {
|
|
if err := app.Bootstrap(); err != nil {
|
|
logger.Error.Fatalln(err)
|
|
}
|
|
if err := apis.Serve(app, apis.ServeConfig{
|
|
HttpAddr: httpListen,
|
|
ShowStartBanner: true,
|
|
}); err != nil {
|
|
logger.Error.Fatalln(err)
|
|
}
|
|
} else {
|
|
if err := app.Start(); err != nil {
|
|
logger.Error.Fatalln(err)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func importSettings(app *pocketbase.PocketBase) error {
|
|
settingsPrivateRecords, err := app.FindAllRecords("settings_private")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settingsPrivateCollection, err := app.FindCollectionByNameOrId("settings_private")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settingsPrivate := core.NewRecord(settingsPrivateCollection)
|
|
if len(settingsPrivateRecords) > 0 {
|
|
settingsPrivate = settingsPrivateRecords[0]
|
|
}
|
|
|
|
settingsPublicRecords, err := app.FindAllRecords("settings_public")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settingsPublicCollection, err := app.FindCollectionByNameOrId("settings_public")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settingsPublic := core.NewRecord(settingsPublicCollection)
|
|
if len(settingsPublicRecords) > 0 {
|
|
settingsPublic = settingsPublicRecords[0]
|
|
}
|
|
|
|
// set ping interval settings. priority:
|
|
// 1st: env var
|
|
// 2nd: database entry
|
|
// 3rd: default values
|
|
defaultInterval := "*/3 * * * * *"
|
|
interval := defaultInterval
|
|
if settingsPrivate.GetString("interval") != "" {
|
|
interval = settingsPrivate.GetString("interval")
|
|
}
|
|
if os.Getenv("UPSNAP_INTERVAL") != "" {
|
|
interval = os.Getenv("UPSNAP_INTERVAL")
|
|
}
|
|
|
|
// validate interval before saving
|
|
p := cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
|
if _, err := p.Parse(interval); err != nil {
|
|
if interval == "*/3 * * * *" || interval == "@every 3s" {
|
|
settingsPrivate.Set("interval", defaultInterval)
|
|
if e := app.Save(settingsPrivate); e != nil {
|
|
logger.Error.Println(e)
|
|
}
|
|
} else {
|
|
logger.Error.Printf("'%s' ping interval is not valid.\n", interval)
|
|
logger.Error.Println("Please go to '/settings/' and change your ping interval.")
|
|
logger.Error.Println("Falling back to default interval: " + defaultInterval)
|
|
}
|
|
interval = defaultInterval
|
|
}
|
|
|
|
settingsPrivate.Set("interval", interval)
|
|
if scanRange := os.Getenv("UPSNAP_SCAN_RANGE"); scanRange != "" {
|
|
settingsPrivate.Set("scan_range", scanRange)
|
|
}
|
|
|
|
if websiteTitle := os.Getenv("UPSNAP_WEBSITE_TITLE"); websiteTitle != "" {
|
|
settingsPublic.Set("website_title", websiteTitle)
|
|
}
|
|
|
|
if err := app.Save(settingsPrivate); err != nil {
|
|
return err
|
|
}
|
|
if err := app.Save(settingsPublic); err != nil {
|
|
return err
|
|
}
|
|
if err := setSetupCompleted(app); err != nil {
|
|
logger.Error.Println(err)
|
|
return err
|
|
}
|
|
|
|
logger.Info.Println("Ping interval set to", interval)
|
|
return nil
|
|
}
|
|
|
|
func resetDeviceStates(app *pocketbase.PocketBase) error {
|
|
devices, err := app.FindAllRecords("devices")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, device := range devices {
|
|
device.Set("status", "offline")
|
|
if err := app.Save(device); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setSetupCompleted(app core.App) error {
|
|
totalAdmins, err := app.CountRecords(core.CollectionNameSuperusers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settingsPublicRecords, err := app.FindAllRecords("settings_public")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if totalAdmins > 0 {
|
|
settingsPublicRecords[0].Set("setup_completed", true)
|
|
} else {
|
|
settingsPublicRecords[0].Set("setup_completed", false)
|
|
}
|
|
if err := app.Save(settingsPublicRecords[0]); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|