mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat: show new updates in dashboard (#690)
* feat: show new update banner * feat(http): add request logger * refactor: updates checker * feat: make update check optional * fix: empty releases * add toggle switch for update checks * feat: toggle updates check from settings * feat: toggle updates check from settings * feat: check on toggle enabled --------- Co-authored-by: soup <soup@r4tio.dev>
This commit is contained in:
parent
3fdd7cf5e4
commit
2917a7d42d
24 changed files with 687 additions and 121 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -32,6 +32,7 @@ web/build
|
||||||
bin/
|
bin/
|
||||||
log/
|
log/
|
||||||
dist/
|
dist/
|
||||||
|
.run/
|
||||||
# If needed, package-lock.json shall be added
|
# If needed, package-lock.json shall be added
|
||||||
# manually using an explicit git add command.
|
# manually using an explicit git add command.
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -3,7 +3,7 @@
|
||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
|
|
||||||
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
|
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
|
||||||
GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
|
GIT_TAG := $(shell git describe --abbrev=0 --tags)
|
||||||
|
|
||||||
SERVICE = autobrr
|
SERVICE = autobrr
|
||||||
GO = go
|
GO = go
|
||||||
|
|
|
@ -4,10 +4,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
_ "time/tzdata"
|
||||||
"github.com/asaskevich/EventBus"
|
|
||||||
"github.com/r3labs/sse/v2"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/action"
|
"github.com/autobrr/autobrr/internal/action"
|
||||||
"github.com/autobrr/autobrr/internal/api"
|
"github.com/autobrr/autobrr/internal/api"
|
||||||
|
@ -26,9 +23,12 @@ import (
|
||||||
"github.com/autobrr/autobrr/internal/release"
|
"github.com/autobrr/autobrr/internal/release"
|
||||||
"github.com/autobrr/autobrr/internal/scheduler"
|
"github.com/autobrr/autobrr/internal/scheduler"
|
||||||
"github.com/autobrr/autobrr/internal/server"
|
"github.com/autobrr/autobrr/internal/server"
|
||||||
|
"github.com/autobrr/autobrr/internal/update"
|
||||||
"github.com/autobrr/autobrr/internal/user"
|
"github.com/autobrr/autobrr/internal/user"
|
||||||
|
|
||||||
_ "time/tzdata"
|
"github.com/asaskevich/EventBus"
|
||||||
|
"github.com/r3labs/sse/v2"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -69,11 +69,11 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msgf("Starting autobrr")
|
log.Info().Msgf("Starting autobrr")
|
||||||
log.Info().Msgf("Version: %v", version)
|
log.Info().Msgf("Version: %s", version)
|
||||||
log.Info().Msgf("Commit: %v", commit)
|
log.Info().Msgf("Commit: %s", commit)
|
||||||
log.Info().Msgf("Build date: %v", date)
|
log.Info().Msgf("Build date: %s", date)
|
||||||
log.Info().Msgf("Log-level: %v", cfg.Config.LogLevel)
|
log.Info().Msgf("Log-level: %s", cfg.Config.LogLevel)
|
||||||
log.Info().Msgf("Using database: %v", db.Driver)
|
log.Info().Msgf("Using database: %s", db.Driver)
|
||||||
|
|
||||||
// setup repos
|
// setup repos
|
||||||
var (
|
var (
|
||||||
|
@ -94,7 +94,8 @@ func main() {
|
||||||
var (
|
var (
|
||||||
apiService = api.NewService(log, apikeyRepo)
|
apiService = api.NewService(log, apikeyRepo)
|
||||||
notificationService = notification.NewService(log, notificationRepo)
|
notificationService = notification.NewService(log, notificationRepo)
|
||||||
schedulingService = scheduler.NewService(log, version, notificationService)
|
updateService = update.NewUpdate(log, cfg.Config)
|
||||||
|
schedulingService = scheduler.NewService(log, cfg.Config, notificationService, updateService)
|
||||||
indexerAPIService = indexer.NewAPIService(log)
|
indexerAPIService = indexer.NewAPIService(log)
|
||||||
userService = user.NewService(userRepo)
|
userService = user.NewService(userRepo)
|
||||||
authService = auth.NewService(log, userService)
|
authService = auth.NewService(log, userService)
|
||||||
|
@ -115,7 +116,7 @@ func main() {
|
||||||
go func() {
|
go func() {
|
||||||
httpServer := http.NewServer(
|
httpServer := http.NewServer(
|
||||||
log,
|
log,
|
||||||
cfg.Config,
|
cfg,
|
||||||
serverEvents,
|
serverEvents,
|
||||||
db,
|
db,
|
||||||
version,
|
version,
|
||||||
|
@ -131,17 +132,15 @@ func main() {
|
||||||
ircService,
|
ircService,
|
||||||
notificationService,
|
notificationService,
|
||||||
releaseService,
|
releaseService,
|
||||||
|
updateService,
|
||||||
)
|
)
|
||||||
errorChannel <- httpServer.Open()
|
errorChannel <- httpServer.Open()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
srv := server.NewServer(log, ircService, indexerService, feedService, schedulingService)
|
|
||||||
srv.Hostname = cfg.Config.Host
|
|
||||||
srv.Port = cfg.Config.Port
|
|
||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM)
|
||||||
|
|
||||||
|
srv := server.NewServer(log, cfg.Config, ircService, indexerService, feedService, schedulingService, updateService)
|
||||||
if err := srv.Start(); err != nil {
|
if err := srv.Start(); err != nil {
|
||||||
log.Fatal().Stack().Err(err).Msg("could not start server")
|
log.Fatal().Stack().Err(err).Msg("could not start server")
|
||||||
return
|
return
|
||||||
|
|
|
@ -51,6 +51,12 @@ logLevel = "TRACE"
|
||||||
#
|
#
|
||||||
#logMaxBackups = 3
|
#logMaxBackups = 3
|
||||||
|
|
||||||
|
# Check for updates
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
#
|
||||||
|
checkForUpdates = true
|
||||||
|
|
||||||
# Session secret
|
# Session secret
|
||||||
#
|
#
|
||||||
sessionSecret = "secret-session-key"
|
sessionSecret = "secret-session-key"
|
||||||
|
|
|
@ -2,6 +2,7 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -72,6 +73,10 @@ logLevel = "DEBUG"
|
||||||
#
|
#
|
||||||
#logMaxBackups = 3
|
#logMaxBackups = 3
|
||||||
|
|
||||||
|
# Check for updates
|
||||||
|
#
|
||||||
|
checkForUpdates = true
|
||||||
|
|
||||||
# Session secret
|
# Session secret
|
||||||
#
|
#
|
||||||
sessionSecret = "{{ .sessionSecret }}"
|
sessionSecret = "{{ .sessionSecret }}"
|
||||||
|
@ -148,6 +153,7 @@ func writeConfig(configPath string, configFile string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config interface {
|
type Config interface {
|
||||||
|
UpdateConfig() error
|
||||||
DynamicReload(log logger.Logger)
|
DynamicReload(log logger.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,6 +185,7 @@ func (c *AppConfig) defaults() {
|
||||||
BaseURL: "/",
|
BaseURL: "/",
|
||||||
SessionSecret: "secret-session-key",
|
SessionSecret: "secret-session-key",
|
||||||
CustomDefinitions: "",
|
CustomDefinitions: "",
|
||||||
|
CheckForUpdates: true,
|
||||||
DatabaseType: "sqlite",
|
DatabaseType: "sqlite",
|
||||||
PostgresHost: "",
|
PostgresHost: "",
|
||||||
PostgresPort: 0,
|
PostgresPort: 0,
|
||||||
|
@ -240,6 +247,9 @@ func (c *AppConfig) DynamicReload(log logger.Logger) {
|
||||||
logPath := viper.GetString("logPath")
|
logPath := viper.GetString("logPath")
|
||||||
c.Config.LogPath = logPath
|
c.Config.LogPath = logPath
|
||||||
|
|
||||||
|
checkUpdates := viper.GetBool("checkForUpdates")
|
||||||
|
c.Config.CheckForUpdates = checkUpdates
|
||||||
|
|
||||||
log.Debug().Msg("config file reloaded!")
|
log.Debug().Msg("config file reloaded!")
|
||||||
|
|
||||||
c.m.Unlock()
|
c.m.Unlock()
|
||||||
|
@ -248,3 +258,46 @@ func (c *AppConfig) DynamicReload(log logger.Logger) {
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *AppConfig) UpdateConfig() error {
|
||||||
|
file := path.Join(c.Config.ConfigPath, "config.toml")
|
||||||
|
|
||||||
|
f, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not read config file: %s", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(f), "\n")
|
||||||
|
lines = c.processLines(lines)
|
||||||
|
|
||||||
|
output := strings.Join(lines, "\n")
|
||||||
|
if err := os.WriteFile(file, []byte(output), 0644); err != nil {
|
||||||
|
return errors.Wrap(err, "could not write config file: %s", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppConfig) processLines(lines []string) []string {
|
||||||
|
// keep track of not found values to append at bottom
|
||||||
|
var (
|
||||||
|
foundLineUpdate = false
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
// set checkForUpdates
|
||||||
|
if !foundLineUpdate && strings.Contains(line, "checkForUpdates =") {
|
||||||
|
lines[i] = fmt.Sprintf("checkForUpdates = %t", c.Config.CheckForUpdates)
|
||||||
|
foundLineUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append missing vars to bottom
|
||||||
|
if !foundLineUpdate {
|
||||||
|
lines = append(lines, "# Check for updates")
|
||||||
|
lines = append(lines, "#")
|
||||||
|
lines = append(lines, fmt.Sprintf("checkForUpdates = %t", c.Config.CheckForUpdates))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
55
internal/config/config_test.go
Normal file
55
internal/config/config_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppConfig_processLines(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Config *domain.Config
|
||||||
|
m sync.Mutex
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
lines []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "append missing",
|
||||||
|
fields: fields{
|
||||||
|
Config: &domain.Config{CheckForUpdates: true},
|
||||||
|
m: sync.Mutex{},
|
||||||
|
},
|
||||||
|
args: args{[]string{}},
|
||||||
|
want: []string{"# Check for updates", "#", "checkForUpdates = true"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update existing",
|
||||||
|
fields: fields{
|
||||||
|
Config: &domain.Config{CheckForUpdates: true},
|
||||||
|
m: sync.Mutex{},
|
||||||
|
},
|
||||||
|
args: args{[]string{"# Check for updates", "#", "#checkForUpdates = false"}},
|
||||||
|
want: []string{"# Check for updates", "#", "checkForUpdates = true"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &AppConfig{
|
||||||
|
Config: tt.fields.Config,
|
||||||
|
m: tt.fields.m,
|
||||||
|
}
|
||||||
|
if got := c.processLines(tt.args.lines); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("processLines() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ type Config struct {
|
||||||
BaseURL string `toml:"baseUrl"`
|
BaseURL string `toml:"baseUrl"`
|
||||||
SessionSecret string `toml:"sessionSecret"`
|
SessionSecret string `toml:"sessionSecret"`
|
||||||
CustomDefinitions string `toml:"customDefinitions"`
|
CustomDefinitions string `toml:"customDefinitions"`
|
||||||
|
CheckForUpdates bool `toml:"checkForUpdates"`
|
||||||
DatabaseType string `toml:"databaseType"`
|
DatabaseType string `toml:"databaseType"`
|
||||||
PostgresHost string `toml:"postgresHost"`
|
PostgresHost string `toml:"postgresHost"`
|
||||||
PostgresPort int `toml:"postgresPort"`
|
PostgresPort int `toml:"postgresPort"`
|
||||||
|
@ -19,3 +20,12 @@ type Config struct {
|
||||||
PostgresUser string `toml:"postgresUser"`
|
PostgresUser string `toml:"postgresUser"`
|
||||||
PostgresPass string `toml:"postgresPass"`
|
PostgresPass string `toml:"postgresPass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigUpdate struct {
|
||||||
|
Host *string `json:"host,omitempty"`
|
||||||
|
Port *int `json:"port,omitempty"`
|
||||||
|
LogLevel *string `json:"log_level,omitempty"`
|
||||||
|
LogPath *string `json:"log_path,omitempty"`
|
||||||
|
BaseURL *string `json:"base_url,omitempty"`
|
||||||
|
CheckForUpdates *bool `json:"check_for_updates,omitempty"`
|
||||||
|
}
|
||||||
|
|
|
@ -1,52 +1,84 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/config"
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
type configJson struct {
|
type configJson struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
LogLevel string `json:"log_level"`
|
LogLevel string `json:"log_level"`
|
||||||
LogPath string `json:"log_path"`
|
LogPath string `json:"log_path"`
|
||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url"`
|
||||||
Version string `json:"version"`
|
CheckForUpdates bool `json:"check_for_updates"`
|
||||||
Commit string `json:"commit"`
|
Version string `json:"version"`
|
||||||
Date string `json:"date"`
|
Commit string `json:"commit"`
|
||||||
|
Date string `json:"date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type configHandler struct {
|
type configHandler struct {
|
||||||
encoder encoder
|
encoder encoder
|
||||||
|
|
||||||
|
cfg *config.AppConfig
|
||||||
server Server
|
server Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfigHandler(encoder encoder, server Server) *configHandler {
|
func newConfigHandler(encoder encoder, server Server, cfg *config.AppConfig) *configHandler {
|
||||||
return &configHandler{
|
return &configHandler{
|
||||||
encoder: encoder,
|
encoder: encoder,
|
||||||
|
cfg: cfg,
|
||||||
server: server,
|
server: server,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h configHandler) Routes(r chi.Router) {
|
func (h configHandler) Routes(r chi.Router) {
|
||||||
r.Get("/", h.getConfig)
|
r.Get("/", h.getConfig)
|
||||||
|
r.Patch("/", h.updateConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h configHandler) getConfig(w http.ResponseWriter, r *http.Request) {
|
func (h configHandler) getConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
conf := configJson{
|
conf := configJson{
|
||||||
Host: h.server.config.Host,
|
Host: h.cfg.Config.Host,
|
||||||
Port: h.server.config.Port,
|
Port: h.cfg.Config.Port,
|
||||||
LogLevel: h.server.config.LogLevel,
|
LogLevel: h.cfg.Config.LogLevel,
|
||||||
LogPath: h.server.config.LogPath,
|
LogPath: h.cfg.Config.LogPath,
|
||||||
BaseURL: h.server.config.BaseURL,
|
BaseURL: h.cfg.Config.BaseURL,
|
||||||
Version: h.server.version,
|
CheckForUpdates: h.cfg.Config.CheckForUpdates,
|
||||||
Commit: h.server.commit,
|
Version: h.server.version,
|
||||||
Date: h.server.date,
|
Commit: h.server.commit,
|
||||||
|
Date: h.server.date,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.encoder.StatusResponse(ctx, w, conf, http.StatusOK)
|
render.JSON(w, r, conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h configHandler) updateConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data domain.ConfigUpdate
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
h.encoder.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.CheckForUpdates != nil {
|
||||||
|
h.cfg.Config.CheckForUpdates = *data.CheckForUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.cfg.UpdateConfig(); err != nil {
|
||||||
|
render.Status(r, http.StatusInternalServerError)
|
||||||
|
render.JSON(w, r, errorResponse{
|
||||||
|
Message: err.Error(),
|
||||||
|
Status: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.NoContent(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
func (s Server) IsAuthenticated(next http.Handler) http.Handler {
|
func (s Server) IsAuthenticated(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -31,3 +38,49 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoggerMiddleware(logger *zerolog.Logger) func(next http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := logger.With().Logger()
|
||||||
|
|
||||||
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||||
|
|
||||||
|
t1 := time.Now()
|
||||||
|
defer func() {
|
||||||
|
t2 := time.Now()
|
||||||
|
|
||||||
|
// Recover and record stack traces in case of a panic
|
||||||
|
if rec := recover(); rec != nil {
|
||||||
|
log.Error().
|
||||||
|
Str("type", "error").
|
||||||
|
Timestamp().
|
||||||
|
Interface("recover_info", rec).
|
||||||
|
Bytes("debug_stack", debug.Stack()).
|
||||||
|
Msg("log system error")
|
||||||
|
http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log end request
|
||||||
|
log.Info().
|
||||||
|
Str("type", "access").
|
||||||
|
Timestamp().
|
||||||
|
Fields(map[string]interface{}{
|
||||||
|
"remote_ip": r.RemoteAddr,
|
||||||
|
"url": r.URL.Path,
|
||||||
|
"proto": r.Proto,
|
||||||
|
"method": r.Method,
|
||||||
|
"user_agent": r.Header.Get("User-Agent"),
|
||||||
|
"status": ww.Status(),
|
||||||
|
"latency_ms": float64(t2.Sub(t1).Nanoseconds()) / 1000000.0,
|
||||||
|
"bytes_in": r.Header.Get("Content-Length"),
|
||||||
|
"bytes_out": ww.BytesWritten(),
|
||||||
|
}).
|
||||||
|
Msg("incoming_request")
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(ww, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/config"
|
||||||
"github.com/autobrr/autobrr/internal/database"
|
"github.com/autobrr/autobrr/internal/database"
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
|
||||||
"github.com/autobrr/autobrr/internal/logger"
|
"github.com/autobrr/autobrr/internal/logger"
|
||||||
"github.com/autobrr/autobrr/web"
|
"github.com/autobrr/autobrr/web"
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ type Server struct {
|
||||||
sse *sse.Server
|
sse *sse.Server
|
||||||
db *database.DB
|
db *database.DB
|
||||||
|
|
||||||
config *domain.Config
|
config *config.AppConfig
|
||||||
cookieStore *sessions.CookieStore
|
cookieStore *sessions.CookieStore
|
||||||
|
|
||||||
version string
|
version string
|
||||||
|
@ -41,9 +41,10 @@ type Server struct {
|
||||||
ircService ircService
|
ircService ircService
|
||||||
notificationService notificationService
|
notificationService notificationService
|
||||||
releaseService releaseService
|
releaseService releaseService
|
||||||
|
updateService updateService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(log logger.Logger, config *domain.Config, sse *sse.Server, db *database.DB, version string, commit string, date string, actionService actionService, apiService apikeyService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, feedSvc feedService, indexerSvc indexerService, ircSvc ircService, notificationSvc notificationService, releaseSvc releaseService) Server {
|
func NewServer(log logger.Logger, config *config.AppConfig, sse *sse.Server, db *database.DB, version string, commit string, date string, actionService actionService, apiService apikeyService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, feedSvc feedService, indexerSvc indexerService, ircSvc ircService, notificationSvc notificationService, releaseSvc releaseService, updateSvc updateService) Server {
|
||||||
return Server{
|
return Server{
|
||||||
log: log.With().Str("module", "http").Logger(),
|
log: log.With().Str("module", "http").Logger(),
|
||||||
config: config,
|
config: config,
|
||||||
|
@ -53,7 +54,7 @@ func NewServer(log logger.Logger, config *domain.Config, sse *sse.Server, db *da
|
||||||
commit: commit,
|
commit: commit,
|
||||||
date: date,
|
date: date,
|
||||||
|
|
||||||
cookieStore: sessions.NewCookieStore([]byte(config.SessionSecret)),
|
cookieStore: sessions.NewCookieStore([]byte(config.Config.SessionSecret)),
|
||||||
|
|
||||||
actionService: actionService,
|
actionService: actionService,
|
||||||
apiService: apiService,
|
apiService: apiService,
|
||||||
|
@ -65,11 +66,12 @@ func NewServer(log logger.Logger, config *domain.Config, sse *sse.Server, db *da
|
||||||
ircService: ircSvc,
|
ircService: ircSvc,
|
||||||
notificationService: notificationSvc,
|
notificationService: notificationSvc,
|
||||||
releaseService: releaseSvc,
|
releaseService: releaseSvc,
|
||||||
|
updateService: updateSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) Open() error {
|
func (s Server) Open() error {
|
||||||
addr := fmt.Sprintf("%v:%v", s.config.Host, s.config.Port)
|
addr := fmt.Sprintf("%v:%v", s.config.Config.Host, s.config.Config.Port)
|
||||||
listener, err := net.Listen("tcp", addr)
|
listener, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -79,6 +81,8 @@ func (s Server) Open() error {
|
||||||
Handler: s.Handler(),
|
Handler: s.Handler(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.Info().Msgf("Starting server. Listening on %s", listener.Addr().String())
|
||||||
|
|
||||||
return server.Serve(listener)
|
return server.Serve(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +92,7 @@ func (s Server) Handler() http.Handler {
|
||||||
r.Use(middleware.RequestID)
|
r.Use(middleware.RequestID)
|
||||||
r.Use(middleware.RealIP)
|
r.Use(middleware.RealIP)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
r.Use(LoggerMiddleware(&s.log))
|
||||||
|
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
|
@ -113,15 +118,15 @@ func (s Server) Handler() http.Handler {
|
||||||
fileSystem.ServeHTTP(w, r)
|
fileSystem.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/api/auth", newAuthHandler(encoder, s.log, s.config, s.cookieStore, s.authService).Routes)
|
r.Route("/api", func(r chi.Router) {
|
||||||
r.Route("/api/healthz", newHealthHandler(encoder, s.db).Routes)
|
r.Route("/auth", newAuthHandler(encoder, s.log, s.config.Config, s.cookieStore, s.authService).Routes)
|
||||||
|
r.Route("/healthz", newHealthHandler(encoder, s.db).Routes)
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(s.IsAuthenticated)
|
r.Use(s.IsAuthenticated)
|
||||||
|
|
||||||
r.Route("/api", func(r chi.Router) {
|
|
||||||
r.Route("/actions", newActionHandler(encoder, s.actionService).Routes)
|
r.Route("/actions", newActionHandler(encoder, s.actionService).Routes)
|
||||||
r.Route("/config", newConfigHandler(encoder, s).Routes)
|
r.Route("/config", newConfigHandler(encoder, s, s.config).Routes)
|
||||||
r.Route("/download_clients", newDownloadClientHandler(encoder, s.downloadClientService).Routes)
|
r.Route("/download_clients", newDownloadClientHandler(encoder, s.downloadClientService).Routes)
|
||||||
r.Route("/filters", newFilterHandler(encoder, s.filterService).Routes)
|
r.Route("/filters", newFilterHandler(encoder, s.filterService).Routes)
|
||||||
r.Route("/feeds", newFeedHandler(encoder, s.feedService).Routes)
|
r.Route("/feeds", newFeedHandler(encoder, s.feedService).Routes)
|
||||||
|
@ -130,6 +135,7 @@ func (s Server) Handler() http.Handler {
|
||||||
r.Route("/keys", newAPIKeyHandler(encoder, s.apiService).Routes)
|
r.Route("/keys", newAPIKeyHandler(encoder, s.apiService).Routes)
|
||||||
r.Route("/notification", newNotificationHandler(encoder, s.notificationService).Routes)
|
r.Route("/notification", newNotificationHandler(encoder, s.notificationService).Routes)
|
||||||
r.Route("/release", newReleaseHandler(encoder, s.releaseService).Routes)
|
r.Route("/release", newReleaseHandler(encoder, s.releaseService).Routes)
|
||||||
|
r.Route("/updates", newUpdateHandler(encoder, s.updateService).Routes)
|
||||||
|
|
||||||
r.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
@ -157,7 +163,7 @@ func (s Server) index(w http.ResponseWriter, r *http.Request) {
|
||||||
p := web.IndexParams{
|
p := web.IndexParams{
|
||||||
Title: "Dashboard",
|
Title: "Dashboard",
|
||||||
Version: s.version,
|
Version: s.version,
|
||||||
BaseUrl: s.config.BaseURL,
|
BaseUrl: s.config.Config.BaseURL,
|
||||||
}
|
}
|
||||||
web.Index(w, p)
|
web.Index(w, p)
|
||||||
}
|
}
|
||||||
|
|
50
internal/http/update.go
Normal file
50
internal/http/update.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/pkg/version"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateService interface {
|
||||||
|
CheckUpdates(ctx context.Context)
|
||||||
|
GetLatestRelease(ctx context.Context) *version.Release
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateHandler struct {
|
||||||
|
encoder encoder
|
||||||
|
service updateService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateHandler(encoder encoder, service updateService) *updateHandler {
|
||||||
|
return &updateHandler{
|
||||||
|
encoder: encoder,
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h updateHandler) Routes(r chi.Router) {
|
||||||
|
r.Get("/latest", h.getLatest)
|
||||||
|
r.Get("/check", h.checkUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h updateHandler) getLatest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
latest := h.service.GetLatestRelease(r.Context())
|
||||||
|
if latest != nil {
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
render.JSON(w, r, latest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.NoContent(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h updateHandler) checkUpdates(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.service.CheckUpdates(r.Context())
|
||||||
|
|
||||||
|
render.NoContent(w, r)
|
||||||
|
}
|
|
@ -6,46 +6,42 @@ import (
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
"github.com/autobrr/autobrr/internal/notification"
|
"github.com/autobrr/autobrr/internal/notification"
|
||||||
"github.com/autobrr/autobrr/pkg/version"
|
"github.com/autobrr/autobrr/internal/update"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CheckUpdatesJob struct {
|
type CheckUpdatesJob struct {
|
||||||
Name string
|
Name string
|
||||||
Log zerolog.Logger
|
Log zerolog.Logger
|
||||||
Version string
|
Version string
|
||||||
NotifSvc notification.Service
|
NotifSvc notification.Service
|
||||||
|
updateService *update.Service
|
||||||
|
|
||||||
lastCheckVersion string
|
lastCheckVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *CheckUpdatesJob) Run() {
|
func (j *CheckUpdatesJob) Run() {
|
||||||
v := version.Checker{
|
newRelease, err := j.updateService.CheckUpdateAvailable(context.TODO())
|
||||||
Owner: "autobrr",
|
|
||||||
Repo: "autobrr",
|
|
||||||
}
|
|
||||||
|
|
||||||
newAvailable, newVersion, err := v.CheckNewVersion(context.TODO(), j.Version)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
j.Log.Error().Err(err).Msg("could not check for new release")
|
j.Log.Error().Err(err).Msg("could not check for new release")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if newAvailable {
|
if newRelease != nil {
|
||||||
j.Log.Info().Msgf("a new release has been found: %v Consider updating.", newVersion)
|
|
||||||
|
|
||||||
// this is not persisted so this can trigger more than once
|
// this is not persisted so this can trigger more than once
|
||||||
// lets check if we have different versions between runs
|
// lets check if we have different versions between runs
|
||||||
if newVersion != j.lastCheckVersion {
|
if newRelease.TagName != j.lastCheckVersion {
|
||||||
|
j.Log.Info().Msgf("a new release has been found: %v Consider updating.", newRelease.TagName)
|
||||||
|
|
||||||
j.NotifSvc.Send(domain.NotificationEventAppUpdateAvailable, domain.NotificationPayload{
|
j.NotifSvc.Send(domain.NotificationEventAppUpdateAvailable, domain.NotificationPayload{
|
||||||
Subject: "New update available!",
|
Subject: "New update available!",
|
||||||
Message: newVersion,
|
Message: newRelease.TagName,
|
||||||
Event: domain.NotificationEventAppUpdateAvailable,
|
Event: domain.NotificationEventAppUpdateAvailable,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
j.lastCheckVersion = newVersion
|
j.lastCheckVersion = newRelease.TagName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,10 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
"github.com/autobrr/autobrr/internal/logger"
|
"github.com/autobrr/autobrr/internal/logger"
|
||||||
"github.com/autobrr/autobrr/internal/notification"
|
"github.com/autobrr/autobrr/internal/notification"
|
||||||
|
"github.com/autobrr/autobrr/internal/update"
|
||||||
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
@ -21,19 +23,22 @@ type Service interface {
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
log zerolog.Logger
|
log zerolog.Logger
|
||||||
|
config *domain.Config
|
||||||
version string
|
version string
|
||||||
notificationSvc notification.Service
|
notificationSvc notification.Service
|
||||||
|
updateSvc *update.Service
|
||||||
|
|
||||||
cron *cron.Cron
|
cron *cron.Cron
|
||||||
jobs map[string]cron.EntryID
|
jobs map[string]cron.EntryID
|
||||||
m sync.RWMutex
|
m sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(log logger.Logger, version string, notificationSvc notification.Service) Service {
|
func NewService(log logger.Logger, config *domain.Config, notificationSvc notification.Service, updateSvc *update.Service) Service {
|
||||||
return &service{
|
return &service{
|
||||||
log: log.With().Str("module", "scheduler").Logger(),
|
log: log.With().Str("module", "scheduler").Logger(),
|
||||||
version: version,
|
config: config,
|
||||||
notificationSvc: notificationSvc,
|
notificationSvc: notificationSvc,
|
||||||
|
updateSvc: updateSvc,
|
||||||
cron: cron.New(cron.WithChain(
|
cron: cron.New(cron.WithChain(
|
||||||
cron.Recover(cron.DefaultLogger),
|
cron.Recover(cron.DefaultLogger),
|
||||||
)),
|
)),
|
||||||
|
@ -56,16 +61,19 @@ func (s *service) Start() {
|
||||||
func (s *service) addAppJobs() {
|
func (s *service) addAppJobs() {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
checkUpdates := &CheckUpdatesJob{
|
if s.config.CheckForUpdates {
|
||||||
Name: "app-check-updates",
|
checkUpdates := &CheckUpdatesJob{
|
||||||
Log: s.log.With().Str("job", "app-check-updates").Logger(),
|
Name: "app-check-updates",
|
||||||
Version: s.version,
|
Log: s.log.With().Str("job", "app-check-updates").Logger(),
|
||||||
NotifSvc: s.notificationSvc,
|
Version: s.version,
|
||||||
lastCheckVersion: "",
|
NotifSvc: s.notificationSvc,
|
||||||
}
|
updateService: s.updateSvc,
|
||||||
|
lastCheckVersion: s.version,
|
||||||
|
}
|
||||||
|
|
||||||
if id, err := s.AddJob(checkUpdates, time.Duration(36*time.Hour), "app-check-updates"); err != nil {
|
if id, err := s.AddJob(checkUpdates, 2*time.Hour, "app-check-updates"); err != nil {
|
||||||
s.log.Error().Err(err).Msgf("scheduler.addAppJobs: error adding job: %v", id)
|
s.log.Error().Err(err).Msgf("scheduler.addAppJobs: error adding job: %v", id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +89,7 @@ func (s *service) AddJob(job cron.Job, interval time.Duration, identifier string
|
||||||
cron.SkipIfStillRunning(cron.DiscardLogger)).Then(job),
|
cron.SkipIfStillRunning(cron.DiscardLogger)).Then(job),
|
||||||
)
|
)
|
||||||
|
|
||||||
s.log.Debug().Msgf("scheduler.AddJob: job successfully added: %v", id)
|
s.log.Debug().Msgf("scheduler.AddJob: job successfully added: %s id %d", identifier, id)
|
||||||
|
|
||||||
s.m.Lock()
|
s.m.Lock()
|
||||||
// add to job map
|
// add to job map
|
||||||
|
|
|
@ -1,43 +1,49 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/feed"
|
"github.com/autobrr/autobrr/internal/feed"
|
||||||
"github.com/autobrr/autobrr/internal/indexer"
|
"github.com/autobrr/autobrr/internal/indexer"
|
||||||
"github.com/autobrr/autobrr/internal/irc"
|
"github.com/autobrr/autobrr/internal/irc"
|
||||||
"github.com/autobrr/autobrr/internal/logger"
|
"github.com/autobrr/autobrr/internal/logger"
|
||||||
"github.com/autobrr/autobrr/internal/scheduler"
|
"github.com/autobrr/autobrr/internal/scheduler"
|
||||||
|
"github.com/autobrr/autobrr/internal/update"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
log zerolog.Logger
|
log zerolog.Logger
|
||||||
Hostname string
|
config *domain.Config
|
||||||
Port int
|
|
||||||
|
|
||||||
indexerService indexer.Service
|
indexerService indexer.Service
|
||||||
ircService irc.Service
|
ircService irc.Service
|
||||||
feedService feed.Service
|
feedService feed.Service
|
||||||
scheduler scheduler.Service
|
scheduler scheduler.Service
|
||||||
|
updateService *update.Service
|
||||||
|
|
||||||
stopWG sync.WaitGroup
|
stopWG sync.WaitGroup
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(log logger.Logger, ircSvc irc.Service, indexerSvc indexer.Service, feedSvc feed.Service, scheduler scheduler.Service) *Server {
|
func NewServer(log logger.Logger, config *domain.Config, ircSvc irc.Service, indexerSvc indexer.Service, feedSvc feed.Service, scheduler scheduler.Service, updateSvc *update.Service) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
log: log.With().Str("module", "server").Logger(),
|
log: log.With().Str("module", "server").Logger(),
|
||||||
|
config: config,
|
||||||
indexerService: indexerSvc,
|
indexerService: indexerSvc,
|
||||||
ircService: ircSvc,
|
ircService: ircSvc,
|
||||||
feedService: feedSvc,
|
feedService: feedSvc,
|
||||||
scheduler: scheduler,
|
scheduler: scheduler,
|
||||||
|
updateService: updateSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
s.log.Info().Msgf("Starting server. Listening on %v:%v", s.Hostname, s.Port)
|
go s.checkUpdates()
|
||||||
|
|
||||||
// start cron scheduler
|
// start cron scheduler
|
||||||
s.scheduler.Start()
|
s.scheduler.Start()
|
||||||
|
@ -68,3 +74,11 @@ func (s *Server) Shutdown() {
|
||||||
// stop cron scheduler
|
// stop cron scheduler
|
||||||
s.scheduler.Stop()
|
s.scheduler.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) checkUpdates() {
|
||||||
|
if s.config.CheckForUpdates {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
s.updateService.CheckUpdates(context.Background())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
71
internal/update/update.go
Normal file
71
internal/update/update.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/autobrr/autobrr/internal/logger"
|
||||||
|
"github.com/autobrr/autobrr/pkg/version"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
log zerolog.Logger
|
||||||
|
config *domain.Config
|
||||||
|
|
||||||
|
m sync.RWMutex
|
||||||
|
releaseChecker *version.Checker
|
||||||
|
latestRelease *version.Release
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdate(log logger.Logger, config *domain.Config) *Service {
|
||||||
|
return &Service{
|
||||||
|
log: log.With().Str("module", "update").Logger(),
|
||||||
|
config: config,
|
||||||
|
releaseChecker: version.NewChecker("autobrr", "autobrr", config.Version),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetLatestRelease(ctx context.Context) *version.Release {
|
||||||
|
s.m.RLock()
|
||||||
|
defer s.m.RUnlock()
|
||||||
|
return s.latestRelease
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CheckUpdates(ctx context.Context) {
|
||||||
|
if _, err := s.CheckUpdateAvailable(ctx); err != nil {
|
||||||
|
s.log.Error().Err(err).Msg("error checking new release")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CheckUpdateAvailable(ctx context.Context) (*version.Release, error) {
|
||||||
|
s.log.Trace().Msg("checking for updates...")
|
||||||
|
|
||||||
|
newAvailable, newVersion, err := s.releaseChecker.CheckNewVersion(ctx, s.config.Version)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error().Err(err).Msg("could not check for new release")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if newAvailable {
|
||||||
|
s.log.Info().Msgf("autobrr outdated, found newer release: %s", newVersion.TagName)
|
||||||
|
|
||||||
|
s.m.Lock()
|
||||||
|
defer s.m.Unlock()
|
||||||
|
|
||||||
|
if s.latestRelease != nil && s.latestRelease.TagName == newVersion.TagName {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.latestRelease = newVersion
|
||||||
|
|
||||||
|
return newVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/pkg/errors"
|
"github.com/autobrr/autobrr/pkg/errors"
|
||||||
|
|
||||||
|
@ -13,16 +15,50 @@ import (
|
||||||
|
|
||||||
// Release is a GitHub release
|
// Release is a GitHub release
|
||||||
type Release struct {
|
type Release struct {
|
||||||
TagName string `json:"tag_name,omitempty"`
|
ID int64 `json:"id,omitempty"`
|
||||||
TargetCommitish *string `json:"target_commitish,omitempty"`
|
NodeID string `json:"node_id,omitempty"`
|
||||||
Name *string `json:"name,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
Body *string `json:"body,omitempty"`
|
HtmlURL string `json:"html_url,omitempty"`
|
||||||
Draft *bool `json:"draft,omitempty"`
|
TagName string `json:"tag_name,omitempty"`
|
||||||
Prerelease *bool `json:"prerelease,omitempty"`
|
TargetCommitish string `json:"target_commitish,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Body *string `json:"body,omitempty"`
|
||||||
|
Draft bool `json:"draft,omitempty"`
|
||||||
|
Prerelease bool `json:"prerelease,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
PublishedAt time.Time `json:"published_at"`
|
||||||
|
Author Author `json:"author"`
|
||||||
|
Assets []Asset `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
NodeId string `json:"node_id"`
|
||||||
|
AvatarUrl string `json:"avatar_url"`
|
||||||
|
GravatarId string `json:"gravatar_id"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
HtmlUrl string `json:"html_url"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
type Asset struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
NodeId string `json:"node_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Uploader Author `json:"uploader"`
|
||||||
|
ContentType string `json:"content_type"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
DownloadCount int64 `json:"download_count"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
BrowserDownloadUrl string `json:"browser_download_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Release) IsPreOrDraft() bool {
|
func (r *Release) IsPreOrDraft() bool {
|
||||||
if *r.Draft || *r.Prerelease {
|
if r.Draft || r.Prerelease {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -30,12 +66,21 @@ func (r *Release) IsPreOrDraft() bool {
|
||||||
|
|
||||||
type Checker struct {
|
type Checker struct {
|
||||||
// user/repo-name or org/repo-name
|
// user/repo-name or org/repo-name
|
||||||
Owner string
|
Owner string
|
||||||
Repo string
|
Repo string
|
||||||
|
CurrentVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChecker(owner, repo, currentVersion string) *Checker {
|
||||||
|
return &Checker{
|
||||||
|
Owner: owner,
|
||||||
|
Repo: repo,
|
||||||
|
CurrentVersion: currentVersion,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checker) get(ctx context.Context) (*Release, error) {
|
func (c *Checker) get(ctx context.Context) (*Release, error) {
|
||||||
url := fmt.Sprintf("https://api.github.com/repos/%v/%v/releases/latest", c.Owner, c.Repo)
|
url := fmt.Sprintf("https://api.autobrr.com/repos/%s/%s/releases/latest", c.Owner, c.Repo)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -43,6 +88,8 @@ func (c *Checker) get(ctx context.Context) (*Release, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||||
|
req.Header.Set("User-Agent", c.buildUserAgent())
|
||||||
|
|
||||||
client := http.DefaultClient
|
client := http.DefaultClient
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
@ -64,17 +111,26 @@ func (c *Checker) get(ctx context.Context) (*Release, error) {
|
||||||
return &release, nil
|
return &release, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checker) CheckNewVersion(ctx context.Context, version string) (bool, string, error) {
|
func (c *Checker) CheckNewVersion(ctx context.Context, version string) (bool, *Release, error) {
|
||||||
if isDevelop(version) {
|
if isDevelop(version) {
|
||||||
return false, "", nil
|
return false, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := c.get(ctx)
|
release, err := c.get(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.checkNewVersion(version, release)
|
newAvailable, _, err := c.checkNewVersion(version, release)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !newAvailable {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, release, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checker) checkNewVersion(version string, release *Release) (bool, string, error) {
|
func (c *Checker) checkNewVersion(version string, release *Release) (bool, string, error) {
|
||||||
|
@ -100,8 +156,12 @@ func (c *Checker) checkNewVersion(version string, release *Release) (bool, strin
|
||||||
return false, "", nil
|
return false, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Checker) buildUserAgent() string {
|
||||||
|
return fmt.Sprintf("autobrr/%s (%s %s)", c.CurrentVersion, runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
func isDevelop(version string) bool {
|
func isDevelop(version string) bool {
|
||||||
tags := []string{"dev", "develop", "master", "latest"}
|
tags := []string{"dev", "develop", "master", "latest", ""}
|
||||||
|
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
if version == tag {
|
if version == tag {
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestGitHubReleaseChecker_checkNewVersion(t *testing.T) {
|
||||||
version: "v0.2.0",
|
version: "v0.2.0",
|
||||||
release: &Release{
|
release: &Release{
|
||||||
TagName: "v0.3.0",
|
TagName: "v0.3.0",
|
||||||
TargetCommitish: nil,
|
TargetCommitish: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantNew: true,
|
wantNew: true,
|
||||||
|
@ -43,7 +43,7 @@ func TestGitHubReleaseChecker_checkNewVersion(t *testing.T) {
|
||||||
version: "v0.2.0",
|
version: "v0.2.0",
|
||||||
release: &Release{
|
release: &Release{
|
||||||
TagName: "v0.2.0",
|
TagName: "v0.2.0",
|
||||||
TargetCommitish: nil,
|
TargetCommitish: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantNew: false,
|
wantNew: false,
|
||||||
|
@ -57,7 +57,7 @@ func TestGitHubReleaseChecker_checkNewVersion(t *testing.T) {
|
||||||
version: "v0.3.0",
|
version: "v0.3.0",
|
||||||
release: &Release{
|
release: &Release{
|
||||||
TagName: "v0.2.0",
|
TagName: "v0.2.0",
|
||||||
TargetCommitish: nil,
|
TargetCommitish: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantNew: false,
|
wantNew: false,
|
||||||
|
@ -71,7 +71,7 @@ func TestGitHubReleaseChecker_checkNewVersion(t *testing.T) {
|
||||||
version: "v0.3.0",
|
version: "v0.3.0",
|
||||||
release: &Release{
|
release: &Release{
|
||||||
TagName: "v0.3.0-rc1",
|
TagName: "v0.3.0-rc1",
|
||||||
TargetCommitish: nil,
|
TargetCommitish: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantNew: false,
|
wantNew: false,
|
||||||
|
@ -85,7 +85,7 @@ func TestGitHubReleaseChecker_checkNewVersion(t *testing.T) {
|
||||||
version: "v0.3.0-RC1",
|
version: "v0.3.0-RC1",
|
||||||
release: &Release{
|
release: &Release{
|
||||||
TagName: "v0.3.0-RC2",
|
TagName: "v0.3.0-RC2",
|
||||||
TargetCommitish: nil,
|
TargetCommitish: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantNew: true,
|
wantNew: true,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { baseUrl, sseBaseUrl } from "../utils";
|
import { baseUrl, sseBaseUrl } from "../utils";
|
||||||
import { AuthContext } from "../utils/Context";
|
import { AuthContext } from "../utils/Context";
|
||||||
|
import { GithubRelease } from "../types/Update";
|
||||||
|
|
||||||
interface ConfigType {
|
interface ConfigType {
|
||||||
body?: BodyInit | Record<string, unknown> | unknown;
|
body?: BodyInit | Record<string, unknown> | unknown;
|
||||||
|
@ -80,7 +81,8 @@ export const APIClient = {
|
||||||
delete: (key: string) => appClient.Delete(`api/keys/${key}`)
|
delete: (key: string) => appClient.Delete(`api/keys/${key}`)
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
get: () => appClient.Get<Config>("api/config")
|
get: () => appClient.Get<Config>("api/config"),
|
||||||
|
update: (config: ConfigUpdate) => appClient.Patch("api/config", config)
|
||||||
},
|
},
|
||||||
download_clients: {
|
download_clients: {
|
||||||
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
|
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
|
||||||
|
@ -180,5 +182,9 @@ export const APIClient = {
|
||||||
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
|
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
|
||||||
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
||||||
delete: () => appClient.Delete("api/release/all")
|
delete: () => appClient.Delete("api/release/all")
|
||||||
|
},
|
||||||
|
updates: {
|
||||||
|
check: () => appClient.Get("api/updates/check"),
|
||||||
|
getLatestRelease: () => appClient.Get<GithubRelease|undefined>("api/updates/latest")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,11 +2,13 @@ import { Fragment } from "react";
|
||||||
import { Link, NavLink, Outlet } from "react-router-dom";
|
import { Link, NavLink, Outlet } from "react-router-dom";
|
||||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||||
import { BookOpenIcon, UserIcon } from "@heroicons/react/24/solid";
|
import { BookOpenIcon, UserIcon } from "@heroicons/react/24/solid";
|
||||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { AuthContext } from "../utils/Context";
|
import { AuthContext } from "../utils/Context";
|
||||||
|
|
||||||
import logo from "../logo.png";
|
import logo from "../logo.png";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { APIClient } from "../api/APIClient";
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -27,6 +29,17 @@ export default function Base() {
|
||||||
{ name: "Logs", path: "/logs" }
|
{ name: "Logs", path: "/logs" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const { data } = useQuery(
|
||||||
|
["updates"],
|
||||||
|
() => APIClient.updates.getLatestRelease(),
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
onError: err => console.log(err)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Disclosure
|
<Disclosure
|
||||||
|
@ -185,6 +198,16 @@ export default function Base() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{data && data.html_url && (
|
||||||
|
<a href={data.html_url} target="_blank">
|
||||||
|
<div className="flex mt-4 py-2 bg-blue-500 rounded justify-center">
|
||||||
|
<MegaphoneIcon className="h-6 w-6 text-blue-100"/>
|
||||||
|
<span className="text-blue-100 font-medium mx-3">New update available!</span>
|
||||||
|
<span className="inline-flex items-center rounded-md bg-blue-100 px-2.5 py-0.5 text-sm font-medium text-blue-800">{data?.name}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
|
<Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import { useQuery } from "react-query";
|
import { useMutation, useQuery } from "react-query";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
import { Checkbox } from "../../components/Checkbox";
|
import { Checkbox } from "../../components/Checkbox";
|
||||||
import { SettingsContext } from "../../utils/Context";
|
import { SettingsContext } from "../../utils/Context";
|
||||||
|
import { GithubRelease } from "../../types/Update";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import Toast from "../../components/notifications/Toast";
|
||||||
|
import { queryClient } from "../../App";
|
||||||
|
|
||||||
interface RowItemProps {
|
interface RowItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
newUpdate?: GithubRelease;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RowItem = ({ label, value, title }: RowItemProps) => {
|
const RowItem = ({ label, value, title }: RowItemProps) => {
|
||||||
|
@ -23,6 +28,25 @@ const RowItem = ({ label, value, title }: RowItemProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RowItemVersion = ({ label, value, title, newUpdate }: RowItemProps) => {
|
||||||
|
if (!value)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||||
|
<dt className="font-medium text-gray-500 dark:text-white" title={title}>{label}:</dt>
|
||||||
|
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2 break-all">
|
||||||
|
{value}
|
||||||
|
{newUpdate && newUpdate.html_url && (
|
||||||
|
<span>
|
||||||
|
<a href={newUpdate.html_url} target="_blank"><span className="ml-2 inline-flex items-center rounded-md bg-green-100 px-2.5 py-0.5 text-sm font-medium text-green-800">{newUpdate.name} available!</span></a>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function ApplicationSettings() {
|
function ApplicationSettings() {
|
||||||
const [settings, setSettings] = SettingsContext.use();
|
const [settings, setSettings] = SettingsContext.use();
|
||||||
|
|
||||||
|
@ -36,6 +60,38 @@ function ApplicationSettings() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: updateData } = useQuery(
|
||||||
|
["updates"],
|
||||||
|
() => APIClient.updates.getLatestRelease(),
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
onError: err => console.log(err)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkUpdateMutation = useMutation(
|
||||||
|
() => APIClient.updates.check(),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["updates"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleCheckUpdateMutation = useMutation(
|
||||||
|
(value: boolean) => APIClient.config.update({ check_for_updates: value }),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries(["config"]);
|
||||||
|
|
||||||
|
checkUpdateMutation.mutate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
@ -98,7 +154,7 @@ function ApplicationSettings() {
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<div className="px-4 py-5 sm:p-0">
|
<div className="px-4 py-5 sm:p-0">
|
||||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<RowItem label="Version" value={data?.version} />
|
<RowItemVersion label="Version" value={data?.version} newUpdate={updateData ?? undefined} />
|
||||||
<RowItem label="Commit" value={data?.commit} />
|
<RowItem label="Commit" value={data?.commit} />
|
||||||
<RowItem label="Build date" value={data?.date} />
|
<RowItem label="Build date" value={data?.date} />
|
||||||
<RowItem label="Log path" value={data?.log_path} title="Set in config.toml" />
|
<RowItem label="Log path" value={data?.log_path} title="Set in config.toml" />
|
||||||
|
@ -117,6 +173,16 @@ function ApplicationSettings() {
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-4 sm:px-6 py-1">
|
||||||
|
<Checkbox
|
||||||
|
label="Check for updates"
|
||||||
|
description="Get notified of new updates."
|
||||||
|
value={data?.check_for_updates ?? true}
|
||||||
|
setValue={(newValue: boolean) => {
|
||||||
|
toggleCheckUpdateMutation.mutate(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="px-4 sm:px-6 py-1">
|
<div className="px-4 sm:px-6 py-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Dark theme"
|
label="Dark theme"
|
||||||
|
|
20
web/src/types/Config.d.ts
vendored
Normal file
20
web/src/types/Config.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
interface Config {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
log_level: string;
|
||||||
|
log_path: string;
|
||||||
|
base_url: string;
|
||||||
|
check_for_updates: boolean;
|
||||||
|
version: string;
|
||||||
|
commit: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigUpdate {
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
log_level?: string;
|
||||||
|
log_path?: string;
|
||||||
|
base_url?: string;
|
||||||
|
check_for_updates: boolean;
|
||||||
|
}
|
11
web/src/types/Irc.d.ts
vendored
11
web/src/types/Irc.d.ts
vendored
|
@ -67,14 +67,3 @@ interface IrcAuth {
|
||||||
account?: string; // optional
|
account?: string; // optional
|
||||||
password?: string; // optional
|
password?: string; // optional
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Config {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
log_level: string;
|
|
||||||
log_path: string;
|
|
||||||
base_url: string;
|
|
||||||
version: string;
|
|
||||||
commit: string;
|
|
||||||
date: string;
|
|
||||||
}
|
|
46
web/src/types/Update.d.ts
vendored
Normal file
46
web/src/types/Update.d.ts
vendored
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
interface UpdateAvailableResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GithubRelease {
|
||||||
|
id: number;
|
||||||
|
node_id: string;
|
||||||
|
url: string;
|
||||||
|
html_url: string;
|
||||||
|
tag_name: string;
|
||||||
|
target_commitish: string;
|
||||||
|
name: string;
|
||||||
|
body: string;
|
||||||
|
created_at: Date;
|
||||||
|
published_at: Date;
|
||||||
|
author: GithubAuthor;
|
||||||
|
assets: GitHubReleaseAsset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubReleaseAsset {
|
||||||
|
url: string;
|
||||||
|
id: number;
|
||||||
|
node_id: string;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
uploader: GithubAuthor;
|
||||||
|
content_type: string;
|
||||||
|
state: string;
|
||||||
|
size: number;
|
||||||
|
download_count: number;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
browser_download_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GithubAuthor {
|
||||||
|
login: string;
|
||||||
|
id: number;
|
||||||
|
node_id: string;
|
||||||
|
avatar_url: string;
|
||||||
|
gravatar_id: string;
|
||||||
|
url: string;
|
||||||
|
html_url: string;
|
||||||
|
type: string;
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ export const AuthContext = newRidgeState<AuthInfo>(
|
||||||
|
|
||||||
interface SettingsType {
|
interface SettingsType {
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
|
checkForUpdates: boolean;
|
||||||
darkTheme: boolean;
|
darkTheme: boolean;
|
||||||
scrollOnNewLog: boolean;
|
scrollOnNewLog: boolean;
|
||||||
indentLogLines: boolean;
|
indentLogLines: boolean;
|
||||||
|
@ -54,6 +55,7 @@ interface SettingsType {
|
||||||
export const SettingsContext = newRidgeState<SettingsType>(
|
export const SettingsContext = newRidgeState<SettingsType>(
|
||||||
{
|
{
|
||||||
debug: false,
|
debug: false,
|
||||||
|
checkForUpdates: true,
|
||||||
darkTheme: true,
|
darkTheme: true,
|
||||||
scrollOnNewLog: false,
|
scrollOnNewLog: false,
|
||||||
indentLogLines: false,
|
indentLogLines: false,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue