mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Manager refactor, part 1 (#4298)
* Move BackupDatabase and AnonymiseDatabase to internal/manager * Rename config.Instance to config.Config * Rename FFMPEG * Rework manager and initialization process * Fix Makefile * Tweak phasher * Fix config races * Fix setup error not clearing
This commit is contained in:
parent
fc1fc20df4
commit
b78771dbcd
45 changed files with 1230 additions and 1213 deletions
26
Makefile
26
Makefile
|
|
@ -9,9 +9,11 @@ endif
|
||||||
ifdef IS_WIN_SHELL
|
ifdef IS_WIN_SHELL
|
||||||
RM := del /s /q
|
RM := del /s /q
|
||||||
RMDIR := rmdir /s /q
|
RMDIR := rmdir /s /q
|
||||||
|
NOOP := @@
|
||||||
else
|
else
|
||||||
RM := rm -f
|
RM := rm -f
|
||||||
RMDIR := rm -rf
|
RMDIR := rm -rf
|
||||||
|
NOOP := @:
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# set LDFLAGS environment variable to any extra ldflags required
|
# set LDFLAGS environment variable to any extra ldflags required
|
||||||
|
|
@ -54,38 +56,36 @@ release: pre-ui generate ui build-release
|
||||||
# for a static-pie release build: `make flags-static-pie flags-release stash`
|
# for a static-pie release build: `make flags-static-pie flags-release stash`
|
||||||
# for a static windows debug build: `make flags-static-windows stash`
|
# for a static windows debug build: `make flags-static-windows stash`
|
||||||
|
|
||||||
# shell noop: prevents "nothing to be done" warnings
|
# $(NOOP) prevents "nothing to be done" warnings
|
||||||
.PHONY: flags
|
|
||||||
flags:
|
|
||||||
ifdef IS_WIN_SHELL
|
|
||||||
@@
|
|
||||||
else
|
|
||||||
@:
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: flags-release
|
.PHONY: flags-release
|
||||||
flags-release: flags
|
flags-release:
|
||||||
|
$(NOOP)
|
||||||
$(eval LDFLAGS += -s -w)
|
$(eval LDFLAGS += -s -w)
|
||||||
$(eval GO_BUILD_FLAGS += -trimpath)
|
$(eval GO_BUILD_FLAGS += -trimpath)
|
||||||
|
|
||||||
.PHONY: flags-pie
|
.PHONY: flags-pie
|
||||||
flags-pie: flags
|
flags-pie:
|
||||||
|
$(NOOP)
|
||||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||||
|
|
||||||
.PHONY: flags-static
|
.PHONY: flags-static
|
||||||
flags-static: flags
|
flags-static:
|
||||||
|
$(NOOP)
|
||||||
$(eval LDFLAGS += -extldflags=-static)
|
$(eval LDFLAGS += -extldflags=-static)
|
||||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
||||||
|
|
||||||
.PHONY: flags-static-pie
|
.PHONY: flags-static-pie
|
||||||
flags-static-pie: flags
|
flags-static-pie:
|
||||||
|
$(NOOP)
|
||||||
$(eval LDFLAGS += -extldflags=-static-pie)
|
$(eval LDFLAGS += -extldflags=-static-pie)
|
||||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
||||||
|
|
||||||
# identical to flags-static-pie, but excluding netgo, which is not needed on windows
|
# identical to flags-static-pie, but excluding netgo, which is not needed on windows
|
||||||
.PHONY: flags-static-windows
|
.PHONY: flags-static-windows
|
||||||
flags-static-windows: flags
|
flags-static-windows:
|
||||||
|
$(NOOP)
|
||||||
$(eval LDFLAGS += -extldflags=-static-pie)
|
$(eval LDFLAGS += -extldflags=-static-pie)
|
||||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo)
|
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo)
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ On Windows or macOS, running the app might present a security prompt since the b
|
||||||
|
|
||||||
On Windows, bypass this by clicking "more info" and then the "run anyway" button. On macOS, Control+Click the app, click "Open", and then "Open" again.
|
On Windows, bypass this by clicking "more info" and then the "run anyway" button. On macOS, Control+Click the app, click "Open", and then "Open" again.
|
||||||
|
|
||||||
#### FFMPEG
|
#### FFmpeg
|
||||||
Stash requires ffmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
|
Stash requires FFmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
|
@ -66,13 +65,13 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! Consier using GNU Parallel.")
|
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! If required, use e.g. GNU Parallel to run concurrently.")
|
||||||
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
|
ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
|
||||||
encoder := ffmpeg.NewEncoder(ffmpegPath)
|
encoder := ffmpeg.NewEncoder(ffmpegPath)
|
||||||
encoder.InitHWSupport(context.TODO())
|
// don't need to InitHWSupport, phashing doesn't use hw acceleration
|
||||||
ffprobe := ffmpeg.FFProbe(ffprobePath)
|
ffprobe := ffmpeg.FFProbe(ffprobePath)
|
||||||
|
|
||||||
for _, item := range args {
|
for _, item := range args {
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,154 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"runtime/debug"
|
||||||
|
"runtime/pprof"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/api"
|
"github.com/stashapp/stash/internal/api"
|
||||||
|
"github.com/stashapp/stash/internal/build"
|
||||||
"github.com/stashapp/stash/internal/desktop"
|
"github.com/stashapp/stash/internal/desktop"
|
||||||
|
"github.com/stashapp/stash/internal/log"
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/ui"
|
"github.com/stashapp/stash/ui"
|
||||||
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var exitCode = 0
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer recoverPanic()
|
defer func() {
|
||||||
|
if exitCode != 0 {
|
||||||
_, err := manager.Initialize()
|
os.Exit(exitCode)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer recoverPanic()
|
|
||||||
if err := api.Start(); err != nil {
|
|
||||||
handleError(err)
|
|
||||||
} else {
|
|
||||||
manager.GetInstance().Shutdown(0)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go handleSignals()
|
defer recoverPanic()
|
||||||
desktop.Start(manager.GetInstance(), &ui.FaviconProvider)
|
|
||||||
|
|
||||||
blockForever()
|
helpFlag := false
|
||||||
|
pflag.BoolVarP(&helpFlag, "help", "h", false, "show this help text and exit")
|
||||||
|
|
||||||
|
versionFlag := false
|
||||||
|
pflag.BoolVarP(&versionFlag, "version", "v", false, "show version number and exit")
|
||||||
|
|
||||||
|
cpuProfilePath := ""
|
||||||
|
pflag.StringVar(&cpuProfilePath, "cpuprofile", "", "write cpu profile to file")
|
||||||
|
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
if helpFlag {
|
||||||
|
pflag.Usage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionFlag {
|
||||||
|
fmt.Println(build.VersionString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
exitError(fmt.Errorf("config initialization error: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := initLog(cfg)
|
||||||
|
|
||||||
|
if cpuProfilePath != "" {
|
||||||
|
if err := initProfiling(cpuProfilePath); err != nil {
|
||||||
|
exitError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer pprof.StopCPUProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := manager.Initialize(cfg, l)
|
||||||
|
if err != nil {
|
||||||
|
exitError(fmt.Errorf("manager initialization error: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer mgr.Shutdown()
|
||||||
|
|
||||||
|
server, err := api.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
exitError(fmt.Errorf("api initialization error: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer server.Shutdown()
|
||||||
|
|
||||||
|
exit := make(chan int)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := server.Start()
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
exitError(fmt.Errorf("http server error: %w", err))
|
||||||
|
exit <- 1
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go handleSignals(exit)
|
||||||
|
desktop.Start(exit, &ui.FaviconProvider)
|
||||||
|
|
||||||
|
exitCode = <-exit
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLog(cfg *config.Config) *log.Logger {
|
||||||
|
l := log.NewLogger()
|
||||||
|
l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel())
|
||||||
|
logger.Logger = l
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func initProfiling(path string) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create CPU profile file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = pprof.StartCPUProfile(f); err != nil {
|
||||||
|
return fmt.Errorf("could not start CPU profiling: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("profiling to %s", path)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func recoverPanic() {
|
func recoverPanic() {
|
||||||
if p := recover(); p != nil {
|
if err := recover(); err != nil {
|
||||||
handleError(fmt.Errorf("Panic: %v", p))
|
exitCode = 1
|
||||||
|
logger.Errorf("panic: %v\n%s", err, debug.Stack())
|
||||||
|
if desktop.IsDesktop() {
|
||||||
|
desktop.FatalError(fmt.Errorf("Panic: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleError(err error) {
|
func exitError(err error) {
|
||||||
|
exitCode = 1
|
||||||
|
logger.Error(err)
|
||||||
if desktop.IsDesktop() {
|
if desktop.IsDesktop() {
|
||||||
desktop.FatalError(err)
|
desktop.FatalError(err)
|
||||||
manager.GetInstance().Shutdown(0)
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSignals() {
|
func handleSignals(exit chan<- int) {
|
||||||
// handle signals
|
// handle signals
|
||||||
signals := make(chan os.Signal, 1)
|
signals := make(chan os.Signal, 1)
|
||||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
<-signals
|
<-signals
|
||||||
manager.GetInstance().Shutdown(0)
|
exit <- 0
|
||||||
}
|
|
||||||
|
|
||||||
func blockForever() {
|
|
||||||
select {}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -505,19 +505,10 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAIn
|
||||||
c.Set(config.DLNAVideoSortOrder, input.VideoSortOrder)
|
c.Set(config.DLNAVideoSortOrder, input.VideoSortOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentDLNAEnabled := c.GetDLNADefaultEnabled()
|
refresh := false
|
||||||
if input.Enabled != nil && *input.Enabled != currentDLNAEnabled {
|
if input.Enabled != nil {
|
||||||
c.Set(config.DLNADefaultEnabled, *input.Enabled)
|
c.Set(config.DLNADefaultEnabled, *input.Enabled)
|
||||||
|
refresh = true
|
||||||
// start/stop the DLNA service as needed
|
|
||||||
dlnaService := manager.GetInstance().DLNAService
|
|
||||||
if !*input.Enabled && dlnaService.IsRunning() {
|
|
||||||
dlnaService.Stop(nil)
|
|
||||||
} else if *input.Enabled && !dlnaService.IsRunning() {
|
|
||||||
if err := dlnaService.Start(nil); err != nil {
|
|
||||||
logger.Warnf("error starting DLNA service: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Interfaces != nil {
|
if input.Interfaces != nil {
|
||||||
|
|
@ -528,6 +519,10 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAIn
|
||||||
return makeConfigDLNAResult(), err
|
return makeConfigDLNAResult(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if refresh {
|
||||||
|
manager.GetInstance().RefreshDLNA()
|
||||||
|
}
|
||||||
|
|
||||||
return makeConfigDLNAResult(), nil
|
return makeConfigDLNAResult(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ package api
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -12,7 +10,6 @@ import (
|
||||||
"github.com/stashapp/stash/internal/identify"
|
"github.com/stashapp/stash/internal/identify"
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -110,31 +107,10 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||||
// if download is true, then backup to temporary file and return a link
|
// if download is true, then backup to temporary file and return a link
|
||||||
download := input.Download != nil && *input.Download
|
download := input.Download != nil && *input.Download
|
||||||
mgr := manager.GetInstance()
|
mgr := manager.GetInstance()
|
||||||
database := mgr.Database
|
|
||||||
var backupPath string
|
|
||||||
if download {
|
|
||||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
|
|
||||||
}
|
|
||||||
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "backup*.sqlite")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
backupPath = f.Name()
|
backupPath, backupName, err := mgr.BackupDatabase(download)
|
||||||
f.Close()
|
|
||||||
} else {
|
|
||||||
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
|
|
||||||
if backupDirectoryPath != "" {
|
|
||||||
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
backupPath = database.DatabaseBackupPath(backupDirectoryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := database.Backup(backupPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Errorf("Error backing up database: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,8 +123,7 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||||
|
|
||||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||||
|
|
||||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
ret := baseURL + "/downloads/" + downloadHash + "/" + backupName
|
||||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
|
||||||
return &ret, nil
|
return &ret, nil
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("Successfully backed up database to: %s", backupPath)
|
logger.Infof("Successfully backed up database to: %s", backupPath)
|
||||||
|
|
@ -158,33 +133,11 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) {
|
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) {
|
||||||
// if download is true, then backup to temporary file and return a link
|
// if download is true, then save to temporary file and return a link
|
||||||
download := input.Download != nil && *input.Download
|
download := input.Download != nil && *input.Download
|
||||||
mgr := manager.GetInstance()
|
mgr := manager.GetInstance()
|
||||||
database := mgr.Database
|
|
||||||
var outPath string
|
|
||||||
if download {
|
|
||||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
|
|
||||||
}
|
|
||||||
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "anonymous*.sqlite")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
outPath = f.Name()
|
outPath, outName, err := mgr.AnonymiseDatabase(download)
|
||||||
f.Close()
|
|
||||||
} else {
|
|
||||||
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
|
|
||||||
if backupDirectoryPath != "" {
|
|
||||||
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outPath = database.AnonymousDatabasePath(backupDirectoryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := database.Anonymise(outPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error anonymising database: %v", err)
|
logger.Errorf("Error anonymising database: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -199,8 +152,7 @@ func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input Anonymis
|
||||||
|
|
||||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||||
|
|
||||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
ret := baseURL + "/downloads/" + downloadHash + "/" + outName
|
||||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
|
||||||
return &ret, nil
|
return &ret, nil
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("Successfully anonymised database to: %s", outPath)
|
logger.Infof("Successfully anonymised database to: %s", outPath)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
"github.com/stashapp/stash/internal/manager/task"
|
"github.com/stashapp/stash/internal/manager/task"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14,13 +13,9 @@ func refreshPackageType(typeArg PackageType) {
|
||||||
mgr := manager.GetInstance()
|
mgr := manager.GetInstance()
|
||||||
|
|
||||||
if typeArg == PackageTypePlugin {
|
if typeArg == PackageTypePlugin {
|
||||||
if err := mgr.PluginCache.LoadPlugins(); err != nil {
|
mgr.RefreshPluginCache()
|
||||||
logger.Errorf("Error reading plugin configs: %v", err)
|
|
||||||
}
|
|
||||||
} else if typeArg == PackageTypeScraper {
|
} else if typeArg == PackageTypeScraper {
|
||||||
if err := mgr.ScraperCache.ReloadScrapers(); err != nil {
|
mgr.RefreshScraperCache()
|
||||||
logger.Errorf("Error reading scraper configs: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
|
||||||
"github.com/stashapp/stash/pkg/plugin"
|
"github.com/stashapp/stash/pkg/plugin"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil"
|
"github.com/stashapp/stash/pkg/sliceutil"
|
||||||
)
|
)
|
||||||
|
|
@ -17,11 +16,7 @@ func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {
|
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {
|
||||||
err := manager.GetInstance().PluginCache.LoadPlugins()
|
manager.GetInstance().RefreshPluginCache()
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Error reading plugin configs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *mutationResolver) ReloadScrapers(ctx context.Context) (bool, error) {
|
func (r *mutationResolver) ReloadScrapers(ctx context.Context) (bool, error) {
|
||||||
err := manager.GetInstance().ScraperCache.ReloadScrapers()
|
manager.GetInstance().RefreshScraperCache()
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ import (
|
||||||
|
|
||||||
type downloadsRoutes struct{}
|
type downloadsRoutes struct{}
|
||||||
|
|
||||||
func getDownloadsRoutes() chi.Router {
|
|
||||||
return downloadsRoutes{}.Routes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs downloadsRoutes) Routes() chi.Router {
|
func (rs downloadsRoutes) Routes() chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,6 @@ type imageRoutes struct {
|
||||||
fileGetter models.FileGetter
|
fileGetter models.FileGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func getImageRoutes(repo models.Repository) chi.Router {
|
|
||||||
return imageRoutes{
|
|
||||||
routes: routes{txnManager: repo.TxnManager},
|
|
||||||
imageFinder: repo.Image,
|
|
||||||
fileGetter: repo.File,
|
|
||||||
}.Routes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs imageRoutes) Routes() chi.Router {
|
func (rs imageRoutes) Routes() chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|
@ -76,7 +68,7 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||||
Preset: manager.GetInstance().Config.GetPreviewPreset().String(),
|
Preset: manager.GetInstance().Config.GetPreviewPreset().String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMPEG, manager.GetInstance().FFProbe, clipPreviewOptions)
|
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMpeg, manager.GetInstance().FFProbe, clipPreviewOptions)
|
||||||
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// don't log for unsupported image format
|
// don't log for unsupported image format
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,6 @@ type movieRoutes struct {
|
||||||
movieFinder MovieFinder
|
movieFinder MovieFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMovieRoutes(repo models.Repository) chi.Router {
|
|
||||||
return movieRoutes{
|
|
||||||
routes: routes{txnManager: repo.TxnManager},
|
|
||||||
movieFinder: repo.Movie,
|
|
||||||
}.Routes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs movieRoutes) Routes() chi.Router {
|
func (rs movieRoutes) Routes() chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,6 @@ type performerRoutes struct {
|
||||||
performerFinder PerformerFinder
|
performerFinder PerformerFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPerformerRoutes(repo models.Repository) chi.Router {
|
|
||||||
return performerRoutes{
|
|
||||||
routes: routes{txnManager: repo.TxnManager},
|
|
||||||
performerFinder: repo.Performer,
|
|
||||||
}.Routes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs performerRoutes) Routes() chi.Router {
|
func (rs performerRoutes) Routes() chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,6 @@ type pluginRoutes struct {
|
||||||
pluginCache *plugin.Cache
|
pluginCache *plugin.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPluginRoutes(pluginCache *plugin.Cache) chi.Router {
|
|
||||||
return pluginRoutes{
|
|
||||||
pluginCache: pluginCache,
|
|
||||||
}.Routes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs pluginRoutes) Routes() chi.Router {
|
func (rs pluginRoutes) Routes() chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,17 +51,6 @@ type sceneRoutes struct {
|
||||||
tagFinder SceneMarkerTagFinder
|
tagFinder SceneMarkerTagFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSceneRoutes(repo models.Repository) chi.Router {
|
|
||||||
return sceneRoutes{
|
|
||||||
routes: routes{txnManager: repo.TxnManager},
|
|
||||||
sceneFinder: repo.Scene,
|
|
||||||
fileGetter: repo.File,
|
|
||||||
captionFinder: repo.File,
|
|
||||||
sceneMarkerFinder: repo.SceneMarker,
|
|
||||||
tagFinder: repo.Tag,
|
|
||||||
}.Routes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs sceneRoutes) Routes() chi.Router {
|
func (rs sceneRoutes) Routes() chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,6 @@ type studioRoutes struct {
|
||||||
studioFinder StudioFinder
|
studioFinder StudioFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStudioRoutes(repo models.Repository) chi.Router {
|
|
||||||
return studioRoutes{
|
|
||||||
routes: routes{txnManager: repo.TxnManager},
|
|
||||||
studioFinder: repo.Studio,
|
|
||||||
}.Routes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs studioRoutes) Routes() chi.Router {
|
func (rs studioRoutes) Routes() chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,6 @@ type tagRoutes struct {
|
||||||
tagFinder TagFinder
|
tagFinder TagFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTagRoutes(repo models.Repository) chi.Router {
|
|
||||||
return tagRoutes{
|
|
||||||
routes: routes{txnManager: repo.TxnManager},
|
|
||||||
tagFinder: repo.Tag,
|
|
||||||
}.Routes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs tagRoutes) Routes() chi.Router {
|
func (rs tagRoutes) Routes() chi.Router {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,25 +46,65 @@ const (
|
||||||
playgroundEndpoint = "/playground"
|
playgroundEndpoint = "/playground"
|
||||||
)
|
)
|
||||||
|
|
||||||
var uiBox = ui.UIBox
|
type Server struct {
|
||||||
var loginUIBox = ui.LoginUIBox
|
http.Server
|
||||||
|
displayAddress string
|
||||||
|
|
||||||
func Start() error {
|
manager *manager.Manager
|
||||||
c := config.GetInstance()
|
}
|
||||||
|
|
||||||
initCustomPerformerImages(c.GetCustomPerformerImageLocation())
|
// Called at startup
|
||||||
|
func Initialize() (*Server, error) {
|
||||||
|
mgr := manager.GetInstance()
|
||||||
|
cfg := mgr.Config
|
||||||
|
|
||||||
|
initCustomPerformerImages(cfg.GetCustomPerformerImageLocation())
|
||||||
|
|
||||||
|
displayHost := cfg.GetHost()
|
||||||
|
if displayHost == "0.0.0.0" {
|
||||||
|
displayHost = "localhost"
|
||||||
|
}
|
||||||
|
displayAddress := displayHost + ":" + strconv.Itoa(cfg.GetPort())
|
||||||
|
|
||||||
|
address := cfg.GetHost() + ":" + strconv.Itoa(cfg.GetPort())
|
||||||
|
tlsConfig, err := makeTLSConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
// assume we don't want to start with a broken TLS configuration
|
||||||
|
return nil, fmt.Errorf("error loading TLS config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsConfig != nil {
|
||||||
|
displayAddress = "https://" + displayAddress + "/"
|
||||||
|
} else {
|
||||||
|
displayAddress = "http://" + displayAddress + "/"
|
||||||
|
}
|
||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
server := &Server{
|
||||||
|
Server: http.Server{
|
||||||
|
Addr: address,
|
||||||
|
Handler: r,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
// disable http/2 support by default
|
||||||
|
// when http/2 is enabled, we are unable to hijack and close
|
||||||
|
// the connection/request. This is necessary to stop running
|
||||||
|
// streams when deleting a scene file.
|
||||||
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||||
|
},
|
||||||
|
displayAddress: displayAddress,
|
||||||
|
manager: mgr,
|
||||||
|
}
|
||||||
|
|
||||||
r.Use(middleware.Heartbeat("/healthz"))
|
r.Use(middleware.Heartbeat("/healthz"))
|
||||||
r.Use(cors.AllowAll().Handler)
|
r.Use(cors.AllowAll().Handler)
|
||||||
r.Use(authenticateHandler())
|
r.Use(authenticateHandler())
|
||||||
visitedPluginHandler := manager.GetInstance().SessionStore.VisitedPluginHandler()
|
visitedPluginHandler := mgr.SessionStore.VisitedPluginHandler()
|
||||||
r.Use(visitedPluginHandler)
|
r.Use(visitedPluginHandler)
|
||||||
|
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
if c.GetLogAccess() {
|
if cfg.GetLogAccess() {
|
||||||
httpLogger := httplog.NewLogger("Stash", httplog.Options{
|
httpLogger := httplog.NewLogger("Stash", httplog.Options{
|
||||||
Concise: true,
|
Concise: true,
|
||||||
})
|
})
|
||||||
|
|
@ -83,7 +123,7 @@ func Start() error {
|
||||||
return errors.New(message)
|
return errors.New(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := manager.GetInstance().Repository
|
repo := mgr.Repository
|
||||||
|
|
||||||
dataloaders := loaders.Middleware{
|
dataloaders := loaders.Middleware{
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
|
|
@ -91,10 +131,10 @@ func Start() error {
|
||||||
|
|
||||||
r.Use(dataloaders.Middleware)
|
r.Use(dataloaders.Middleware)
|
||||||
|
|
||||||
pluginCache := manager.GetInstance().PluginCache
|
pluginCache := mgr.PluginCache
|
||||||
sceneService := manager.GetInstance().SceneService
|
sceneService := mgr.SceneService
|
||||||
imageService := manager.GetInstance().ImageService
|
imageService := mgr.ImageService
|
||||||
galleryService := manager.GetInstance().GalleryService
|
galleryService := mgr.GalleryService
|
||||||
resolver := &Resolver{
|
resolver := &Resolver{
|
||||||
repository: repo,
|
repository: repo,
|
||||||
sceneService: sceneService,
|
sceneService: sceneService,
|
||||||
|
|
@ -117,7 +157,7 @@ func Start() error {
|
||||||
gqlSrv.AddTransport(gqlTransport.GET{})
|
gqlSrv.AddTransport(gqlTransport.GET{})
|
||||||
gqlSrv.AddTransport(gqlTransport.POST{})
|
gqlSrv.AddTransport(gqlTransport.POST{})
|
||||||
gqlSrv.AddTransport(gqlTransport.MultipartForm{
|
gqlSrv.AddTransport(gqlTransport.MultipartForm{
|
||||||
MaxUploadSize: c.GetMaxUploadSize(),
|
MaxUploadSize: cfg.GetMaxUploadSize(),
|
||||||
})
|
})
|
||||||
|
|
||||||
gqlSrv.SetQueryCache(gqlLru.New(1000))
|
gqlSrv.SetQueryCache(gqlLru.New(1000))
|
||||||
|
|
@ -134,7 +174,7 @@ func Start() error {
|
||||||
// chain the visited plugin handler
|
// chain the visited plugin handler
|
||||||
// also requires the dataloader middleware
|
// also requires the dataloader middleware
|
||||||
gqlHandler := visitedPluginHandler(dataloaders.Middleware(http.HandlerFunc(gqlHandlerFunc)))
|
gqlHandler := visitedPluginHandler(dataloaders.Middleware(http.HandlerFunc(gqlHandlerFunc)))
|
||||||
manager.GetInstance().PluginCache.RegisterGQLHandler(gqlHandler)
|
pluginCache.RegisterGQLHandler(gqlHandler)
|
||||||
|
|
||||||
r.HandleFunc(gqlEndpoint, gqlHandlerFunc)
|
r.HandleFunc(gqlEndpoint, gqlHandlerFunc)
|
||||||
r.HandleFunc(playgroundEndpoint, func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc(playgroundEndpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -143,23 +183,23 @@ func Start() error {
|
||||||
gqlPlayground.Handler("GraphQL playground", endpoint)(w, r)
|
gqlPlayground.Handler("GraphQL playground", endpoint)(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Mount("/performer", getPerformerRoutes(repo))
|
r.Mount("/performer", server.getPerformerRoutes())
|
||||||
r.Mount("/scene", getSceneRoutes(repo))
|
r.Mount("/scene", server.getSceneRoutes())
|
||||||
r.Mount("/image", getImageRoutes(repo))
|
r.Mount("/image", server.getImageRoutes())
|
||||||
r.Mount("/studio", getStudioRoutes(repo))
|
r.Mount("/studio", server.getStudioRoutes())
|
||||||
r.Mount("/movie", getMovieRoutes(repo))
|
r.Mount("/movie", server.getMovieRoutes())
|
||||||
r.Mount("/tag", getTagRoutes(repo))
|
r.Mount("/tag", server.getTagRoutes())
|
||||||
r.Mount("/downloads", getDownloadsRoutes())
|
r.Mount("/downloads", server.getDownloadsRoutes())
|
||||||
r.Mount("/plugin", getPluginRoutes(pluginCache))
|
r.Mount("/plugin", server.getPluginRoutes())
|
||||||
|
|
||||||
r.HandleFunc("/css", cssHandler(c))
|
r.HandleFunc("/css", cssHandler(cfg))
|
||||||
r.HandleFunc("/javascript", javascriptHandler(c))
|
r.HandleFunc("/javascript", javascriptHandler(cfg))
|
||||||
r.HandleFunc("/customlocales", customLocalesHandler(c))
|
r.HandleFunc("/customlocales", customLocalesHandler(cfg))
|
||||||
|
|
||||||
staticLoginUI := statigz.FileServer(loginUIBox.(fs.ReadDirFS))
|
staticLoginUI := statigz.FileServer(ui.LoginUIBox.(fs.ReadDirFS))
|
||||||
|
|
||||||
r.Get(loginEndpoint, handleLogin(loginUIBox))
|
r.Get(loginEndpoint, handleLogin())
|
||||||
r.Post(loginEndpoint, handleLoginPost(loginUIBox))
|
r.Post(loginEndpoint, handleLoginPost())
|
||||||
r.Get(logoutEndpoint, handleLogout())
|
r.Get(logoutEndpoint, handleLogout())
|
||||||
r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint)
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint)
|
||||||
|
|
@ -168,13 +208,13 @@ func Start() error {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Serve static folders
|
// Serve static folders
|
||||||
customServedFolders := c.GetCustomServedFolders()
|
customServedFolders := cfg.GetCustomServedFolders()
|
||||||
if customServedFolders != nil {
|
if customServedFolders != nil {
|
||||||
r.Mount("/custom", getCustomRoutes(customServedFolders))
|
r.Mount("/custom", getCustomRoutes(customServedFolders))
|
||||||
}
|
}
|
||||||
|
|
||||||
customUILocation := c.GetCustomUILocation()
|
customUILocation := cfg.GetCustomUILocation()
|
||||||
staticUI := statigz.FileServer(uiBox.(fs.ReadDirFS))
|
staticUI := statigz.FileServer(ui.UIBox.(fs.ReadDirFS))
|
||||||
|
|
||||||
// Serve the web app
|
// Serve the web app
|
||||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -190,8 +230,8 @@ func Start() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ext == ".html" || ext == "" {
|
if ext == ".html" || ext == "" {
|
||||||
themeColor := c.GetThemeColor()
|
themeColor := cfg.GetThemeColor()
|
||||||
data, err := fs.ReadFile(uiBox, "index.html")
|
data, err := fs.ReadFile(ui.UIBox, "index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
@ -217,51 +257,91 @@ func Start() error {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
displayHost := c.GetHost()
|
logger.Infof("stash version: %s", build.VersionString())
|
||||||
if displayHost == "0.0.0.0" {
|
|
||||||
displayHost = "localhost"
|
|
||||||
}
|
|
||||||
displayAddress := displayHost + ":" + strconv.Itoa(c.GetPort())
|
|
||||||
|
|
||||||
address := c.GetHost() + ":" + strconv.Itoa(c.GetPort())
|
|
||||||
tlsConfig, err := makeTLSConfig(c)
|
|
||||||
if err != nil {
|
|
||||||
// assume we don't want to start with a broken TLS configuration
|
|
||||||
panic(fmt.Errorf("error loading TLS config: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: address,
|
|
||||||
Handler: r,
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
// disable http/2 support by default
|
|
||||||
// when http/2 is enabled, we are unable to hijack and close
|
|
||||||
// the connection/request. This is necessary to stop running
|
|
||||||
// streams when deleting a scene file.
|
|
||||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("stash version: %s\n", build.VersionString())
|
|
||||||
go printLatestVersion(context.TODO())
|
go printLatestVersion(context.TODO())
|
||||||
logger.Infof("stash is listening on " + address)
|
|
||||||
if tlsConfig != nil {
|
return server, nil
|
||||||
displayAddress = "https://" + displayAddress + "/"
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
logger.Infof("stash is listening on " + s.Addr)
|
||||||
|
logger.Infof("stash is running at " + s.displayAddress)
|
||||||
|
|
||||||
|
if s.TLSConfig != nil {
|
||||||
|
return s.ListenAndServeTLS("", "")
|
||||||
} else {
|
} else {
|
||||||
displayAddress = "http://" + displayAddress + "/"
|
return s.ListenAndServe()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("stash is running at " + displayAddress)
|
func (s *Server) Shutdown() {
|
||||||
if tlsConfig != nil {
|
err := s.Server.Shutdown(context.TODO())
|
||||||
err = server.ListenAndServeTLS("", "")
|
if err != nil {
|
||||||
} else {
|
logger.Errorf("Error shutting down http server: %v", err)
|
||||||
err = server.ListenAndServe()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.Is(err, http.ErrServerClosed) {
|
func (s *Server) getPerformerRoutes() chi.Router {
|
||||||
return err
|
repo := s.manager.Repository
|
||||||
|
return performerRoutes{
|
||||||
|
routes: routes{txnManager: repo.TxnManager},
|
||||||
|
performerFinder: repo.Performer,
|
||||||
|
}.Routes()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
func (s *Server) getSceneRoutes() chi.Router {
|
||||||
|
repo := s.manager.Repository
|
||||||
|
return sceneRoutes{
|
||||||
|
routes: routes{txnManager: repo.TxnManager},
|
||||||
|
sceneFinder: repo.Scene,
|
||||||
|
fileGetter: repo.File,
|
||||||
|
captionFinder: repo.File,
|
||||||
|
sceneMarkerFinder: repo.SceneMarker,
|
||||||
|
tagFinder: repo.Tag,
|
||||||
|
}.Routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getImageRoutes() chi.Router {
|
||||||
|
repo := s.manager.Repository
|
||||||
|
return imageRoutes{
|
||||||
|
routes: routes{txnManager: repo.TxnManager},
|
||||||
|
imageFinder: repo.Image,
|
||||||
|
fileGetter: repo.File,
|
||||||
|
}.Routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getStudioRoutes() chi.Router {
|
||||||
|
repo := s.manager.Repository
|
||||||
|
return studioRoutes{
|
||||||
|
routes: routes{txnManager: repo.TxnManager},
|
||||||
|
studioFinder: repo.Studio,
|
||||||
|
}.Routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getMovieRoutes() chi.Router {
|
||||||
|
repo := s.manager.Repository
|
||||||
|
return movieRoutes{
|
||||||
|
routes: routes{txnManager: repo.TxnManager},
|
||||||
|
movieFinder: repo.Movie,
|
||||||
|
}.Routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getTagRoutes() chi.Router {
|
||||||
|
repo := s.manager.Repository
|
||||||
|
return tagRoutes{
|
||||||
|
routes: routes{txnManager: repo.TxnManager},
|
||||||
|
tagFinder: repo.Tag,
|
||||||
|
}.Routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getDownloadsRoutes() chi.Router {
|
||||||
|
return downloadsRoutes{}.Routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getPluginRoutes() chi.Router {
|
||||||
|
return pluginRoutes{
|
||||||
|
pluginCache: s.manager.PluginCache,
|
||||||
|
}.Routes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(w io.Writer, path string) error {
|
func copyFile(w io.Writer, path string) error {
|
||||||
|
|
@ -290,7 +370,7 @@ func serveFiles(w http.ResponseWriter, r *http.Request, paths []string) {
|
||||||
utils.ServeStaticContent(w, r, buffer.Bytes())
|
utils.ServeStaticContent(w, r, buffer.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func cssHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request) {
|
func cssHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var paths []string
|
var paths []string
|
||||||
|
|
||||||
|
|
@ -308,7 +388,7 @@ func cssHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func javascriptHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request) {
|
func javascriptHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var paths []string
|
var paths []string
|
||||||
|
|
||||||
|
|
@ -326,7 +406,7 @@ func javascriptHandler(c *config.Instance) func(w http.ResponseWriter, r *http.R
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func customLocalesHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request) {
|
func customLocalesHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
buffer := bytes.Buffer{}
|
buffer := bytes.Buffer{}
|
||||||
|
|
||||||
|
|
@ -351,7 +431,7 @@ func customLocalesHandler(c *config.Instance) func(w http.ResponseWriter, r *htt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
|
func makeTLSConfig(c *config.Config) (*tls.Config, error) {
|
||||||
c.InitTLS()
|
c.InitTLS()
|
||||||
certFile, keyFile := c.GetTLSFiles()
|
certFile, keyFile := c.GetTLSFiles()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,13 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/session"
|
"github.com/stashapp/stash/pkg/session"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
"github.com/stashapp/stash/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const returnURLParam = "returnURL"
|
const returnURLParam = "returnURL"
|
||||||
|
|
||||||
func getLoginPage(loginUIBox fs.FS) []byte {
|
func getLoginPage() []byte {
|
||||||
data, err := fs.ReadFile(loginUIBox, "login.html")
|
data, err := fs.ReadFile(ui.LoginUIBox, "login.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
@ -31,8 +32,8 @@ type loginTemplateData struct {
|
||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveLoginPage(loginUIBox fs.FS, w http.ResponseWriter, r *http.Request, returnURL string, loginError string) {
|
func serveLoginPage(w http.ResponseWriter, r *http.Request, returnURL string, loginError string) {
|
||||||
loginPage := string(getLoginPage(loginUIBox))
|
loginPage := string(getLoginPage())
|
||||||
prefix := getProxyPrefix(r)
|
prefix := getProxyPrefix(r)
|
||||||
loginPage = strings.ReplaceAll(loginPage, "/%BASE_URL%", prefix)
|
loginPage = strings.ReplaceAll(loginPage, "/%BASE_URL%", prefix)
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ func serveLoginPage(loginUIBox fs.FS, w http.ResponseWriter, r *http.Request, re
|
||||||
utils.ServeStaticContent(w, r, buffer.Bytes())
|
utils.ServeStaticContent(w, r, buffer.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLogin(loginUIBox fs.FS) http.HandlerFunc {
|
func handleLogin() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
returnURL := r.URL.Query().Get(returnURLParam)
|
returnURL := r.URL.Query().Get(returnURLParam)
|
||||||
|
|
||||||
|
|
@ -71,11 +72,11 @@ func handleLogin(loginUIBox fs.FS) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serveLoginPage(loginUIBox, w, r, returnURL, "")
|
serveLoginPage(w, r, returnURL, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLoginPost(loginUIBox fs.FS) http.HandlerFunc {
|
func handleLoginPost() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
url := r.FormValue(returnURLParam)
|
url := r.FormValue(returnURLParam)
|
||||||
if url == "" {
|
if url == "" {
|
||||||
|
|
@ -92,7 +93,7 @@ func handleLoginPost(loginUIBox fs.FS) http.HandlerFunc {
|
||||||
|
|
||||||
if errors.As(err, &invalidCredentialsError) {
|
if errors.As(err, &invalidCredentialsError) {
|
||||||
// serve login page with an error
|
// serve login page with an error
|
||||||
serveLoginPage(loginUIBox, w, r, url, "Username or password is invalid")
|
serveLoginPage(w, r, url, "Username or password is invalid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/sqlite"
|
"github.com/stashapp/stash/pkg/sqlite"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
|
|
@ -77,6 +78,9 @@ func runTests(m *testing.M) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
// initialise empty config - needed by some db migrations
|
||||||
|
_ = config.InitializeEmpty()
|
||||||
|
|
||||||
ret := runTests(m)
|
ret := runTests(m)
|
||||||
os.Exit(ret)
|
os.Exit(ret)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,6 @@ import (
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShutdownHandler interface {
|
|
||||||
Shutdown(code int)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FaviconProvider interface {
|
type FaviconProvider interface {
|
||||||
GetFavicon() []byte
|
GetFavicon() []byte
|
||||||
GetFaviconPng() []byte
|
GetFaviconPng() []byte
|
||||||
|
|
@ -27,7 +23,7 @@ type FaviconProvider interface {
|
||||||
|
|
||||||
// Start starts the desktop icon process. It blocks until the process exits.
|
// Start starts the desktop icon process. It blocks until the process exits.
|
||||||
// MUST be run on the main goroutine or will have no effect on macOS
|
// MUST be run on the main goroutine or will have no effect on macOS
|
||||||
func Start(shutdownHandler ShutdownHandler, faviconProvider FaviconProvider) {
|
func Start(exit chan<- int, faviconProvider FaviconProvider) {
|
||||||
if IsDesktop() {
|
if IsDesktop() {
|
||||||
hideConsole()
|
hideConsole()
|
||||||
|
|
||||||
|
|
@ -36,7 +32,7 @@ func Start(shutdownHandler ShutdownHandler, faviconProvider FaviconProvider) {
|
||||||
openURLInBrowser("")
|
openURLInBrowser("")
|
||||||
}
|
}
|
||||||
writeStashIcon(faviconProvider)
|
writeStashIcon(faviconProvider)
|
||||||
startSystray(shutdownHandler, faviconProvider)
|
startSystray(exit, faviconProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
package desktop
|
package desktop
|
||||||
|
|
||||||
func startSystray(shutdownHandler ShutdownHandler, favicon FaviconProvider) {
|
func startSystray(exit chan<- int, favicon FaviconProvider) {
|
||||||
// The systray is not available on Linux because the required libraries (libappindicator3 and gtk+3.0)
|
// The systray is not available on Linux because the required libraries (libappindicator3 and gtk+3.0)
|
||||||
// are not able to be statically compiled. Technically, the systray works perfectly fine when dynamically
|
// are not able to be statically compiled. Technically, the systray works perfectly fine when dynamically
|
||||||
// linked, but we cannot distribute it for compatibility reasons.
|
// linked, but we cannot distribute it for compatibility reasons.
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// MUST be run on the main goroutine or will have no effect on macOS
|
// MUST be run on the main goroutine or will have no effect on macOS
|
||||||
func startSystray(shutdownHandler ShutdownHandler, faviconProvider FaviconProvider) {
|
func startSystray(exit chan<- int, faviconProvider FaviconProvider) {
|
||||||
// Shows a small notification to inform that Stash will no longer show a terminal window,
|
// Shows a small notification to inform that Stash will no longer show a terminal window,
|
||||||
// and instead will be available in the tray. Will only show the first time a pre-desktop integration
|
// and instead will be available in the tray. Will only show the first time a pre-desktop integration
|
||||||
// system is started from a non-terminal method, e.g. double-clicking an icon.
|
// system is started from a non-terminal method, e.g. double-clicking an icon.
|
||||||
|
|
@ -39,12 +39,12 @@ func startSystray(shutdownHandler ShutdownHandler, faviconProvider FaviconProvid
|
||||||
|
|
||||||
for {
|
for {
|
||||||
systray.Run(func() {
|
systray.Run(func() {
|
||||||
systrayInitialize(shutdownHandler, faviconProvider)
|
systrayInitialize(exit, faviconProvider)
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func systrayInitialize(shutdownHandler ShutdownHandler, faviconProvider FaviconProvider) {
|
func systrayInitialize(exit chan<- int, faviconProvider FaviconProvider) {
|
||||||
favicon := faviconProvider.GetFavicon()
|
favicon := faviconProvider.GetFavicon()
|
||||||
systray.SetTemplateIcon(favicon, favicon)
|
systray.SetTemplateIcon(favicon, favicon)
|
||||||
systray.SetTooltip("🟢 Stash is Running.")
|
systray.SetTooltip("🟢 Stash is Running.")
|
||||||
|
|
@ -86,7 +86,7 @@ func systrayInitialize(shutdownHandler ShutdownHandler, faviconProvider FaviconP
|
||||||
openURLInBrowser("")
|
openURLInBrowser("")
|
||||||
case <-quitStashButton.ClickedCh:
|
case <-quitStashButton.ClickedCh:
|
||||||
systray.Quit()
|
systray.Quit()
|
||||||
shutdownHandler.Shutdown(0)
|
exit <- 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
// should be run with -race
|
// should be run with -race
|
||||||
func TestConcurrentConfigAccess(t *testing.T) {
|
func TestConcurrentConfigAccess(t *testing.T) {
|
||||||
i := GetInstance()
|
i := InitializeEmpty()
|
||||||
|
|
||||||
const workers = 8
|
const workers = 8
|
||||||
const loops = 200
|
const loops = 200
|
||||||
|
|
@ -16,13 +16,12 @@ func TestConcurrentConfigAccess(t *testing.T) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(wk int) {
|
go func(wk int) {
|
||||||
for l := 0; l < loops; l++ {
|
for l := 0; l < loops; l++ {
|
||||||
if err := i.SetInitialMemoryConfig(); err != nil {
|
if err := i.SetInitialConfig(); err != nil {
|
||||||
t.Errorf("Failure setting initial configuration in worker %v iteration %v: %v", wk, l, err)
|
t.Errorf("Failure setting initial configuration in worker %v iteration %v: %v", wk, l, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
i.HasCredentials()
|
i.HasCredentials()
|
||||||
i.ValidateCredentials("", "")
|
i.ValidateCredentials("", "")
|
||||||
i.GetCPUProfilePath()
|
|
||||||
i.GetConfigFile()
|
i.GetConfigFile()
|
||||||
i.GetConfigPath()
|
i.GetConfigPath()
|
||||||
i.GetDefaultDatabaseFilePath()
|
i.GetDefaultDatabaseFilePath()
|
||||||
|
|
|
||||||
|
|
@ -6,84 +6,107 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/build"
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
initOnce sync.Once
|
|
||||||
instanceOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
type flagStruct struct {
|
type flagStruct struct {
|
||||||
configFilePath string
|
configFilePath string
|
||||||
cpuProfilePath string
|
|
||||||
nobrowser bool
|
nobrowser bool
|
||||||
helpFlag bool
|
|
||||||
versionFlag bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInstance() *Instance {
|
var flags flagStruct
|
||||||
instanceOnce.Do(func() {
|
|
||||||
instance = &Instance{
|
func init() {
|
||||||
|
pflag.IP("host", net.IPv4(0, 0, 0, 0), "ip address for the host")
|
||||||
|
pflag.Int("port", 9999, "port to serve from")
|
||||||
|
pflag.StringVarP(&flags.configFilePath, "config", "c", "", "config file to use")
|
||||||
|
pflag.BoolVar(&flags.nobrowser, "nobrowser", false, "Don't open a browser window after launch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called at startup
|
||||||
|
func Initialize() (*Config, error) {
|
||||||
|
cfg := &Config{
|
||||||
main: viper.New(),
|
main: viper.New(),
|
||||||
overrides: viper.New(),
|
overrides: viper.New(),
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
cfg.initOverrides()
|
||||||
|
|
||||||
|
err := cfg.initConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.isNewSystem {
|
||||||
|
if cfg.Validate() == nil {
|
||||||
|
// system has been initialised by the environment
|
||||||
|
cfg.isNewSystem = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.isNewSystem {
|
||||||
|
cfg.setExistingSystemDefaults()
|
||||||
|
|
||||||
|
err := cfg.SetInitialConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cfg.Write()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cfg.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = cfg
|
||||||
|
return instance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by tests to initialize an empty config
|
||||||
|
func InitializeEmpty() *Config {
|
||||||
|
cfg := &Config{
|
||||||
|
main: viper.New(),
|
||||||
|
overrides: viper.New(),
|
||||||
|
}
|
||||||
|
instance = cfg
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
func Initialize() (*Instance, error) {
|
func bindEnv(v *viper.Viper, key string) {
|
||||||
var err error
|
if err := v.BindEnv(key); err != nil {
|
||||||
initOnce.Do(func() {
|
panic(fmt.Sprintf("unable to set environment key (%v): %v", key, err))
|
||||||
flags := initFlags()
|
|
||||||
|
|
||||||
if flags.helpFlag {
|
|
||||||
pflag.Usage()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if flags.versionFlag {
|
|
||||||
fmt.Printf(build.VersionString() + "\n")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
overrides := makeOverrideConfig()
|
|
||||||
|
|
||||||
_ = GetInstance()
|
|
||||||
instance.overrides = overrides
|
|
||||||
instance.cpuProfilePath = flags.cpuProfilePath
|
|
||||||
// instance.configUpdates = make(chan int)
|
|
||||||
|
|
||||||
if err = initConfig(instance, flags); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if instance.isNewSystem {
|
|
||||||
if instance.Validate() == nil {
|
|
||||||
// system has been initialised by the environment
|
|
||||||
instance.isNewSystem = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !instance.isNewSystem {
|
func (i *Config) initOverrides() {
|
||||||
err = instance.setExistingSystemDefaults()
|
v := i.overrides
|
||||||
if err == nil {
|
|
||||||
err = instance.SetInitialConfig()
|
if err := v.BindPFlags(pflag.CommandLine); err != nil {
|
||||||
}
|
logger.Infof("failed to bind flags: %v", err)
|
||||||
}
|
|
||||||
})
|
|
||||||
return instance, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig(instance *Instance, flags flagStruct) error {
|
v.SetEnvPrefix("stash") // will be uppercased automatically
|
||||||
v := instance.main
|
bindEnv(v, "host") // STASH_HOST
|
||||||
|
bindEnv(v, "port") // STASH_PORT
|
||||||
|
bindEnv(v, "external_host") // STASH_EXTERNAL_HOST
|
||||||
|
bindEnv(v, "generated") // STASH_GENERATED
|
||||||
|
bindEnv(v, "metadata") // STASH_METADATA
|
||||||
|
bindEnv(v, "cache") // STASH_CACHE
|
||||||
|
bindEnv(v, "stash") // STASH_STASH
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Config) initConfig() error {
|
||||||
|
v := i.main
|
||||||
|
|
||||||
// The config file is called config. Leave off the file extension.
|
// The config file is called config. Leave off the file extension.
|
||||||
v.SetConfigName("config")
|
v.SetConfigName("config")
|
||||||
|
|
@ -105,11 +128,11 @@ func initConfig(instance *Instance, flags flagStruct) error {
|
||||||
|
|
||||||
// if file does not exist, assume it is a new system
|
// if file does not exist, assume it is a new system
|
||||||
if exists, _ := fsutil.FileExists(configFile); !exists {
|
if exists, _ := fsutil.FileExists(configFile); !exists {
|
||||||
instance.isNewSystem = true
|
i.isNewSystem = true
|
||||||
|
|
||||||
// ensure we can write to the file
|
// ensure we can write to the file
|
||||||
if err := fsutil.Touch(configFile); err != nil {
|
if err := fsutil.Touch(configFile); err != nil {
|
||||||
return fmt.Errorf(`could not write to provided config path "%s": %s`, configFile, err.Error())
|
return fmt.Errorf(`could not write to provided config path "%s": %v`, configFile, err)
|
||||||
} else {
|
} else {
|
||||||
// remove the file
|
// remove the file
|
||||||
os.Remove(configFile)
|
os.Remove(configFile)
|
||||||
|
|
@ -123,7 +146,7 @@ func initConfig(instance *Instance, flags flagStruct) error {
|
||||||
// if not found, assume its a new system
|
// if not found, assume its a new system
|
||||||
var notFoundErr viper.ConfigFileNotFoundError
|
var notFoundErr viper.ConfigFileNotFoundError
|
||||||
if errors.As(err, ¬FoundErr) {
|
if errors.As(err, ¬FoundErr) {
|
||||||
instance.isNewSystem = true
|
i.isNewSystem = true
|
||||||
return nil
|
return nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -131,48 +154,3 @@ func initConfig(instance *Instance, flags flagStruct) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initFlags() flagStruct {
|
|
||||||
flags := flagStruct{}
|
|
||||||
|
|
||||||
pflag.IP("host", net.IPv4(0, 0, 0, 0), "ip address for the host")
|
|
||||||
pflag.Int("port", 9999, "port to serve from")
|
|
||||||
pflag.StringVarP(&flags.configFilePath, "config", "c", "", "config file to use")
|
|
||||||
pflag.StringVar(&flags.cpuProfilePath, "cpuprofile", "", "write cpu profile to file")
|
|
||||||
pflag.BoolVar(&flags.nobrowser, "nobrowser", false, "Don't open a browser window after launch")
|
|
||||||
pflag.BoolVarP(&flags.helpFlag, "help", "h", false, "show this help text and exit")
|
|
||||||
pflag.BoolVarP(&flags.versionFlag, "version", "v", false, "show version number and exit")
|
|
||||||
|
|
||||||
pflag.Parse()
|
|
||||||
|
|
||||||
return flags
|
|
||||||
}
|
|
||||||
|
|
||||||
func initEnvs(viper *viper.Viper) {
|
|
||||||
viper.SetEnvPrefix("stash") // will be uppercased automatically
|
|
||||||
bindEnv(viper, "host") // STASH_HOST
|
|
||||||
bindEnv(viper, "port") // STASH_PORT
|
|
||||||
bindEnv(viper, "external_host") // STASH_EXTERNAL_HOST
|
|
||||||
bindEnv(viper, "generated") // STASH_GENERATED
|
|
||||||
bindEnv(viper, "metadata") // STASH_METADATA
|
|
||||||
bindEnv(viper, "cache") // STASH_CACHE
|
|
||||||
bindEnv(viper, "stash") // STASH_STASH
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindEnv(viper *viper.Viper, key string) {
|
|
||||||
if err := viper.BindEnv(key); err != nil {
|
|
||||||
panic(fmt.Sprintf("unable to set environment key (%v): %v", key, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeOverrideConfig() *viper.Viper {
|
|
||||||
viper := viper.New()
|
|
||||||
|
|
||||||
if err := viper.BindPFlags(pflag.CommandLine); err != nil {
|
|
||||||
logger.Infof("failed to bind flags: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
initEnvs(viper)
|
|
||||||
|
|
||||||
return viper
|
|
||||||
}
|
|
||||||
|
|
|
||||||
50
internal/manager/enums.go
Normal file
50
internal/manager/enums.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SystemStatusEnum string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SystemStatusEnumSetup SystemStatusEnum = "SETUP"
|
||||||
|
SystemStatusEnumNeedsMigration SystemStatusEnum = "NEEDS_MIGRATION"
|
||||||
|
SystemStatusEnumOk SystemStatusEnum = "OK"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllSystemStatusEnum = []SystemStatusEnum{
|
||||||
|
SystemStatusEnumSetup,
|
||||||
|
SystemStatusEnumNeedsMigration,
|
||||||
|
SystemStatusEnumOk,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e SystemStatusEnum) IsValid() bool {
|
||||||
|
switch e {
|
||||||
|
case SystemStatusEnumSetup, SystemStatusEnumNeedsMigration, SystemStatusEnumOk:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e SystemStatusEnum) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SystemStatusEnum) UnmarshalGQL(v interface{}) error {
|
||||||
|
str, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enums must be strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = SystemStatusEnum(str)
|
||||||
|
if !e.IsValid() {
|
||||||
|
return fmt.Errorf("%s is not a valid SystemStatusEnum", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e SystemStatusEnum) MarshalGQL(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type fingerprintCalculator struct {
|
type fingerprintCalculator struct {
|
||||||
Config *config.Instance
|
Config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fingerprintCalculator) calculateOshash(f *models.BaseFile, o file.Opener) (*models.Fingerprint, error) {
|
func (c *fingerprintCalculator) calculateOshash(f *models.BaseFile, o file.Opener) (*models.Fingerprint, error) {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func (g *generatorInfo) calculateFrameRate(videoStream *ffmpeg.FFProbeStream) er
|
||||||
|
|
||||||
// If we are missing the frame count or frame rate then seek through the file and extract the info with regex
|
// If we are missing the frame count or frame rate then seek through the file and extract the info with regex
|
||||||
if numberOfFrames == 0 || !isValidFloat64(framerate) {
|
if numberOfFrames == 0 || !isValidFloat64(framerate) {
|
||||||
info, err := instance.FFMPEG.CalculateFrameRate(context.TODO(), &g.VideoFile)
|
info, err := instance.FFMpeg.CalculateFrameRate(context.TODO(), &g.VideoFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("error calculating frame rate: %v", err)
|
logger.Errorf("error calculating frame rate: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO
|
||||||
SlowSeek: slowSeek,
|
SlowSeek: slowSeek,
|
||||||
Columns: cols,
|
Columns: cols,
|
||||||
g: &generate.Generator{
|
g: &generate.Generator{
|
||||||
Encoder: instance.FFMPEG,
|
Encoder: instance.FFMpeg,
|
||||||
FFMpegConfig: instance.Config,
|
FFMpegConfig: instance.Config,
|
||||||
LockManager: instance.ReadLockManager,
|
LockManager: instance.ReadLockManager,
|
||||||
ScenePaths: instance.Paths.Scene,
|
ScenePaths: instance.Paths.Scene,
|
||||||
|
|
|
||||||
308
internal/manager/init.go
Normal file
308
internal/manager/init.go
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/internal/desktop"
|
||||||
|
"github.com/stashapp/stash/internal/dlna"
|
||||||
|
"github.com/stashapp/stash/internal/log"
|
||||||
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
|
"github.com/stashapp/stash/pkg/gallery"
|
||||||
|
"github.com/stashapp/stash/pkg/image"
|
||||||
|
"github.com/stashapp/stash/pkg/job"
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models/paths"
|
||||||
|
"github.com/stashapp/stash/pkg/pkg"
|
||||||
|
"github.com/stashapp/stash/pkg/plugin"
|
||||||
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
|
"github.com/stashapp/stash/pkg/scraper"
|
||||||
|
"github.com/stashapp/stash/pkg/session"
|
||||||
|
"github.com/stashapp/stash/pkg/sqlite"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
"github.com/stashapp/stash/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Called at startup
|
||||||
|
func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
db := sqlite.NewDatabase()
|
||||||
|
repo := db.Repository()
|
||||||
|
|
||||||
|
// start with empty paths
|
||||||
|
mgrPaths := &paths.Paths{}
|
||||||
|
|
||||||
|
scraperRepository := scraper.NewRepository(repo)
|
||||||
|
scraperCache := scraper.NewCache(cfg, scraperRepository)
|
||||||
|
|
||||||
|
pluginCache := plugin.NewCache(cfg)
|
||||||
|
|
||||||
|
sceneService := &scene.Service{
|
||||||
|
File: db.File,
|
||||||
|
Repository: db.Scene,
|
||||||
|
MarkerRepository: db.SceneMarker,
|
||||||
|
PluginCache: pluginCache,
|
||||||
|
Paths: mgrPaths,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageService := &image.Service{
|
||||||
|
File: db.File,
|
||||||
|
Repository: db.Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryService := &gallery.Service{
|
||||||
|
Repository: db.Gallery,
|
||||||
|
ImageFinder: db.Image,
|
||||||
|
ImageService: imageService,
|
||||||
|
File: db.File,
|
||||||
|
Folder: db.Folder,
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneServer := &SceneServer{
|
||||||
|
TxnManager: repo.TxnManager,
|
||||||
|
SceneCoverGetter: repo.Scene,
|
||||||
|
}
|
||||||
|
|
||||||
|
dlnaRepository := dlna.NewRepository(repo)
|
||||||
|
dlnaService := dlna.NewService(dlnaRepository, cfg, sceneServer)
|
||||||
|
|
||||||
|
mgr := &Manager{
|
||||||
|
Config: cfg,
|
||||||
|
Logger: l,
|
||||||
|
|
||||||
|
Paths: mgrPaths,
|
||||||
|
|
||||||
|
JobManager: initJobManager(cfg),
|
||||||
|
ReadLockManager: fsutil.NewReadLockManager(),
|
||||||
|
|
||||||
|
DownloadStore: NewDownloadStore(),
|
||||||
|
|
||||||
|
PluginCache: pluginCache,
|
||||||
|
ScraperCache: scraperCache,
|
||||||
|
|
||||||
|
DLNAService: dlnaService,
|
||||||
|
|
||||||
|
Database: db,
|
||||||
|
Repository: repo,
|
||||||
|
|
||||||
|
SceneService: sceneService,
|
||||||
|
ImageService: imageService,
|
||||||
|
GalleryService: galleryService,
|
||||||
|
|
||||||
|
scanSubs: &subscriptionManager{},
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.RefreshPluginSourceManager()
|
||||||
|
mgr.RefreshScraperSourceManager()
|
||||||
|
|
||||||
|
if !cfg.IsNewSystem() {
|
||||||
|
logger.Infof("using config file: %s", cfg.GetConfigFile())
|
||||||
|
|
||||||
|
err := cfg.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.postInit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.checkSecurityTripwire()
|
||||||
|
} else {
|
||||||
|
cfgFile := cfg.GetConfigFile()
|
||||||
|
if cfgFile != "" {
|
||||||
|
cfgFile += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// create temporary session store - this will be re-initialised
|
||||||
|
// after config is complete
|
||||||
|
mgr.SessionStore = session.NewStore(cfg)
|
||||||
|
|
||||||
|
logger.Warnf("config file %snot found. Assuming new system...", cfgFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = mgr
|
||||||
|
return mgr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialisePackageManager(localPath string, srcPathGetter pkg.SourcePathGetter) *pkg.Manager {
|
||||||
|
const timeout = 10 * time.Second
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
},
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pkg.Manager{
|
||||||
|
Local: &pkg.Store{
|
||||||
|
BaseDir: localPath,
|
||||||
|
ManifestFile: pkg.ManifestFile,
|
||||||
|
},
|
||||||
|
PackagePathGetter: srcPathGetter,
|
||||||
|
Client: httpClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDuration(t time.Duration) string {
|
||||||
|
return fmt.Sprintf("%02.f:%02.f:%02.f", t.Hours(), t.Minutes(), t.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
func initJobManager(cfg *config.Config) *job.Manager {
|
||||||
|
ret := job.NewManager()
|
||||||
|
|
||||||
|
// desktop notifications
|
||||||
|
ctx := context.Background()
|
||||||
|
c := ret.Subscribe(context.Background())
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case j := <-c.RemovedJob:
|
||||||
|
if cfg.GetNotificationsEnabled() {
|
||||||
|
cleanDesc := strings.TrimRight(j.Description, ".")
|
||||||
|
|
||||||
|
if j.StartTime == nil {
|
||||||
|
// Task was never started
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timeElapsed := j.EndTime.Sub(*j.StartTime)
|
||||||
|
msg := fmt.Sprintf("Task \"%s\" is finished in %s.", cleanDesc, formatDuration(timeElapsed))
|
||||||
|
desktop.SendNotification("Task Finished", msg)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// postInit initialises the paths, caches and database after the initial
|
||||||
|
// configuration has been set. Should only be called if the configuration
|
||||||
|
// is valid.
|
||||||
|
func (s *Manager) postInit(ctx context.Context) error {
|
||||||
|
s.RefreshConfig()
|
||||||
|
|
||||||
|
s.SessionStore = session.NewStore(s.Config)
|
||||||
|
s.PluginCache.RegisterSessionStore(s.SessionStore)
|
||||||
|
|
||||||
|
s.RefreshPluginCache()
|
||||||
|
s.RefreshScraperCache()
|
||||||
|
s.RefreshStreamManager()
|
||||||
|
s.RefreshDLNA()
|
||||||
|
|
||||||
|
s.SetBlobStoreOptions()
|
||||||
|
|
||||||
|
s.writeStashIcon()
|
||||||
|
|
||||||
|
// clear the downloads and tmp directories
|
||||||
|
// #1021 - only clear these directories if the generated folder is non-empty
|
||||||
|
if s.Config.GetGeneratedPath() != "" {
|
||||||
|
const deleteTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
utils.Timeout(func() {
|
||||||
|
if err := fsutil.EmptyDir(s.Paths.Generated.Downloads); err != nil {
|
||||||
|
logger.Warnf("could not empty downloads directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := fsutil.EnsureDir(s.Paths.Generated.Tmp); err != nil {
|
||||||
|
logger.Warnf("could not create temporary directory: %v", err)
|
||||||
|
} else {
|
||||||
|
if err := fsutil.EmptyDir(s.Paths.Generated.Tmp); err != nil {
|
||||||
|
logger.Warnf("could not empty temporary directory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deleteTimeout, func(done chan struct{}) {
|
||||||
|
logger.Info("Please wait. Deleting temporary files...") // print
|
||||||
|
<-done // and wait for deletion
|
||||||
|
logger.Info("Temporary files deleted.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Database.Open(s.Config.GetDatabasePath()); err != nil {
|
||||||
|
var migrationNeededErr *sqlite.MigrationNeededError
|
||||||
|
if errors.As(err, &migrationNeededErr) {
|
||||||
|
logger.Warn(err)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the proxy if defined in config
|
||||||
|
if s.Config.GetProxy() != "" {
|
||||||
|
os.Setenv("HTTP_PROXY", s.Config.GetProxy())
|
||||||
|
os.Setenv("HTTPS_PROXY", s.Config.GetProxy())
|
||||||
|
os.Setenv("NO_PROXY", s.Config.GetNoProxy())
|
||||||
|
logger.Info("Using HTTP proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.initFFmpeg(ctx); err != nil {
|
||||||
|
return fmt.Errorf("error initializing FFmpeg subsystem: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Manager) checkSecurityTripwire() {
|
||||||
|
if err := session.CheckExternalAccessTripwire(s.Config); err != nil {
|
||||||
|
session.LogExternalAccessError(*err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Manager) writeStashIcon() {
|
||||||
|
iconPath := filepath.Join(s.Config.GetConfigPath(), "icon.png")
|
||||||
|
err := os.WriteFile(iconPath, ui.FaviconProvider.GetFaviconPng(), 0644)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Couldn't write icon file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Manager) initFFmpeg(ctx context.Context) error {
|
||||||
|
// use same directory as config path
|
||||||
|
configDirectory := s.Config.GetConfigPath()
|
||||||
|
paths := []string{
|
||||||
|
configDirectory,
|
||||||
|
paths.GetStashHomeDirectory(),
|
||||||
|
}
|
||||||
|
ffmpegPath, ffprobePath := ffmpeg.GetPaths(paths)
|
||||||
|
|
||||||
|
if ffmpegPath == "" || ffprobePath == "" {
|
||||||
|
logger.Infof("couldn't find FFmpeg, attempting to download it")
|
||||||
|
if err := ffmpeg.Download(ctx, configDirectory); err != nil {
|
||||||
|
path, absErr := filepath.Abs(configDirectory)
|
||||||
|
if absErr != nil {
|
||||||
|
path = configDirectory
|
||||||
|
}
|
||||||
|
msg := `Unable to automatically download FFmpeg
|
||||||
|
|
||||||
|
Check the readme for download links.
|
||||||
|
The ffmpeg and ffprobe binaries should be placed in %s.
|
||||||
|
|
||||||
|
`
|
||||||
|
logger.Errorf(msg, path)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
// After download get new paths for ffmpeg and ffprobe
|
||||||
|
ffmpegPath, ffprobePath = ffmpeg.GetPaths(paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath)
|
||||||
|
s.FFProbe = ffmpeg.FFProbe(ffprobePath)
|
||||||
|
|
||||||
|
s.FFMpeg.InitHWSupport(ctx)
|
||||||
|
s.RefreshStreamManager()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -4,139 +4,51 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/desktop"
|
|
||||||
"github.com/stashapp/stash/internal/dlna"
|
"github.com/stashapp/stash/internal/dlna"
|
||||||
"github.com/stashapp/stash/internal/log"
|
"github.com/stashapp/stash/internal/log"
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
file_image "github.com/stashapp/stash/pkg/file/image"
|
|
||||||
"github.com/stashapp/stash/pkg/file/video"
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/gallery"
|
|
||||||
"github.com/stashapp/stash/pkg/image"
|
|
||||||
"github.com/stashapp/stash/pkg/job"
|
"github.com/stashapp/stash/pkg/job"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/paths"
|
"github.com/stashapp/stash/pkg/models/paths"
|
||||||
"github.com/stashapp/stash/pkg/pkg"
|
"github.com/stashapp/stash/pkg/pkg"
|
||||||
"github.com/stashapp/stash/pkg/plugin"
|
"github.com/stashapp/stash/pkg/plugin"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
|
||||||
"github.com/stashapp/stash/pkg/scraper"
|
"github.com/stashapp/stash/pkg/scraper"
|
||||||
"github.com/stashapp/stash/pkg/session"
|
"github.com/stashapp/stash/pkg/session"
|
||||||
"github.com/stashapp/stash/pkg/sqlite"
|
"github.com/stashapp/stash/pkg/sqlite"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
|
||||||
"github.com/stashapp/stash/ui"
|
|
||||||
|
|
||||||
// register custom migrations
|
// register custom migrations
|
||||||
_ "github.com/stashapp/stash/pkg/sqlite/migrations"
|
_ "github.com/stashapp/stash/pkg/sqlite/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemStatus struct {
|
|
||||||
DatabaseSchema *int `json:"databaseSchema"`
|
|
||||||
DatabasePath *string `json:"databasePath"`
|
|
||||||
ConfigPath *string `json:"configPath"`
|
|
||||||
AppSchema int `json:"appSchema"`
|
|
||||||
Status SystemStatusEnum `json:"status"`
|
|
||||||
Os string `json:"os"`
|
|
||||||
WorkingDir string `json:"working_dir"`
|
|
||||||
HomeDir string `json:"home_dir"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SystemStatusEnum string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SystemStatusEnumSetup SystemStatusEnum = "SETUP"
|
|
||||||
SystemStatusEnumNeedsMigration SystemStatusEnum = "NEEDS_MIGRATION"
|
|
||||||
SystemStatusEnumOk SystemStatusEnum = "OK"
|
|
||||||
)
|
|
||||||
|
|
||||||
var AllSystemStatusEnum = []SystemStatusEnum{
|
|
||||||
SystemStatusEnumSetup,
|
|
||||||
SystemStatusEnumNeedsMigration,
|
|
||||||
SystemStatusEnumOk,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e SystemStatusEnum) IsValid() bool {
|
|
||||||
switch e {
|
|
||||||
case SystemStatusEnumSetup, SystemStatusEnumNeedsMigration, SystemStatusEnumOk:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e SystemStatusEnum) String() string {
|
|
||||||
return string(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *SystemStatusEnum) UnmarshalGQL(v interface{}) error {
|
|
||||||
str, ok := v.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("enums must be strings")
|
|
||||||
}
|
|
||||||
|
|
||||||
*e = SystemStatusEnum(str)
|
|
||||||
if !e.IsValid() {
|
|
||||||
return fmt.Errorf("%s is not a valid SystemStatusEnum", str)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e SystemStatusEnum) MarshalGQL(w io.Writer) {
|
|
||||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetupInput struct {
|
|
||||||
// Empty to indicate $HOME/.stash/config.yml default
|
|
||||||
ConfigLocation string `json:"configLocation"`
|
|
||||||
Stashes []*config.StashConfigInput `json:"stashes"`
|
|
||||||
// Empty to indicate default
|
|
||||||
DatabaseFile string `json:"databaseFile"`
|
|
||||||
// Empty to indicate default
|
|
||||||
GeneratedLocation string `json:"generatedLocation"`
|
|
||||||
// Empty to indicate default
|
|
||||||
CacheLocation string `json:"cacheLocation"`
|
|
||||||
|
|
||||||
StoreBlobsInDatabase bool `json:"storeBlobsInDatabase"`
|
|
||||||
// Empty to indicate default
|
|
||||||
BlobsLocation string `json:"blobsLocation"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
Config *config.Instance
|
Config *config.Config
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
|
|
||||||
Paths *paths.Paths
|
Paths *paths.Paths
|
||||||
|
|
||||||
FFMPEG *ffmpeg.FFMpeg
|
FFMpeg *ffmpeg.FFMpeg
|
||||||
FFProbe ffmpeg.FFProbe
|
FFProbe ffmpeg.FFProbe
|
||||||
StreamManager *ffmpeg.StreamManager
|
StreamManager *ffmpeg.StreamManager
|
||||||
|
|
||||||
|
JobManager *job.Manager
|
||||||
ReadLockManager *fsutil.ReadLockManager
|
ReadLockManager *fsutil.ReadLockManager
|
||||||
|
|
||||||
|
DownloadStore *DownloadStore
|
||||||
SessionStore *session.Store
|
SessionStore *session.Store
|
||||||
|
|
||||||
JobManager *job.Manager
|
|
||||||
|
|
||||||
PluginCache *plugin.Cache
|
PluginCache *plugin.Cache
|
||||||
ScraperCache *scraper.Cache
|
ScraperCache *scraper.Cache
|
||||||
|
|
||||||
PluginPackageManager *pkg.Manager
|
PluginPackageManager *pkg.Manager
|
||||||
ScraperPackageManager *pkg.Manager
|
ScraperPackageManager *pkg.Manager
|
||||||
|
|
||||||
DownloadStore *DownloadStore
|
|
||||||
|
|
||||||
DLNAService *dlna.Service
|
DLNAService *dlna.Service
|
||||||
|
|
||||||
Database *sqlite.Database
|
Database *sqlite.Database
|
||||||
|
|
@ -146,378 +58,18 @@ type Manager struct {
|
||||||
ImageService ImageService
|
ImageService ImageService
|
||||||
GalleryService GalleryService
|
GalleryService GalleryService
|
||||||
|
|
||||||
Scanner *file.Scanner
|
|
||||||
Cleaner *file.Cleaner
|
|
||||||
|
|
||||||
scanSubs *subscriptionManager
|
scanSubs *subscriptionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance *Manager
|
var instance *Manager
|
||||||
var once sync.Once
|
|
||||||
|
|
||||||
func GetInstance() *Manager {
|
func GetInstance() *Manager {
|
||||||
if _, err := Initialize(); err != nil {
|
if instance == nil {
|
||||||
panic(err)
|
panic("manager not initialized")
|
||||||
}
|
}
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
func Initialize() (*Manager, error) {
|
|
||||||
var err error
|
|
||||||
once.Do(func() {
|
|
||||||
err = initialize()
|
|
||||||
})
|
|
||||||
|
|
||||||
return instance, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func initialize() error {
|
|
||||||
ctx := context.TODO()
|
|
||||||
cfg, err := config.Initialize()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initializing configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l := initLog()
|
|
||||||
initProfiling(cfg.GetCPUProfilePath())
|
|
||||||
|
|
||||||
db := sqlite.NewDatabase()
|
|
||||||
repo := db.Repository()
|
|
||||||
|
|
||||||
// start with empty paths
|
|
||||||
emptyPaths := paths.Paths{}
|
|
||||||
|
|
||||||
instance = &Manager{
|
|
||||||
Config: cfg,
|
|
||||||
Logger: l,
|
|
||||||
ReadLockManager: fsutil.NewReadLockManager(),
|
|
||||||
DownloadStore: NewDownloadStore(),
|
|
||||||
PluginCache: plugin.NewCache(cfg),
|
|
||||||
|
|
||||||
Database: db,
|
|
||||||
Repository: repo,
|
|
||||||
Paths: &emptyPaths,
|
|
||||||
|
|
||||||
scanSubs: &subscriptionManager{},
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.SceneService = &scene.Service{
|
|
||||||
File: repo.File,
|
|
||||||
Repository: repo.Scene,
|
|
||||||
MarkerRepository: repo.SceneMarker,
|
|
||||||
PluginCache: instance.PluginCache,
|
|
||||||
Paths: instance.Paths,
|
|
||||||
Config: cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.ImageService = &image.Service{
|
|
||||||
File: repo.File,
|
|
||||||
Repository: repo.Image,
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.GalleryService = &gallery.Service{
|
|
||||||
Repository: repo.Gallery,
|
|
||||||
ImageFinder: repo.Image,
|
|
||||||
ImageService: instance.ImageService,
|
|
||||||
File: repo.File,
|
|
||||||
Folder: repo.Folder,
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.JobManager = initJobManager()
|
|
||||||
|
|
||||||
sceneServer := SceneServer{
|
|
||||||
TxnManager: repo.TxnManager,
|
|
||||||
SceneCoverGetter: repo.Scene,
|
|
||||||
}
|
|
||||||
|
|
||||||
dlnaRepository := dlna.NewRepository(repo)
|
|
||||||
instance.DLNAService = dlna.NewService(dlnaRepository, cfg, &sceneServer)
|
|
||||||
|
|
||||||
instance.RefreshPluginSourceManager()
|
|
||||||
instance.RefreshScraperSourceManager()
|
|
||||||
|
|
||||||
if !cfg.IsNewSystem() {
|
|
||||||
logger.Infof("using config file: %s", cfg.GetConfigFile())
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
err = cfg.Validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error initializing configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := instance.PostInit(ctx); err != nil {
|
|
||||||
var migrationNeededErr *sqlite.MigrationNeededError
|
|
||||||
if errors.As(err, &migrationNeededErr) {
|
|
||||||
logger.Warn(err.Error())
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initSecurity(cfg)
|
|
||||||
} else {
|
|
||||||
cfgFile := cfg.GetConfigFile()
|
|
||||||
if cfgFile != "" {
|
|
||||||
cfgFile += " "
|
|
||||||
}
|
|
||||||
|
|
||||||
// create temporary session store - this will be re-initialised
|
|
||||||
// after config is complete
|
|
||||||
instance.SessionStore = session.NewStore(cfg)
|
|
||||||
|
|
||||||
logger.Warnf("config file %snot found. Assuming new system...", cfgFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = initFFMPEG(ctx); err != nil {
|
|
||||||
logger.Warnf("could not initialize FFMPEG subsystem: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.Scanner = makeScanner(repo, instance.PluginCache)
|
|
||||||
instance.Cleaner = makeCleaner(repo, instance.PluginCache)
|
|
||||||
|
|
||||||
// if DLNA is enabled, start it now
|
|
||||||
if instance.Config.GetDLNADefaultEnabled() {
|
|
||||||
if err := instance.DLNAService.Start(nil); err != nil {
|
|
||||||
logger.Warnf("could not start DLNA service: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initialisePackageManager(localPath string, srcPathGetter pkg.SourcePathGetter) *pkg.Manager {
|
|
||||||
const timeout = 10 * time.Second
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
},
|
|
||||||
Timeout: timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &pkg.Manager{
|
|
||||||
Local: &pkg.Store{
|
|
||||||
BaseDir: localPath,
|
|
||||||
ManifestFile: pkg.ManifestFile,
|
|
||||||
},
|
|
||||||
PackagePathGetter: srcPathGetter,
|
|
||||||
Client: httpClient,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func videoFileFilter(ctx context.Context, f models.File) bool {
|
|
||||||
return useAsVideo(f.Base().Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageFileFilter(ctx context.Context, f models.File) bool {
|
|
||||||
return useAsImage(f.Base().Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func galleryFileFilter(ctx context.Context, f models.File) bool {
|
|
||||||
return isZip(f.Base().Basename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeScanner(repo models.Repository, pluginCache *plugin.Cache) *file.Scanner {
|
|
||||||
return &file.Scanner{
|
|
||||||
Repository: file.NewRepository(repo),
|
|
||||||
FileDecorators: []file.Decorator{
|
|
||||||
&file.FilteredDecorator{
|
|
||||||
Decorator: &video.Decorator{
|
|
||||||
FFProbe: instance.FFProbe,
|
|
||||||
},
|
|
||||||
Filter: file.FilterFunc(videoFileFilter),
|
|
||||||
},
|
|
||||||
&file.FilteredDecorator{
|
|
||||||
Decorator: &file_image.Decorator{
|
|
||||||
FFProbe: instance.FFProbe,
|
|
||||||
},
|
|
||||||
Filter: file.FilterFunc(imageFileFilter),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
FingerprintCalculator: &fingerprintCalculator{instance.Config},
|
|
||||||
FS: &file.OsFS{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeCleaner(repo models.Repository, pluginCache *plugin.Cache) *file.Cleaner {
|
|
||||||
return &file.Cleaner{
|
|
||||||
FS: &file.OsFS{},
|
|
||||||
Repository: file.NewRepository(repo),
|
|
||||||
Handlers: []file.CleanHandler{
|
|
||||||
&cleanHandler{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initJobManager() *job.Manager {
|
|
||||||
ret := job.NewManager()
|
|
||||||
|
|
||||||
// desktop notifications
|
|
||||||
ctx := context.Background()
|
|
||||||
c := ret.Subscribe(context.Background())
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case j := <-c.RemovedJob:
|
|
||||||
if instance.Config.GetNotificationsEnabled() {
|
|
||||||
cleanDesc := strings.TrimRight(j.Description, ".")
|
|
||||||
|
|
||||||
if j.StartTime == nil {
|
|
||||||
// Task was never started
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
timeElapsed := j.EndTime.Sub(*j.StartTime)
|
|
||||||
desktop.SendNotification("Task Finished", "Task \""+cleanDesc+"\" is finished in "+formatDuration(timeElapsed)+".")
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatDuration(t time.Duration) string {
|
|
||||||
return fmt.Sprintf("%02.f:%02.f:%02.f", t.Hours(), t.Minutes(), t.Seconds())
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSecurity(cfg *config.Instance) {
|
|
||||||
if err := session.CheckExternalAccessTripwire(cfg); err != nil {
|
|
||||||
session.LogExternalAccessError(*err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initProfiling(cpuProfilePath string) {
|
|
||||||
if cpuProfilePath == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(cpuProfilePath)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatalf("unable to create cpu profile file: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("profiling to %s", cpuProfilePath)
|
|
||||||
|
|
||||||
// StopCPUProfile is defer called in main
|
|
||||||
if err = pprof.StartCPUProfile(f); err != nil {
|
|
||||||
logger.Warnf("could not start CPU profiling: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFFMPEG(ctx context.Context) error {
|
|
||||||
// only do this if we have a config file set
|
|
||||||
if instance.Config.GetConfigFile() != "" {
|
|
||||||
// use same directory as config path
|
|
||||||
configDirectory := instance.Config.GetConfigPath()
|
|
||||||
paths := []string{
|
|
||||||
configDirectory,
|
|
||||||
paths.GetStashHomeDirectory(),
|
|
||||||
}
|
|
||||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(paths)
|
|
||||||
|
|
||||||
if ffmpegPath == "" || ffprobePath == "" {
|
|
||||||
logger.Infof("couldn't find FFMPEG, attempting to download it")
|
|
||||||
if err := ffmpeg.Download(ctx, configDirectory); err != nil {
|
|
||||||
msg := `Unable to locate / automatically download FFMPEG
|
|
||||||
|
|
||||||
Check the readme for download links.
|
|
||||||
The FFMPEG and FFProbe binaries should be placed in %s
|
|
||||||
|
|
||||||
The error was: %s
|
|
||||||
`
|
|
||||||
logger.Errorf(msg, configDirectory, err)
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
// After download get new paths for ffmpeg and ffprobe
|
|
||||||
ffmpegPath, ffprobePath = ffmpeg.GetPaths(paths)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.FFMPEG = ffmpeg.NewEncoder(ffmpegPath)
|
|
||||||
instance.FFProbe = ffmpeg.FFProbe(ffprobePath)
|
|
||||||
|
|
||||||
instance.FFMPEG.InitHWSupport(ctx)
|
|
||||||
instance.RefreshStreamManager()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initLog() *log.Logger {
|
|
||||||
config := config.GetInstance()
|
|
||||||
l := log.NewLogger()
|
|
||||||
l.Init(config.GetLogFile(), config.GetLogOut(), config.GetLogLevel())
|
|
||||||
logger.Logger = l
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostInit initialises the paths, caches and txnManager after the initial
|
|
||||||
// configuration has been set. Should only be called if the configuration
|
|
||||||
// is valid.
|
|
||||||
func (s *Manager) PostInit(ctx context.Context) error {
|
|
||||||
if err := s.Config.SetInitialConfig(); err != nil {
|
|
||||||
logger.Warnf("could not set initial configuration: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*s.Paths = paths.NewPaths(s.Config.GetGeneratedPath(), s.Config.GetBlobsPath())
|
|
||||||
s.RefreshConfig()
|
|
||||||
s.SessionStore = session.NewStore(s.Config)
|
|
||||||
s.PluginCache.RegisterSessionStore(s.SessionStore)
|
|
||||||
|
|
||||||
if err := s.PluginCache.LoadPlugins(); err != nil {
|
|
||||||
logger.Errorf("Error reading plugin configs: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
s.SetBlobStoreOptions()
|
|
||||||
|
|
||||||
s.ScraperCache = instance.initScraperCache()
|
|
||||||
writeStashIcon()
|
|
||||||
|
|
||||||
// clear the downloads and tmp directories
|
|
||||||
// #1021 - only clear these directories if the generated folder is non-empty
|
|
||||||
if s.Config.GetGeneratedPath() != "" {
|
|
||||||
const deleteTimeout = 1 * time.Second
|
|
||||||
|
|
||||||
utils.Timeout(func() {
|
|
||||||
if err := fsutil.EmptyDir(instance.Paths.Generated.Downloads); err != nil {
|
|
||||||
logger.Warnf("could not empty Downloads directory: %v", err)
|
|
||||||
}
|
|
||||||
if err := fsutil.EnsureDir(instance.Paths.Generated.Tmp); err != nil {
|
|
||||||
logger.Warnf("could not create Tmp directory: %v", err)
|
|
||||||
} else {
|
|
||||||
if err := fsutil.EmptyDir(instance.Paths.Generated.Tmp); err != nil {
|
|
||||||
logger.Warnf("could not empty Tmp directory: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, deleteTimeout, func(done chan struct{}) {
|
|
||||||
logger.Info("Please wait. Deleting temporary files...") // print
|
|
||||||
<-done // and wait for deletion
|
|
||||||
logger.Info("Temporary files deleted.")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
database := s.Database
|
|
||||||
if err := database.Open(s.Config.GetDatabasePath()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the proxy if defined in config
|
|
||||||
if s.Config.GetProxy() != "" {
|
|
||||||
os.Setenv("HTTP_PROXY", s.Config.GetProxy())
|
|
||||||
os.Setenv("HTTPS_PROXY", s.Config.GetProxy())
|
|
||||||
os.Setenv("NO_PROXY", s.Config.GetNoProxy())
|
|
||||||
logger.Info("Using HTTP Proxy")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Manager) SetBlobStoreOptions() {
|
func (s *Manager) SetBlobStoreOptions() {
|
||||||
storageType := s.Config.GetBlobsStorage()
|
storageType := s.Config.GetBlobsStorage()
|
||||||
blobsPath := s.Config.GetBlobsPath()
|
blobsPath := s.Config.GetBlobsPath()
|
||||||
|
|
@ -529,59 +81,45 @@ func (s *Manager) SetBlobStoreOptions() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeStashIcon() {
|
|
||||||
iconPath := filepath.Join(instance.Config.GetConfigPath(), "icon.png")
|
|
||||||
err := os.WriteFile(iconPath, ui.FaviconProvider.GetFaviconPng(), 0644)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Couldn't write icon file: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initScraperCache initializes a new scraper cache and returns it.
|
|
||||||
func (s *Manager) initScraperCache() *scraper.Cache {
|
|
||||||
scraperRepository := scraper.NewRepository(s.Repository)
|
|
||||||
ret, err := scraper.NewCache(s.Config, scraperRepository)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Error reading scraper configs: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Manager) RefreshConfig() {
|
func (s *Manager) RefreshConfig() {
|
||||||
*s.Paths = paths.NewPaths(s.Config.GetGeneratedPath(), s.Config.GetBlobsPath())
|
cfg := s.Config
|
||||||
config := s.Config
|
*s.Paths = paths.NewPaths(cfg.GetGeneratedPath(), cfg.GetBlobsPath())
|
||||||
if config.Validate() == nil {
|
if cfg.Validate() == nil {
|
||||||
if err := fsutil.EnsureDir(s.Paths.Generated.Screenshots); err != nil {
|
if err := fsutil.EnsureDir(s.Paths.Generated.Screenshots); err != nil {
|
||||||
logger.Warnf("could not create directory for Screenshots: %v", err)
|
logger.Warnf("could not create screenshots directory: %v", err)
|
||||||
}
|
}
|
||||||
if err := fsutil.EnsureDir(s.Paths.Generated.Vtt); err != nil {
|
if err := fsutil.EnsureDir(s.Paths.Generated.Vtt); err != nil {
|
||||||
logger.Warnf("could not create directory for VTT: %v", err)
|
logger.Warnf("could not create VTT directory: %v", err)
|
||||||
}
|
}
|
||||||
if err := fsutil.EnsureDir(s.Paths.Generated.Markers); err != nil {
|
if err := fsutil.EnsureDir(s.Paths.Generated.Markers); err != nil {
|
||||||
logger.Warnf("could not create directory for Markers: %v", err)
|
logger.Warnf("could not create markers directory: %v", err)
|
||||||
}
|
}
|
||||||
if err := fsutil.EnsureDir(s.Paths.Generated.Transcodes); err != nil {
|
if err := fsutil.EnsureDir(s.Paths.Generated.Transcodes); err != nil {
|
||||||
logger.Warnf("could not create directory for Transcodes: %v", err)
|
logger.Warnf("could not create transcodes directory: %v", err)
|
||||||
}
|
}
|
||||||
if err := fsutil.EnsureDir(s.Paths.Generated.Downloads); err != nil {
|
if err := fsutil.EnsureDir(s.Paths.Generated.Downloads); err != nil {
|
||||||
logger.Warnf("could not create directory for Downloads: %v", err)
|
logger.Warnf("could not create downloads directory: %v", err)
|
||||||
}
|
}
|
||||||
if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil {
|
if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil {
|
||||||
logger.Warnf("could not create directory for Interactive Heatmaps: %v", err)
|
logger.Warnf("could not create interactive heatmaps directory: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshScraperCache refreshes the scraper cache. Call this when scraper
|
// RefreshPluginCache refreshes the plugin cache.
|
||||||
// configuration changes.
|
// Call this when the plugin configuration changes.
|
||||||
|
func (s *Manager) RefreshPluginCache() {
|
||||||
|
s.PluginCache.ReloadPlugins()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshScraperCache refreshes the scraper cache.
|
||||||
|
// Call this when the scraper configuration changes.
|
||||||
func (s *Manager) RefreshScraperCache() {
|
func (s *Manager) RefreshScraperCache() {
|
||||||
s.ScraperCache = s.initScraperCache()
|
s.ScraperCache.ReloadScrapers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshStreamManager refreshes the stream manager. Call this when cache directory
|
// RefreshStreamManager refreshes the stream manager.
|
||||||
// changes.
|
// Call this when the cache directory changes.
|
||||||
func (s *Manager) RefreshStreamManager() {
|
func (s *Manager) RefreshStreamManager() {
|
||||||
// shutdown existing manager if needed
|
// shutdown existing manager if needed
|
||||||
if s.StreamManager != nil {
|
if s.StreamManager != nil {
|
||||||
|
|
@ -589,8 +127,22 @@ func (s *Manager) RefreshStreamManager() {
|
||||||
s.StreamManager = nil
|
s.StreamManager = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := s.Config.GetCachePath()
|
cfg := s.Config
|
||||||
s.StreamManager = ffmpeg.NewStreamManager(cacheDir, s.FFMPEG, s.FFProbe, s.Config, s.ReadLockManager)
|
cacheDir := cfg.GetCachePath()
|
||||||
|
s.StreamManager = ffmpeg.NewStreamManager(cacheDir, s.FFMpeg, s.FFProbe, cfg, s.ReadLockManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshDLNA starts/stops the DLNA service as needed.
|
||||||
|
func (s *Manager) RefreshDLNA() {
|
||||||
|
dlnaService := s.DLNAService
|
||||||
|
enabled := s.Config.GetDLNADefaultEnabled()
|
||||||
|
if !enabled && dlnaService.IsRunning() {
|
||||||
|
dlnaService.Stop(nil)
|
||||||
|
} else if enabled && !dlnaService.IsRunning() {
|
||||||
|
if err := dlnaService.Start(nil); err != nil {
|
||||||
|
logger.Warnf("error starting DLNA service: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Manager) RefreshScraperSourceManager() {
|
func (s *Manager) RefreshScraperSourceManager() {
|
||||||
|
|
@ -625,7 +177,11 @@ func setSetupDefaults(input *SetupInput) {
|
||||||
|
|
||||||
func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
||||||
setSetupDefaults(&input)
|
setSetupDefaults(&input)
|
||||||
c := s.Config
|
cfg := s.Config
|
||||||
|
|
||||||
|
if err := cfg.SetInitialConfig(); err != nil {
|
||||||
|
return fmt.Errorf("error setting initial configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// create the config directory if it does not exist
|
// create the config directory if it does not exist
|
||||||
// don't do anything if config is already set in the environment
|
// don't do anything if config is already set in the environment
|
||||||
|
|
@ -652,7 +208,7 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the generated directory if it does not exist
|
// create the generated directory if it does not exist
|
||||||
if !c.HasOverride(config.Generated) {
|
if !cfg.HasOverride(config.Generated) {
|
||||||
if exists, _ := fsutil.DirExists(input.GeneratedLocation); !exists {
|
if exists, _ := fsutil.DirExists(input.GeneratedLocation); !exists {
|
||||||
if err := os.MkdirAll(input.GeneratedLocation, 0755); err != nil {
|
if err := os.MkdirAll(input.GeneratedLocation, 0755); err != nil {
|
||||||
return fmt.Errorf("error creating generated directory: %v", err)
|
return fmt.Errorf("error creating generated directory: %v", err)
|
||||||
|
|
@ -663,75 +219,60 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the cache directory if it does not exist
|
// create the cache directory if it does not exist
|
||||||
if !c.HasOverride(config.Cache) {
|
if !cfg.HasOverride(config.Cache) {
|
||||||
if exists, _ := fsutil.DirExists(input.CacheLocation); !exists {
|
if exists, _ := fsutil.DirExists(input.CacheLocation); !exists {
|
||||||
if err := os.MkdirAll(input.CacheLocation, 0755); err != nil {
|
if err := os.MkdirAll(input.CacheLocation, 0755); err != nil {
|
||||||
return fmt.Errorf("error creating cache directory: %v", err)
|
return fmt.Errorf("error creating cache directory: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Config.Set(config.Cache, input.CacheLocation)
|
cfg.Set(config.Cache, input.CacheLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.StoreBlobsInDatabase {
|
if input.StoreBlobsInDatabase {
|
||||||
s.Config.Set(config.BlobsStorage, config.BlobStorageTypeDatabase)
|
cfg.Set(config.BlobsStorage, config.BlobStorageTypeDatabase)
|
||||||
} else {
|
} else {
|
||||||
if !c.HasOverride(config.BlobsPath) {
|
if !cfg.HasOverride(config.BlobsPath) {
|
||||||
if exists, _ := fsutil.DirExists(input.BlobsLocation); !exists {
|
if exists, _ := fsutil.DirExists(input.BlobsLocation); !exists {
|
||||||
if err := os.MkdirAll(input.BlobsLocation, 0755); err != nil {
|
if err := os.MkdirAll(input.BlobsLocation, 0755); err != nil {
|
||||||
return fmt.Errorf("error creating blobs directory: %v", err)
|
return fmt.Errorf("error creating blobs directory: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Config.Set(config.BlobsPath, input.BlobsLocation)
|
cfg.Set(config.BlobsPath, input.BlobsLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Config.Set(config.BlobsStorage, config.BlobStorageTypeFilesystem)
|
cfg.Set(config.BlobsStorage, config.BlobStorageTypeFilesystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the configuration
|
// set the configuration
|
||||||
if !c.HasOverride(config.Database) {
|
if !cfg.HasOverride(config.Database) {
|
||||||
s.Config.Set(config.Database, input.DatabaseFile)
|
cfg.Set(config.Database, input.DatabaseFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Config.Set(config.Stash, input.Stashes)
|
cfg.Set(config.Stash, input.Stashes)
|
||||||
if err := s.Config.Write(); err != nil {
|
|
||||||
|
if err := cfg.Write(); err != nil {
|
||||||
return fmt.Errorf("error writing configuration file: %v", err)
|
return fmt.Errorf("error writing configuration file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialise the database
|
// finish initialization
|
||||||
if err := s.PostInit(ctx); err != nil {
|
if err := s.postInit(ctx); err != nil {
|
||||||
var migrationNeededErr *sqlite.MigrationNeededError
|
return fmt.Errorf("error completing initialization: %v", err)
|
||||||
if errors.As(err, &migrationNeededErr) {
|
|
||||||
logger.Warn(err.Error())
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("error initializing the database: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Config.FinalizeSetup()
|
cfg.FinalizeSetup()
|
||||||
|
|
||||||
if err := initFFMPEG(ctx); err != nil {
|
|
||||||
return fmt.Errorf("error initializing FFMPEG subsystem: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.Scanner = makeScanner(instance.Repository, instance.PluginCache)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Manager) validateFFMPEG() error {
|
func (s *Manager) validateFFmpeg() error {
|
||||||
if s.FFMPEG == nil || s.FFProbe == "" {
|
if s.FFMpeg == nil || s.FFProbe == "" {
|
||||||
return errors.New("missing ffmpeg and/or ffprobe")
|
return errors.New("missing ffmpeg and/or ffprobe")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type MigrateInput struct {
|
|
||||||
BackupPath string `json:"backupPath"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Manager) Migrate(ctx context.Context, input MigrateInput) error {
|
func (s *Manager) Migrate(ctx context.Context, input MigrateInput) error {
|
||||||
database := s.Database
|
database := s.Database
|
||||||
|
|
||||||
|
|
@ -778,6 +319,76 @@ func (s *Manager) Migrate(ctx context.Context, input MigrateInput) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Manager) BackupDatabase(download bool) (string, string, error) {
|
||||||
|
var backupPath string
|
||||||
|
var backupName string
|
||||||
|
if download {
|
||||||
|
backupDir := s.Paths.Generated.Downloads
|
||||||
|
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||||
|
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp(backupDir, "backup*.sqlite")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
backupPath = f.Name()
|
||||||
|
backupName = s.Database.DatabaseBackupPath("")
|
||||||
|
f.Close()
|
||||||
|
} else {
|
||||||
|
backupDir := s.Config.GetBackupDirectoryPathOrDefault()
|
||||||
|
if backupDir != "" {
|
||||||
|
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||||
|
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backupPath = s.Database.DatabaseBackupPath(backupDir)
|
||||||
|
backupName = filepath.Base(backupPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Database.Backup(backupPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return backupPath, backupName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Manager) AnonymiseDatabase(download bool) (string, string, error) {
|
||||||
|
var outPath string
|
||||||
|
var outName string
|
||||||
|
if download {
|
||||||
|
outDir := s.Paths.Generated.Downloads
|
||||||
|
if err := fsutil.EnsureDir(outDir); err != nil {
|
||||||
|
return "", "", fmt.Errorf("could not create output directory %v: %w", outDir, err)
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp(outDir, "anonymous*.sqlite")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
outPath = f.Name()
|
||||||
|
outName = s.Database.AnonymousDatabasePath("")
|
||||||
|
f.Close()
|
||||||
|
} else {
|
||||||
|
outDir := s.Config.GetBackupDirectoryPathOrDefault()
|
||||||
|
if outDir != "" {
|
||||||
|
if err := fsutil.EnsureDir(outDir); err != nil {
|
||||||
|
return "", "", fmt.Errorf("could not create output directory %v: %w", outDir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outPath = s.Database.AnonymousDatabasePath(outDir)
|
||||||
|
outName = filepath.Base(outPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Database.Anonymise(outPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outPath, outName, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Manager) GetSystemStatus() *SystemStatus {
|
func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||||
workingDir := fsutil.GetWorkingDirectory()
|
workingDir := fsutil.GetWorkingDirectory()
|
||||||
homeDir := fsutil.GetHomeDirectory()
|
homeDir := fsutil.GetHomeDirectory()
|
||||||
|
|
@ -809,24 +420,16 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown gracefully stops the manager
|
// Shutdown gracefully stops the manager
|
||||||
func (s *Manager) Shutdown(code int) {
|
func (s *Manager) Shutdown() {
|
||||||
// stop any profiling at exit
|
// TODO: Each part of the manager needs to gracefully stop at some point
|
||||||
pprof.StopCPUProfile()
|
|
||||||
|
|
||||||
if s.StreamManager != nil {
|
if s.StreamManager != nil {
|
||||||
s.StreamManager.Shutdown()
|
s.StreamManager.Shutdown()
|
||||||
s.StreamManager = nil
|
s.StreamManager = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Each part of the manager needs to gracefully stop at some point
|
|
||||||
// for now, we just close the database.
|
|
||||||
err := s.Database.Close()
|
err := s.Database.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error closing database: %s", err)
|
logger.Errorf("Error closing database: %s", err)
|
||||||
if code == 0 {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(code)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
|
"github.com/stashapp/stash/pkg/file"
|
||||||
|
file_image "github.com/stashapp/stash/pkg/file/image"
|
||||||
|
"github.com/stashapp/stash/pkg/file/video"
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/job"
|
"github.com/stashapp/stash/pkg/job"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
|
@ -90,12 +93,32 @@ type ScanMetaDataFilterInput struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Manager) Scan(ctx context.Context, input ScanMetadataInput) (int, error) {
|
func (s *Manager) Scan(ctx context.Context, input ScanMetadataInput) (int, error) {
|
||||||
if err := s.validateFFMPEG(); err != nil {
|
if err := s.validateFFmpeg(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scanner := &file.Scanner{
|
||||||
|
Repository: file.NewRepository(s.Repository),
|
||||||
|
FileDecorators: []file.Decorator{
|
||||||
|
&file.FilteredDecorator{
|
||||||
|
Decorator: &video.Decorator{
|
||||||
|
FFProbe: s.FFProbe,
|
||||||
|
},
|
||||||
|
Filter: file.FilterFunc(videoFileFilter),
|
||||||
|
},
|
||||||
|
&file.FilteredDecorator{
|
||||||
|
Decorator: &file_image.Decorator{
|
||||||
|
FFProbe: s.FFProbe,
|
||||||
|
},
|
||||||
|
Filter: file.FilterFunc(imageFileFilter),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FingerprintCalculator: &fingerprintCalculator{s.Config},
|
||||||
|
FS: &file.OsFS{},
|
||||||
|
}
|
||||||
|
|
||||||
scanJob := ScanJob{
|
scanJob := ScanJob{
|
||||||
scanner: s.Scanner,
|
scanner: scanner,
|
||||||
input: input,
|
input: input,
|
||||||
subscriptions: s.scanSubs,
|
subscriptions: s.scanSubs,
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +183,7 @@ func (s *Manager) RunSingleTask(ctx context.Context, t Task) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Manager) Generate(ctx context.Context, input GenerateMetadataInput) (int, error) {
|
func (s *Manager) Generate(ctx context.Context, input GenerateMetadataInput) (int, error) {
|
||||||
if err := s.validateFFMPEG(); err != nil {
|
if err := s.validateFFmpeg(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if err := instance.Paths.Generated.EnsureTmpDir(); err != nil {
|
if err := instance.Paths.Generated.EnsureTmpDir(); err != nil {
|
||||||
|
|
@ -254,8 +277,16 @@ type CleanMetadataInput struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Manager) Clean(ctx context.Context, input CleanMetadataInput) int {
|
func (s *Manager) Clean(ctx context.Context, input CleanMetadataInput) int {
|
||||||
|
cleaner := &file.Cleaner{
|
||||||
|
FS: &file.OsFS{},
|
||||||
|
Repository: file.NewRepository(s.Repository),
|
||||||
|
Handlers: []file.CleanHandler{
|
||||||
|
&cleanHandler{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
j := cleanJob{
|
j := cleanJob{
|
||||||
cleaner: s.Cleaner,
|
cleaner: cleaner,
|
||||||
repository: s.Repository,
|
repository: s.Repository,
|
||||||
sceneService: s.SceneService,
|
sceneService: s.SceneService,
|
||||||
imageService: s.ImageService,
|
imageService: s.ImageService,
|
||||||
|
|
|
||||||
36
internal/manager/models.go
Normal file
36
internal/manager/models.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SystemStatus struct {
|
||||||
|
DatabaseSchema *int `json:"databaseSchema"`
|
||||||
|
DatabasePath *string `json:"databasePath"`
|
||||||
|
ConfigPath *string `json:"configPath"`
|
||||||
|
AppSchema int `json:"appSchema"`
|
||||||
|
Status SystemStatusEnum `json:"status"`
|
||||||
|
Os string `json:"os"`
|
||||||
|
WorkingDir string `json:"working_dir"`
|
||||||
|
HomeDir string `json:"home_dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetupInput struct {
|
||||||
|
// Empty to indicate $HOME/.stash/config.yml default
|
||||||
|
ConfigLocation string `json:"configLocation"`
|
||||||
|
Stashes []*config.StashConfigInput `json:"stashes"`
|
||||||
|
// Empty to indicate default
|
||||||
|
DatabaseFile string `json:"databaseFile"`
|
||||||
|
// Empty to indicate default
|
||||||
|
GeneratedLocation string `json:"generatedLocation"`
|
||||||
|
// Empty to indicate default
|
||||||
|
CacheLocation string `json:"cacheLocation"`
|
||||||
|
|
||||||
|
StoreBlobsInDatabase bool `json:"storeBlobsInDatabase"`
|
||||||
|
// Empty to indicate default
|
||||||
|
BlobsLocation string `json:"blobsLocation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MigrateInput struct {
|
||||||
|
BackupPath string `json:"backupPath"`
|
||||||
|
}
|
||||||
|
|
@ -144,7 +144,7 @@ type cleanFilter struct {
|
||||||
scanFilter
|
scanFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCleanFilter(c *config.Instance) *cleanFilter {
|
func newCleanFilter(c *config.Config) *cleanFilter {
|
||||||
return &cleanFilter{
|
return &cleanFilter{
|
||||||
scanFilter: scanFilter{
|
scanFilter: scanFilter{
|
||||||
extensionConfig: newExtensionConfig(c),
|
extensionConfig: newExtensionConfig(c),
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||||
}
|
}
|
||||||
|
|
||||||
g := &generate.Generator{
|
g := &generate.Generator{
|
||||||
Encoder: instance.FFMPEG,
|
Encoder: instance.FFMpeg,
|
||||||
FFMpegConfig: instance.Config,
|
FFMpegConfig: instance.Config,
|
||||||
LockManager: instance.ReadLockManager,
|
LockManager: instance.ReadLockManager,
|
||||||
MarkerPaths: instance.Paths.SceneMarkers,
|
MarkerPaths: instance.Paths.SceneMarkers,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func (t *GenerateClipPreviewTask) Start(ctx context.Context) {
|
||||||
Preset: GetInstance().Config.GetPreviewPreset().String(),
|
Preset: GetInstance().Config.GetPreviewPreset().String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder := image.NewThumbnailEncoder(GetInstance().FFMPEG, GetInstance().FFProbe, clipPreviewOptions)
|
encoder := image.NewThumbnailEncoder(GetInstance().FFMpeg, GetInstance().FFProbe, clipPreviewOptions)
|
||||||
err := encoder.GetPreview(filePath, prevPath, models.DefaultGthumbWidth)
|
err := encoder.GetPreview(filePath, prevPath, models.DefaultGthumbWidth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("getting preview for image %s: %w", filePath, err)
|
logger.Errorf("getting preview for image %s: %w", filePath, err)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ func (t *GeneratePhashTask) Start(ctx context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, err := videophash.Generate(instance.FFMPEG, t.File)
|
hash, err := videophash.Generate(instance.FFMpeg, t.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("error generating phash: %s", err.Error())
|
logger.Errorf("error generating phash: %s", err.Error())
|
||||||
logErrorOutput(err)
|
logErrorOutput(err)
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func (t *GenerateCoverTask) Start(ctx context.Context) {
|
||||||
logger.Debugf("Creating screenshot for %s", scenePath)
|
logger.Debugf("Creating screenshot for %s", scenePath)
|
||||||
|
|
||||||
g := generate.Generator{
|
g := generate.Generator{
|
||||||
Encoder: instance.FFMPEG,
|
Encoder: instance.FFMpeg,
|
||||||
FFMpegConfig: instance.Config,
|
FFMpegConfig: instance.Config,
|
||||||
LockManager: instance.ReadLockManager,
|
LockManager: instance.ReadLockManager,
|
||||||
ScenePaths: instance.Paths.Scene,
|
ScenePaths: instance.Paths.Scene,
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ type extensionConfig struct {
|
||||||
zipExt []string
|
zipExt []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newExtensionConfig(c *config.Instance) extensionConfig {
|
func newExtensionConfig(c *config.Config) extensionConfig {
|
||||||
return extensionConfig{
|
return extensionConfig{
|
||||||
vidExt: c.GetVideoExtensions(),
|
vidExt: c.GetVideoExtensions(),
|
||||||
imgExt: c.GetImageExtensions(),
|
imgExt: c.GetImageExtensions(),
|
||||||
|
|
@ -126,7 +126,7 @@ type handlerRequiredFilter struct {
|
||||||
videoFileNamingAlgorithm models.HashAlgorithm
|
videoFileNamingAlgorithm models.HashAlgorithm
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHandlerRequiredFilter(c *config.Instance, repo models.Repository) *handlerRequiredFilter {
|
func newHandlerRequiredFilter(c *config.Config, repo models.Repository) *handlerRequiredFilter {
|
||||||
processes := c.GetParallelTasksWithAutoDetection()
|
processes := c.GetParallelTasksWithAutoDetection()
|
||||||
|
|
||||||
return &handlerRequiredFilter{
|
return &handlerRequiredFilter{
|
||||||
|
|
@ -239,7 +239,7 @@ type scanFilter struct {
|
||||||
minModTime time.Time
|
minModTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScanFilter(c *config.Instance, repo models.Repository, minModTime time.Time) *scanFilter {
|
func newScanFilter(c *config.Config, repo models.Repository, minModTime time.Time) *scanFilter {
|
||||||
return &scanFilter{
|
return &scanFilter{
|
||||||
extensionConfig: newExtensionConfig(c),
|
extensionConfig: newExtensionConfig(c),
|
||||||
txnManager: repo.TxnManager,
|
txnManager: repo.TxnManager,
|
||||||
|
|
@ -325,6 +325,18 @@ func (c *scanConfig) GetCreateGalleriesFromFolders() bool {
|
||||||
return c.createGalleriesFromFolders
|
return c.createGalleriesFromFolders
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func videoFileFilter(ctx context.Context, f models.File) bool {
|
||||||
|
return useAsVideo(f.Base().Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageFileFilter(ctx context.Context, f models.File) bool {
|
||||||
|
return useAsImage(f.Base().Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryFileFilter(ctx context.Context, f models.File) bool {
|
||||||
|
return isZip(f.Base().Basename)
|
||||||
|
}
|
||||||
|
|
||||||
func getScanHandlers(options ScanMetadataInput, taskQueue *job.TaskQueue, progress *job.Progress) []file.Handler {
|
func getScanHandlers(options ScanMetadataInput, taskQueue *job.TaskQueue, progress *job.Progress) []file.Handler {
|
||||||
mgr := GetInstance()
|
mgr := GetInstance()
|
||||||
c := mgr.Config
|
c := mgr.Config
|
||||||
|
|
@ -464,7 +476,7 @@ func (g *imageGenerators) generateThumbnail(ctx context.Context, i *models.Image
|
||||||
Preset: c.GetPreviewPreset().String(),
|
Preset: c.GetPreviewPreset().String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder := image.NewThumbnailEncoder(mgr.FFMPEG, mgr.FFProbe, clipPreviewOptions)
|
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
|
||||||
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -547,7 +559,7 @@ func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *mode
|
||||||
options := getGeneratePreviewOptions(GeneratePreviewOptionsInput{})
|
options := getGeneratePreviewOptions(GeneratePreviewOptionsInput{})
|
||||||
|
|
||||||
generator := &generate.Generator{
|
generator := &generate.Generator{
|
||||||
Encoder: mgr.FFMPEG,
|
Encoder: mgr.FFMpeg,
|
||||||
FFMpegConfig: mgr.Config,
|
FFMpegConfig: mgr.Config,
|
||||||
LockManager: mgr.ReadLockManager,
|
LockManager: mgr.ReadLockManager,
|
||||||
MarkerPaths: g.paths.SceneMarkers,
|
MarkerPaths: g.paths.SceneMarkers,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func GetPaths(paths []string) (string, string) {
|
||||||
|
|
||||||
// Check if ffmpeg exists in the config directory
|
// Check if ffmpeg exists in the config directory
|
||||||
if ffmpegPath == "" {
|
if ffmpegPath == "" {
|
||||||
ffmpegPath = fsutil.FindInPaths(paths, getFFMPEGFilename())
|
ffmpegPath = fsutil.FindInPaths(paths, getFFMpegFilename())
|
||||||
}
|
}
|
||||||
if ffprobePath == "" {
|
if ffprobePath == "" {
|
||||||
ffprobePath = fsutil.FindInPaths(paths, getFFProbeFilename())
|
ffprobePath = fsutil.FindInPaths(paths, getFFProbeFilename())
|
||||||
|
|
@ -39,7 +39,7 @@ func GetPaths(paths []string) (string, string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Download(ctx context.Context, configDirectory string) error {
|
func Download(ctx context.Context, configDirectory string) error {
|
||||||
for _, url := range getFFMPEGURL() {
|
for _, url := range getFFmpegURL() {
|
||||||
err := downloadSingle(ctx, configDirectory, url)
|
err := downloadSingle(ctx, configDirectory, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -47,7 +47,7 @@ func Download(ctx context.Context, configDirectory string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate that the urls contained what we needed
|
// validate that the urls contained what we needed
|
||||||
executables := []string{getFFMPEGFilename(), getFFProbeFilename()}
|
executables := []string{getFFMpegFilename(), getFFProbeFilename()}
|
||||||
for _, executable := range executables {
|
for _, executable := range executables {
|
||||||
_, err := os.Stat(filepath.Join(configDirectory, executable))
|
_, err := os.Stat(filepath.Join(configDirectory, executable))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -173,7 +173,7 @@ func downloadSingle(ctx context.Context, configDirectory, url string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFFMPEGURL() []string {
|
func getFFmpegURL() []string {
|
||||||
var urls []string
|
var urls []string
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
|
|
@ -195,7 +195,7 @@ func getFFMPEGURL() []string {
|
||||||
return urls
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFFMPEGFilename() string {
|
func getFFMpegFilename() string {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return "ffmpeg.exe"
|
return "ffmpeg.exe"
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +209,7 @@ func getFFProbeFilename() string {
|
||||||
return "ffprobe"
|
return "ffprobe"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if FFMPEG in the path has the correct flags
|
// Checks if ffmpeg in the path has the correct flags
|
||||||
func pathBinaryHasCorrectFlags() bool {
|
func pathBinaryHasCorrectFlags() bool {
|
||||||
ffmpegPath, err := exec.LookPath("ffmpeg")
|
ffmpegPath, err := exec.LookPath("ffmpeg")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/plugin/common"
|
"github.com/stashapp/stash/pkg/plugin/common"
|
||||||
|
|
@ -123,46 +124,31 @@ func (c *Cache) RegisterSessionStore(sessionStore *session.Store) {
|
||||||
c.sessionStore = sessionStore
|
c.sessionStore = sessionStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadPlugins clears the plugin cache and loads from the plugin path.
|
// ReloadPlugins clears the plugin cache and loads from the plugin path.
|
||||||
// In the event of an error during loading, the cache will be left empty.
|
// If a plugin cannot be loaded, an error is logged and the plugin is skipped.
|
||||||
func (c *Cache) LoadPlugins() error {
|
func (c *Cache) ReloadPlugins() {
|
||||||
c.plugins = nil
|
path := c.config.GetPluginsPath()
|
||||||
plugins, err := loadPlugins(c.config.GetPluginsPath())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.plugins = plugins
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadPlugins(path string) ([]Config, error) {
|
|
||||||
plugins := make([]Config, 0)
|
plugins := make([]Config, 0)
|
||||||
|
|
||||||
logger.Debugf("Reading plugin configs from %s", path)
|
logger.Debugf("Reading plugin configs from %s", path)
|
||||||
pluginFiles := []string{}
|
|
||||||
err := filepath.Walk(path, func(fp string, f os.FileInfo, err error) error {
|
err := fsutil.SymWalk(path, func(fp string, f os.FileInfo, err error) error {
|
||||||
if filepath.Ext(fp) == ".yml" {
|
if filepath.Ext(fp) == ".yml" {
|
||||||
pluginFiles = append(pluginFiles, fp)
|
plugin, err := loadPluginFromYAMLFile(fp)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error loading plugin %s: %v", fp, err)
|
||||||
|
} else {
|
||||||
|
plugins = append(plugins, *plugin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Errorf("Error reading plugin configs: %v", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range pluginFiles {
|
c.plugins = plugins
|
||||||
plugin, err := loadPluginFromYAMLFile(file)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Error loading plugin %s: %s", file, err.Error())
|
|
||||||
} else {
|
|
||||||
plugins = append(plugins, *plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return plugins, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Cache) enabledPlugins() []Config {
|
func (c Cache) enabledPlugins() []Config {
|
||||||
|
|
|
||||||
|
|
@ -133,32 +133,27 @@ func newClient(gc GlobalConfig) *http.Client {
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCache returns a new Cache loading scraper configurations from the
|
// NewCache returns a new Cache.
|
||||||
// scraper path provided in the global config object. It returns a new
|
|
||||||
// instance and an error if the scraper directory could not be loaded.
|
|
||||||
//
|
//
|
||||||
// Scraper configurations are loaded from yml files in the provided scrapers
|
// Scraper configurations are loaded from yml files in the scrapers
|
||||||
// directory and any subdirectories.
|
// directory in the config and any subdirectories.
|
||||||
func NewCache(globalConfig GlobalConfig, repo Repository) (*Cache, error) {
|
//
|
||||||
|
// Does not load scrapers. Scrapers will need to be
|
||||||
|
// loaded explicitly using ReloadScrapers.
|
||||||
|
func NewCache(globalConfig GlobalConfig, repo Repository) *Cache {
|
||||||
// HTTP Client setup
|
// HTTP Client setup
|
||||||
client := newClient(globalConfig)
|
client := newClient(globalConfig)
|
||||||
|
|
||||||
ret := &Cache{
|
return &Cache{
|
||||||
client: client,
|
client: client,
|
||||||
globalConfig: globalConfig,
|
globalConfig: globalConfig,
|
||||||
repository: repo,
|
repository: repo,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
ret.scrapers, err = ret.loadScrapers()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
// ReloadScrapers clears the scraper cache and reloads from the scraper path.
|
||||||
}
|
// If a scraper cannot be loaded, an error is logged and the scraper is skipped.
|
||||||
|
func (c *Cache) ReloadScrapers() {
|
||||||
func (c *Cache) loadScrapers() (map[string]scraper, error) {
|
|
||||||
path := c.globalConfig.GetScrapersPath()
|
path := c.globalConfig.GetScrapersPath()
|
||||||
scrapers := make(map[string]scraper)
|
scrapers := make(map[string]scraper)
|
||||||
|
|
||||||
|
|
@ -185,23 +180,9 @@ func (c *Cache) loadScrapers() (map[string]scraper, error) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error reading scraper configs: %v", err)
|
logger.Errorf("Error reading scraper configs: %v", err)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return scrapers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReloadScrapers clears the scraper cache and reloads from the scraper path.
|
|
||||||
// In the event of an error during loading, the cache will be left empty.
|
|
||||||
func (c *Cache) ReloadScrapers() error {
|
|
||||||
c.scrapers = nil
|
|
||||||
scrapers, err := c.loadScrapers()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.scrapers = scrapers
|
c.scrapers = scrapers
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListScrapers lists scrapers matching one of the given types.
|
// ListScrapers lists scrapers matching one of the given types.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil"
|
"github.com/stashapp/stash/pkg/sliceutil"
|
||||||
"github.com/stashapp/stash/pkg/sqlite"
|
"github.com/stashapp/stash/pkg/sqlite"
|
||||||
|
|
@ -535,6 +536,10 @@ func indexFromID(ids []int, id int) int {
|
||||||
var db *sqlite.Database
|
var db *sqlite.Database
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
// initialise empty config - needed by some migrations
|
||||||
|
_ = config.InitializeEmpty()
|
||||||
|
|
||||||
|
|
||||||
ret := runTests(m)
|
ret := runTests(m)
|
||||||
os.Exit(ret)
|
os.Exit(ret)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export const Setup: React.FC = () => {
|
||||||
const [storeBlobsInDatabase, setStoreBlobsInDatabase] = useState(false);
|
const [storeBlobsInDatabase, setStoreBlobsInDatabase] = useState(false);
|
||||||
const [blobsLocation, setBlobsLocation] = useState("");
|
const [blobsLocation, setBlobsLocation] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [setupError, setSetupError] = useState("");
|
const [setupError, setSetupError] = useState<string>();
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
@ -617,7 +617,11 @@ export const Setup: React.FC = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) setSetupError(e.message ?? e.toString());
|
if (e instanceof Error && e.message) {
|
||||||
|
setSetupError(e.message);
|
||||||
|
} else {
|
||||||
|
setSetupError(String(e));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
next();
|
next();
|
||||||
|
|
@ -737,6 +741,11 @@ export const Setup: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderError() {
|
function renderError() {
|
||||||
|
function onBackClick() {
|
||||||
|
setSetupError(undefined);
|
||||||
|
goBack(2);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -758,7 +767,7 @@ export const Setup: React.FC = () => {
|
||||||
</section>
|
</section>
|
||||||
<section className="mt-5">
|
<section className="mt-5">
|
||||||
<div className="d-flex justify-content-center">
|
<div className="d-flex justify-content-center">
|
||||||
<Button variant="secondary mx-2 p-5" onClick={() => goBack(2)}>
|
<Button variant="secondary mx-2 p-5" onClick={onBackClick}>
|
||||||
<FormattedMessage id="actions.previous_action" />
|
<FormattedMessage id="actions.previous_action" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -851,7 +860,7 @@ export const Setup: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFinish() {
|
function renderFinish() {
|
||||||
if (setupError) {
|
if (setupError !== undefined) {
|
||||||
return renderError();
|
return renderError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue