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:
DingDongSoLong4 2023-11-28 04:56:46 +02:00 committed by GitHub
parent fc1fc20df4
commit b78771dbcd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1230 additions and 1213 deletions

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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 {}
} }

View file

@ -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
} }

View file

@ -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)

View file

@ -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)
}
} }
} }

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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)
} }
} }

View file

@ -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.

View file

@ -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

View file

@ -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()

View file

@ -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, &notFoundErr) { if errors.As(err, &notFoundErr) {
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
View 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()))
}

View file

@ -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) {

View file

@ -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 {

View file

@ -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
View 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
}

View file

@ -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)
}

View file

@ -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,

View 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"`
}

View file

@ -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),

View file

@ -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,

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -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 {

View file

@ -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.

View file

@ -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)
} }

View file

@ -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();
} }