2019-02-09 12:30:49 +00:00
package manager
import (
2021-04-11 23:31:33 +00:00
"errors"
"fmt"
"os"
"path/filepath"
2021-05-16 07:21:11 +00:00
"runtime/pprof"
2019-08-23 05:27:00 +00:00
"sync"
2021-04-22 03:51:51 +00:00
"time"
2019-08-23 05:27:00 +00:00
2021-04-11 23:31:33 +00:00
"github.com/stashapp/stash/pkg/database"
2021-05-20 06:58:43 +00:00
"github.com/stashapp/stash/pkg/dlna"
2019-02-14 23:42:52 +00:00
"github.com/stashapp/stash/pkg/ffmpeg"
2021-05-24 04:24:18 +00:00
"github.com/stashapp/stash/pkg/job"
2019-02-14 23:42:52 +00:00
"github.com/stashapp/stash/pkg/logger"
2019-03-23 14:56:59 +00:00
"github.com/stashapp/stash/pkg/manager/config"
2019-02-14 23:42:52 +00:00
"github.com/stashapp/stash/pkg/manager/paths"
2021-01-18 01:23:20 +00:00
"github.com/stashapp/stash/pkg/models"
2020-08-08 02:05:35 +00:00
"github.com/stashapp/stash/pkg/plugin"
2020-07-21 04:06:25 +00:00
"github.com/stashapp/stash/pkg/scraper"
2021-06-11 07:24:58 +00:00
"github.com/stashapp/stash/pkg/session"
2021-01-18 01:23:20 +00:00
"github.com/stashapp/stash/pkg/sqlite"
2019-02-14 23:42:52 +00:00
"github.com/stashapp/stash/pkg/utils"
2019-02-09 12:30:49 +00:00
)
type singleton struct {
2021-04-11 23:31:33 +00:00
Config * config . Instance
2021-05-24 04:24:18 +00:00
Paths * paths . Paths
2019-03-23 14:56:59 +00:00
FFMPEGPath string
FFProbePath string
2020-07-21 04:06:25 +00:00
2021-06-11 07:24:58 +00:00
SessionStore * session . Store
2021-05-24 04:24:18 +00:00
JobManager * job . Manager
2020-08-08 02:05:35 +00:00
PluginCache * plugin . Cache
2020-07-21 04:06:25 +00:00
ScraperCache * scraper . Cache
2020-09-15 07:28:53 +00:00
DownloadStore * DownloadStore
2021-01-18 01:23:20 +00:00
2021-05-20 06:58:43 +00:00
DLNAService * dlna . Service
2021-01-18 01:23:20 +00:00
TxnManager models . TransactionManager
2021-05-24 04:24:18 +00:00
scanSubs * subscriptionManager
2019-02-09 12:30:49 +00:00
}
var instance * singleton
var once sync . Once
func GetInstance ( ) * singleton {
Initialize ( )
return instance
}
func Initialize ( ) * singleton {
once . Do ( func ( ) {
2021-04-11 23:31:33 +00:00
cfg , err := config . Initialize ( )
2021-05-13 12:15:21 +00:00
if err != nil {
panic ( fmt . Sprintf ( "error initializing configuration: %s" , err . Error ( ) ) )
}
2019-10-25 00:13:44 +00:00
initLog ( )
2021-05-16 07:21:11 +00:00
initProfiling ( cfg . GetCPUProfilePath ( ) )
2020-09-15 07:28:53 +00:00
2021-04-11 23:31:33 +00:00
instance = & singleton {
Config : cfg ,
2021-05-24 04:24:18 +00:00
JobManager : job . NewManager ( ) ,
2020-09-15 07:28:53 +00:00
DownloadStore : NewDownloadStore ( ) ,
2021-06-03 01:00:17 +00:00
PluginCache : plugin . NewCache ( cfg ) ,
2019-02-11 06:39:21 +00:00
2021-04-11 23:31:33 +00:00
TxnManager : sqlite . NewTransactionManager ( ) ,
2021-05-24 04:24:18 +00:00
scanSubs : & subscriptionManager { } ,
2021-04-11 23:31:33 +00:00
}
2019-02-11 10:49:39 +00:00
2021-05-20 06:58:43 +00:00
sceneServer := SceneServer {
TXNManager : instance . TxnManager ,
}
instance . DLNAService = dlna . NewService ( instance . TxnManager , instance . Config , & sceneServer )
2021-05-13 12:15:21 +00:00
if ! cfg . IsNewSystem ( ) {
2021-04-11 23:31:33 +00:00
logger . Infof ( "using config file: %s" , cfg . GetConfigFile ( ) )
if err == nil {
err = cfg . Validate ( )
}
if err != nil {
panic ( fmt . Sprintf ( "error initializing configuration: %s" , err . Error ( ) ) )
} else {
if err := instance . PostInit ( ) ; err != nil {
panic ( err )
}
}
} else {
2021-05-13 12:15:21 +00:00
cfgFile := cfg . GetConfigFile ( )
if cfgFile != "" {
cfgFile = cfgFile + " "
}
2021-06-11 07:24:58 +00:00
// create temporary session store - this will be re-initialised
// after config is complete
instance . SessionStore = session . NewStore ( cfg )
2021-05-13 12:15:21 +00:00
logger . Warnf ( "config file %snot found. Assuming new system..." , cfgFile )
2021-02-02 09:32:37 +00:00
}
2020-09-15 07:28:53 +00:00
2019-02-11 06:39:21 +00:00
initFFMPEG ( )
2021-05-20 06:58:43 +00:00
// if DLNA is enabled, start it now
if instance . Config . GetDLNADefaultEnabled ( ) {
instance . DLNAService . Start ( nil )
}
2019-02-09 12:30:49 +00:00
} )
return instance
}
2021-05-16 07:21:11 +00:00
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
pprof . StartCPUProfile ( f )
}
2021-05-17 23:14:25 +00:00
func initFFMPEG ( ) 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 ( 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 )
}
2019-02-09 12:30:49 +00:00
}
2021-05-17 23:14:25 +00:00
instance . FFMPEGPath = ffmpegPath
instance . FFProbePath = ffprobePath
2019-02-09 12:30:49 +00:00
}
2019-02-11 07:35:53 +00:00
2021-05-17 23:14:25 +00:00
return nil
2019-02-11 10:49:39 +00:00
}
2019-10-25 00:13:44 +00:00
func initLog ( ) {
2021-04-11 23:31:33 +00:00
config := config . GetInstance ( )
2019-10-25 00:13:44 +00:00
logger . Init ( config . GetLogFile ( ) , config . GetLogOut ( ) , config . GetLogLevel ( ) )
}
2021-04-11 23:31:33 +00:00
// 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 * singleton ) PostInit ( ) error {
2021-05-03 21:42:33 +00:00
s . Config . SetInitialConfig ( )
2021-04-11 23:31:33 +00:00
s . Paths = paths . NewPaths ( s . Config . GetGeneratedPath ( ) )
s . RefreshConfig ( )
2021-06-11 07:24:58 +00:00
s . SessionStore = session . NewStore ( s . Config )
s . PluginCache . RegisterSessionStore ( s . SessionStore )
2021-04-11 23:31:33 +00:00
2021-06-03 01:00:17 +00:00
if err := s . PluginCache . LoadPlugins ( ) ; err != nil {
logger . Errorf ( "Error reading plugin configs: %s" , err . Error ( ) )
}
s . ScraperCache = instance . initScraperCache ( )
2021-04-11 23:31:33 +00:00
// clear the downloads and tmp directories
// #1021 - only clear these directories if the generated folder is non-empty
if s . Config . GetGeneratedPath ( ) != "" {
2021-04-22 03:51:51 +00:00
const deleteTimeout = 1 * time . Second
utils . Timeout ( func ( ) {
utils . EmptyDir ( instance . Paths . Generated . Downloads )
utils . EmptyDir ( instance . Paths . Generated . Tmp )
} , deleteTimeout , func ( done chan struct { } ) {
logger . Info ( "Please wait. Deleting temporary files..." ) // print
<- done // and wait for deletion
logger . Info ( "Temporary files deleted." )
} )
2021-04-11 23:31:33 +00:00
}
if err := database . Initialize ( s . Config . GetDatabasePath ( ) ) ; err != nil {
return err
}
if database . Ready ( ) == nil {
s . PostMigrate ( )
}
return nil
}
2020-08-04 00:42:40 +00:00
// initScraperCache initializes a new scraper cache and returns it.
2021-01-18 01:23:20 +00:00
func ( s * singleton ) initScraperCache ( ) * scraper . Cache {
2021-04-11 23:31:33 +00:00
ret , err := scraper . NewCache ( config . GetInstance ( ) , s . TxnManager )
2020-07-21 04:06:25 +00:00
if err != nil {
logger . Errorf ( "Error reading scraper configs: %s" , err . Error ( ) )
}
return ret
}
2019-11-17 21:42:24 +00:00
func ( s * singleton ) RefreshConfig ( ) {
2021-04-11 23:31:33 +00:00
s . Paths = paths . NewPaths ( s . Config . GetGeneratedPath ( ) )
config := s . Config
if config . Validate ( ) == nil {
2020-09-15 07:28:53 +00:00
utils . EnsureDir ( s . Paths . Generated . Screenshots )
utils . EnsureDir ( s . Paths . Generated . Vtt )
utils . EnsureDir ( s . Paths . Generated . Markers )
utils . EnsureDir ( s . Paths . Generated . Transcodes )
utils . EnsureDir ( s . Paths . Generated . Downloads )
2019-02-11 10:49:39 +00:00
}
}
2020-08-04 00:42:40 +00:00
// RefreshScraperCache refreshes the scraper cache. Call this when scraper
// configuration changes.
func ( s * singleton ) RefreshScraperCache ( ) {
2021-01-18 01:23:20 +00:00
s . ScraperCache = s . initScraperCache ( )
2020-08-04 00:42:40 +00:00
}
2021-04-11 23:31:33 +00:00
func setSetupDefaults ( input * models . SetupInput ) {
if input . ConfigLocation == "" {
input . ConfigLocation = filepath . Join ( utils . GetHomeDirectory ( ) , ".stash" , "config.yml" )
}
configDir := filepath . Dir ( input . ConfigLocation )
if input . GeneratedLocation == "" {
input . GeneratedLocation = filepath . Join ( configDir , "generated" )
}
if input . DatabaseFile == "" {
input . DatabaseFile = filepath . Join ( configDir , "stash-go.sqlite" )
}
}
func ( s * singleton ) Setup ( input models . SetupInput ) error {
setSetupDefaults ( & input )
2021-08-10 04:58:14 +00:00
// create the config directory if it does not exist
configDir := filepath . Dir ( input . ConfigLocation )
if exists , _ := utils . DirExists ( configDir ) ; ! exists {
if err := os . Mkdir ( configDir , 0755 ) ; err != nil {
return fmt . Errorf ( "abc: %s" , err . Error ( ) )
}
}
2021-04-11 23:31:33 +00:00
// create the generated directory if it does not exist
if exists , _ := utils . DirExists ( input . GeneratedLocation ) ; ! exists {
if err := os . Mkdir ( input . GeneratedLocation , 0755 ) ; err != nil {
return fmt . Errorf ( "error creating generated directory: %s" , err . Error ( ) )
}
}
if err := utils . Touch ( input . ConfigLocation ) ; err != nil {
return fmt . Errorf ( "error creating config file: %s" , err . Error ( ) )
}
s . Config . SetConfigFile ( input . ConfigLocation )
// set the configuration
s . Config . Set ( config . Generated , input . GeneratedLocation )
s . Config . Set ( config . Database , input . DatabaseFile )
s . Config . Set ( config . Stash , input . Stashes )
if err := s . Config . Write ( ) ; err != nil {
return fmt . Errorf ( "error writing configuration file: %s" , err . Error ( ) )
}
// initialise the database
if err := s . PostInit ( ) ; err != nil {
return fmt . Errorf ( "error initializing the database: %s" , err . Error ( ) )
}
2021-05-13 12:15:21 +00:00
s . Config . FinalizeSetup ( )
2021-05-17 23:14:25 +00:00
initFFMPEG ( )
return nil
}
func ( s * singleton ) validateFFMPEG ( ) error {
if s . FFMPEGPath == "" || s . FFProbePath == "" {
return errors . New ( "missing ffmpeg and/or ffprobe" )
}
2021-04-11 23:31:33 +00:00
return nil
}
func ( s * singleton ) Migrate ( input models . MigrateInput ) error {
// always backup so that we can roll back to the previous version if
// migration fails
backupPath := input . BackupPath
if backupPath == "" {
backupPath = database . DatabaseBackupPath ( )
}
// perform database backup
if err := database . Backup ( database . DB , backupPath ) ; err != nil {
return fmt . Errorf ( "error backing up database: %s" , err )
}
if err := database . RunMigrations ( ) ; err != nil {
errStr := fmt . Sprintf ( "error performing migration: %s" , err )
// roll back to the backed up version
restoreErr := database . RestoreFromBackup ( backupPath )
if restoreErr != nil {
errStr = fmt . Sprintf ( "ERROR: unable to restore database from backup after migration failure: %s\n%s" , restoreErr . Error ( ) , errStr )
} else {
errStr = "An error occurred migrating the database to the latest schema version. The backup database file was automatically renamed to restore the database.\n" + errStr
}
return errors . New ( errStr )
}
// perform post-migration operations
s . PostMigrate ( )
// if no backup path was provided, then delete the created backup
if input . BackupPath == "" {
if err := os . Remove ( backupPath ) ; err != nil {
logger . Warnf ( "error removing unwanted database backup (%s): %s" , backupPath , err . Error ( ) )
}
}
return nil
}
func ( s * singleton ) GetSystemStatus ( ) * models . SystemStatus {
status := models . SystemStatusEnumOk
dbSchema := int ( database . Version ( ) )
dbPath := database . DatabasePath ( )
appSchema := int ( database . AppSchemaVersion ( ) )
2021-05-13 12:15:21 +00:00
configFile := s . Config . GetConfigFile ( )
2021-04-11 23:31:33 +00:00
2021-05-13 12:15:21 +00:00
if s . Config . IsNewSystem ( ) {
2021-04-11 23:31:33 +00:00
status = models . SystemStatusEnumSetup
} else if dbSchema < appSchema {
status = models . SystemStatusEnumNeedsMigration
}
return & models . SystemStatus {
DatabaseSchema : & dbSchema ,
DatabasePath : & dbPath ,
AppSchema : appSchema ,
Status : status ,
2021-05-13 12:15:21 +00:00
ConfigPath : & configFile ,
2021-04-11 23:31:33 +00:00
}
}
2021-09-07 04:28:40 +00:00
// Shutdown gracefully stops the manager
func ( s * singleton ) Shutdown ( ) error {
// TODO: Each part of the manager needs to gracefully stop at some point
// for now, we just close the database.
return database . Close ( )
}