stash/internal/manager/manager.go

435 lines
12 KiB
Go

// Package manager provides the core manager of the application.
// This consolidates all the services and managers into a single struct.
package manager
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"time"
"github.com/remeh/sizedwaitgroup"
"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/job"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/paths"
"github.com/stashapp/stash/pkg/pkg"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/scraper"
"github.com/stashapp/stash/pkg/session"
"github.com/stashapp/stash/pkg/sqlite"
// register custom migrations
_ "github.com/stashapp/stash/pkg/sqlite/migrations"
)
type Manager struct {
Config *config.Config
Logger *log.Logger
// ImageThumbnailGenerateWaitGroup is the global wait group image thumbnail generation
// It uses the parallel tasks setting from the configuration.
ImageThumbnailGenerateWaitGroup sizedwaitgroup.SizedWaitGroup
Paths *paths.Paths
FFMpeg *ffmpeg.FFMpeg
FFProbe ffmpeg.FFProbe
StreamManager *ffmpeg.StreamManager
JobManager *job.Manager
ReadLockManager *fsutil.ReadLockManager
DownloadStore *DownloadStore
SessionStore *session.Store
PluginCache *plugin.Cache
ScraperCache *scraper.Cache
PluginPackageManager *pkg.Manager
ScraperPackageManager *pkg.Manager
DLNAService *dlna.Service
Database *sqlite.Database
Repository models.Repository
SceneService SceneService
ImageService ImageService
GalleryService GalleryService
GroupService GroupService
scanSubs *subscriptionManager
}
var instance *Manager
func GetInstance() *Manager {
if instance == nil {
panic("manager not initialized")
}
return instance
}
func (s *Manager) SetBlobStoreOptions() {
storageType := s.Config.GetBlobsStorage()
blobsPath := s.Config.GetBlobsPath()
extraBlobsPaths := s.Config.GetExtraBlobsPaths()
s.Database.SetBlobStoreOptions(sqlite.BlobStoreOptions{
UseFilesystem: storageType == config.BlobStorageTypeFilesystem,
UseDatabase: storageType == config.BlobStorageTypeDatabase,
Path: blobsPath,
SupplementaryPaths: extraBlobsPaths,
})
}
func (s *Manager) RefreshConfig() {
cfg := s.Config
*s.Paths = paths.NewPaths(cfg.GetGeneratedPath(), cfg.GetBlobsPath())
if cfg.Validate() == nil {
if err := fsutil.EnsureDir(s.Paths.Generated.Screenshots); err != nil {
logger.Warnf("could not create screenshots directory: %v", err)
}
if err := fsutil.EnsureDir(s.Paths.Generated.Vtt); err != nil {
logger.Warnf("could not create VTT directory: %v", err)
}
if err := fsutil.EnsureDir(s.Paths.Generated.Markers); err != nil {
logger.Warnf("could not create markers directory: %v", err)
}
if err := fsutil.EnsureDir(s.Paths.Generated.Transcodes); err != nil {
logger.Warnf("could not create transcodes directory: %v", err)
}
if err := fsutil.EnsureDir(s.Paths.Generated.Downloads); err != nil {
logger.Warnf("could not create downloads directory: %v", err)
}
if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil {
logger.Warnf("could not create interactive heatmaps directory: %v", err)
}
s.ImageThumbnailGenerateWaitGroup.Size = cfg.GetParallelTasksWithAutoDetection()
}
}
// RefreshPluginCache refreshes the plugin cache.
// 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() {
s.ScraperCache.ReloadScrapers()
}
// RefreshStreamManager refreshes the stream manager.
// Call this when the cache directory changes.
func (s *Manager) RefreshStreamManager() {
// shutdown existing manager if needed
if s.StreamManager != nil {
s.StreamManager.Shutdown()
s.StreamManager = nil
}
cfg := s.Config
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 createPackageManager(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 (s *Manager) RefreshScraperSourceManager() {
s.ScraperPackageManager = createPackageManager(s.Config.GetScrapersPath(), s.Config.GetScraperPackagePathGetter())
}
func (s *Manager) RefreshPluginSourceManager() {
s.PluginPackageManager = createPackageManager(s.Config.GetPluginsPath(), s.Config.GetPluginPackagePathGetter())
}
func setSetupDefaults(input *SetupInput) {
if input.ConfigLocation == "" {
input.ConfigLocation = filepath.Join(fsutil.GetHomeDirectory(), ".stash", "config.yml")
}
configDir := filepath.Dir(input.ConfigLocation)
if input.GeneratedLocation == "" {
input.GeneratedLocation = filepath.Join(configDir, "generated")
}
if input.CacheLocation == "" {
input.CacheLocation = filepath.Join(configDir, "cache")
}
if input.DatabaseFile == "" {
input.DatabaseFile = filepath.Join(configDir, "stash-go.sqlite")
}
if input.BlobsLocation == "" {
input.BlobsLocation = filepath.Join(configDir, "blobs")
}
}
func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
setSetupDefaults(&input)
cfg := s.Config
// create the config directory if it does not exist
// don't do anything if config is already set in the environment
if !config.FileEnvSet() {
// #3304 - if config path is relative, it breaks the ffmpeg/ffprobe
// paths since they must not be relative. The config file property is
// resolved to an absolute path when stash is run normally, so convert
// relative paths to absolute paths during setup.
configFile, _ := filepath.Abs(input.ConfigLocation)
configDir := filepath.Dir(configFile)
if exists, _ := fsutil.DirExists(configDir); !exists {
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("error creating config directory: %v", err)
}
}
if err := fsutil.Touch(configFile); err != nil {
return fmt.Errorf("error creating config file: %v", err)
}
s.Config.SetConfigFile(configFile)
}
if err := cfg.SetInitialConfig(); err != nil {
return fmt.Errorf("error setting initial configuration: %v", err)
}
// create the generated directory if it does not exist
if !cfg.HasOverride(config.Generated) {
if exists, _ := fsutil.DirExists(input.GeneratedLocation); !exists {
if err := os.MkdirAll(input.GeneratedLocation, 0755); err != nil {
return fmt.Errorf("error creating generated directory: %v", err)
}
}
s.Config.SetString(config.Generated, input.GeneratedLocation)
}
// create the cache directory if it does not exist
if !cfg.HasOverride(config.Cache) {
if exists, _ := fsutil.DirExists(input.CacheLocation); !exists {
if err := os.MkdirAll(input.CacheLocation, 0755); err != nil {
return fmt.Errorf("error creating cache directory: %v", err)
}
}
cfg.SetString(config.Cache, input.CacheLocation)
}
if input.StoreBlobsInDatabase {
cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeDatabase)
} else {
if !cfg.HasOverride(config.BlobsPath) {
if exists, _ := fsutil.DirExists(input.BlobsLocation); !exists {
if err := os.MkdirAll(input.BlobsLocation, 0755); err != nil {
return fmt.Errorf("error creating blobs directory: %v", err)
}
}
cfg.SetString(config.BlobsPath, input.BlobsLocation)
}
cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeFilesystem)
}
// set the configuration
if !cfg.HasOverride(config.Database) {
cfg.SetString(config.Database, input.DatabaseFile)
}
cfg.SetInterface(config.Stash, input.Stashes)
if err := cfg.Write(); err != nil {
return fmt.Errorf("error writing configuration file: %v", err)
}
// finish initialization
if err := s.postInit(ctx); err != nil {
return fmt.Errorf("error completing initialization: %v", err)
}
cfg.FinalizeSetup()
return nil
}
func (s *Manager) validateFFmpeg() error {
if s.FFMpeg == nil || s.FFProbe == "" {
return errors.New("missing ffmpeg and/or ffprobe")
}
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 {
workingDir := fsutil.GetWorkingDirectory()
homeDir := fsutil.GetHomeDirectory()
database := s.Database
dbSchema := int(database.Version())
dbPath := database.DatabasePath()
appSchema := int(database.AppSchemaVersion())
status := SystemStatusEnumOk
if s.Config.IsNewSystem() {
status = SystemStatusEnumSetup
} else if dbSchema < appSchema {
status = SystemStatusEnumNeedsMigration
}
configFile := s.Config.GetConfigFile()
ffmpegPath := ""
if s.FFMpeg != nil {
ffmpegPath = s.FFMpeg.Path()
}
ffprobePath := ""
if s.FFProbe != "" {
ffprobePath = s.FFProbe.Path()
}
return &SystemStatus{
Os: runtime.GOOS,
WorkingDir: workingDir,
HomeDir: homeDir,
DatabaseSchema: &dbSchema,
DatabasePath: &dbPath,
AppSchema: appSchema,
Status: status,
ConfigPath: &configFile,
FfmpegPath: &ffmpegPath,
FfprobePath: &ffprobePath,
}
}
// Shutdown gracefully stops the manager
func (s *Manager) Shutdown() {
// TODO: Each part of the manager needs to gracefully stop at some point
if s.StreamManager != nil {
s.StreamManager.Shutdown()
s.StreamManager = nil
}
err := s.Database.Close()
if err != nil {
logger.Errorf("Error closing database: %s", err)
}
}