diff --git a/README.md b/README.md index 0caf408ef..a0cf2aa35 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,15 @@ See a demo [here](https://vimeo.com/275537038) (password is stashapp). -TODO: This is not match the features of the Rails project quite yet. Consider using that until this project is complete. +TODO: This does not match the features of the Rails project quite yet and is still a little buggy. Fall back to the Rails project if you run into issues as an existing user. -## Setup +# Install -TODO: This is not final. There is more work to be done to ease this process. +Stash supports macOS, Windows, and Linux. Download the [latest release here](https://github.com/stashapp/stash/releases). -### OSX / Linux +Simply run the executable and navigate to either https://localhost:9999 or http://localhost:9998 to get started. -1. `mkdir ~/.stash` && `cd ~/.stash` -2. Create a `config.json` file (see below). -3. Run stash with `./stash` and visit `http://localhost:9998` or `https://localhost:9999` - -### Windows - -1. Create a new folder at `C:\Users\YourUsername\.stash` -2. Create a `config.json` file (see below) -3. Run stash with `./stash` and visit `http://localhost:9998` or `https://localhost:9999` +*Note for Windows users:* Running the app might present a security prompt since the binary isn't signed yet. Just click more info and then the run anyway button. #### FFMPEG @@ -34,29 +26,11 @@ If stash is unable to find or download FFMPEG then download it yourself from the The `ffmpeg(.exe)` and `ffprobe(.exe)` files should be placed in `~/.stash` on macOS / Linux or `C:\Users\YourUsername\.stash` on Windows. -#### Config.json +# FAQ -Example: +> Does stash support multiple folders? -*OSX / Linux* -``` -{ - "stash": "/Volumes/Drobo/videos", - "metadata": "/Volumes/Drobo/stash/metadata", - "cache": "/Volumes/Drobo/stash/cache", - "downloads": "/Volumes/Drobo/stash/downloads" -} -``` - -*Windows* -``` -{ - "stash": "C:\\Videos", - "metadata": "C:\\stash\\metadata", - "cache": "C:\\stash\\cache", - "downloads": "C:\\stash\\downloads" -} -``` +Not yet, but this will come in the future. # Development diff --git a/api/server.go b/api/server.go index c1dccbea0..bbedcba69 100644 --- a/api/server.go +++ b/api/server.go @@ -11,9 +11,14 @@ import ( "github.com/gobuffalo/packr/v2" "github.com/rs/cors" "github.com/stashapp/stash/logger" + "github.com/stashapp/stash/manager" + "github.com/stashapp/stash/manager/jsonschema" "github.com/stashapp/stash/models" + "github.com/stashapp/stash/utils" "net/http" + "os" "path" + "path/filepath" "runtime/debug" "strings" ) @@ -23,6 +28,7 @@ const httpsPort = "9999" var certsBox *packr.Box var uiBox *packr.Box +var setupUIBox *packr.Box func Start() { //port := os.Getenv("PORT") @@ -32,6 +38,7 @@ func Start() { certsBox = packr.New("Cert Box", "../certs") uiBox = packr.New("UI Box", "../ui/v1/dist/stash-frontend") + setupUIBox = packr.New("Setup UI Box", "../ui/setup") r := chi.NewRouter() @@ -41,6 +48,7 @@ func Start() { r.Use(middleware.StripSlashes) r.Use(cors.AllowAll().Handler) r.Use(BaseURLMiddleware) + r.Use(ConfigCheckMiddleware) recoverFunc := handler.RecoverFunc(func(ctx context.Context, err interface{}) error { logger.Error(err) @@ -66,6 +74,65 @@ func Start() { r.Mount("/scene", sceneRoutes{}.Routes()) r.Mount("/studio", studioRoutes{}.Routes()) + // Serve the setup UI + r.HandleFunc("/setup*", func(w http.ResponseWriter, r *http.Request) { + ext := path.Ext(r.URL.Path) + if ext == ".html" || ext == "" { + data := setupUIBox.Bytes("index.html") + _, _ = w.Write(data) + } else { + r.URL.Path = strings.Replace(r.URL.Path, "/setup", "", 1) + http.FileServer(setupUIBox).ServeHTTP(w, r) + } + }) + r.Post("/init", func(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, fmt.Sprintf("error: %s", err), 500) + } + stash := filepath.Clean(r.Form.Get("stash")) + metadata := filepath.Clean(r.Form.Get("metadata")) + cache := filepath.Clean(r.Form.Get("cache")) + //downloads := filepath.Clean(r.Form.Get("downloads")) // TODO + downloads := filepath.Join(metadata, "downloads") + + exists, _ := utils.FileExists(stash) + fileInfo, _ := os.Stat(stash) + if !exists || !fileInfo.IsDir() { + http.Error(w, fmt.Sprintf("the stash path either doesn't exist, or is not a directory <%s>. Go back and try again.", stash), 500) + return + } + + exists, _ = utils.FileExists(metadata) + fileInfo, _ = os.Stat(metadata) + if !exists || !fileInfo.IsDir() { + http.Error(w, fmt.Sprintf("the metadata path either doesn't exist, or is not a directory <%s> Go back and try again.", metadata), 500) + return + } + + exists, _ = utils.FileExists(cache) + fileInfo, _ = os.Stat(cache) + if !exists || !fileInfo.IsDir() { + http.Error(w, fmt.Sprintf("the cache path either doesn't exist, or is not a directory <%s> Go back and try again.", cache), 500) + return + } + + _ = os.Mkdir(downloads, 0755) + + config := &jsonschema.Config{ + Stash: stash, + Metadata: metadata, + Cache: cache, + Downloads: downloads, + } + if err := manager.GetInstance().SaveConfig(config); err != nil { + http.Error(w, fmt.Sprintf("there was an error saving the config file: %s", err), 500) + return + } + + http.Redirect(w, r, "/", 301) + }) + // Serve the angular app r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { ext := path.Ext(r.URL.Path) @@ -92,8 +159,10 @@ func Start() { logger.Fatal(server.ListenAndServe()) }() - logger.Infof("stash is running on HTTPS at https://localhost:9999/") - logger.Fatal(httpsServer.ListenAndServeTLS("", "")) + go func() { + logger.Infof("stash is running on HTTPS at https://localhost:9999/") + logger.Fatal(httpsServer.ListenAndServeTLS("", "")) + }() } func makeTLSConfig() *tls.Config { @@ -136,4 +205,18 @@ func BaseURLMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r) } return http.HandlerFunc(fn) +} + +func ConfigCheckMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ext := path.Ext(r.URL.Path) + shouldRedirect := ext == "" && r.Method == "GET" && r.URL.Path != "/init" + if !manager.HasValidConfig() && shouldRedirect { + if !strings.HasPrefix(r.URL.Path, "/setup") { + http.Redirect(w, r, "/setup", 301) + return + } + } + next.ServeHTTP(w, r) + }) } \ No newline at end of file diff --git a/main.go b/main.go index 0d9dedc7f..69da3bf23 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,10 @@ import ( func main() { managerInstance := manager.Initialize() database.Initialize(managerInstance.StaticPaths.DatabaseFile) - api.Start() + blockForever() +} + +func blockForever() { + select {} } diff --git a/manager/jsonschema/config.go b/manager/jsonschema/config.go index f477a597f..00673850c 100644 --- a/manager/jsonschema/config.go +++ b/manager/jsonschema/config.go @@ -2,15 +2,16 @@ package jsonschema import ( "encoding/json" + "fmt" "github.com/stashapp/stash/logger" "os" ) type Config struct { - Stash string `json:"stash"` + Stash string `json:"stash"` Metadata string `json:"metadata"` // Generated string `json:"generated"` // TODO: Generated directory instead of metadata - Cache string `json:"cache"` + Cache string `json:"cache"` Downloads string `json:"downloads"` } @@ -23,6 +24,15 @@ func LoadConfigFile(file string) *Config { } jsonParser := json.NewDecoder(configFile) parseError := jsonParser.Decode(&config) - if parseError != nil { panic(parseError) } + if parseError != nil { + logger.Errorf("config file parse error: %s", parseError) + } return &config } + +func SaveConfigFile(filePath string, config *Config) error { + if config == nil { + return fmt.Errorf("config must not be nil") + } + return marshalToFile(filePath, config) +} \ No newline at end of file diff --git a/manager/manager.go b/manager/manager.go index 4a273c999..ace9eaa2b 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -3,7 +3,9 @@ package manager import ( "github.com/stashapp/stash/ffmpeg" "github.com/stashapp/stash/logger" + "github.com/stashapp/stash/manager/jsonschema" "github.com/stashapp/stash/manager/paths" + "github.com/stashapp/stash/utils" "sync" ) @@ -24,13 +26,16 @@ func GetInstance() *singleton { func Initialize() *singleton { once.Do(func() { + configFile := jsonschema.LoadConfigFile(paths.StaticPaths.ConfigFile) instance = &singleton{ Status: Idle, - Paths: paths.RefreshPaths(), + Paths: paths.NewPaths(configFile), StaticPaths: &paths.StaticPaths, JSON: &jsonUtils{}, } + instance.refreshConfig(configFile) + initFFMPEG() }) @@ -56,3 +61,41 @@ The error was: %s instance.StaticPaths.FFMPEG = ffmpegPath instance.StaticPaths.FFProbe = ffprobePath } + +func HasValidConfig() bool { + configFileExists, _ := utils.FileExists(instance.StaticPaths.ConfigFile) // TODO: Verify JSON is correct + if configFileExists && instance.Paths.Config != nil { + return true + } + return false +} + +func (s *singleton) SaveConfig(config *jsonschema.Config) error { + if err := jsonschema.SaveConfigFile(s.StaticPaths.ConfigFile, config); err != nil { + return err + } + + // Reload the config + s.refreshConfig(config) + + return nil +} + +func (s *singleton) refreshConfig(config *jsonschema.Config) { + if config == nil { + config = jsonschema.LoadConfigFile(s.StaticPaths.ConfigFile) + } + s.Paths = paths.NewPaths(config) + + if HasValidConfig() { + _ = 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.JSON.Performers) + _ = utils.EnsureDir(s.Paths.JSON.Scenes) + _ = utils.EnsureDir(s.Paths.JSON.Galleries) + _ = utils.EnsureDir(s.Paths.JSON.Studios) + } +} diff --git a/manager/paths/paths.go b/manager/paths/paths.go index 3f1165e8d..b47c00bcb 100644 --- a/manager/paths/paths.go +++ b/manager/paths/paths.go @@ -2,7 +2,6 @@ package paths import ( "github.com/stashapp/stash/manager/jsonschema" - "github.com/stashapp/stash/utils" ) type Paths struct { @@ -15,14 +14,9 @@ type Paths struct { SceneMarkers *sceneMarkerPaths } -func RefreshPaths() *Paths { - ensureConfigFile() - return newPaths() -} - -func newPaths() *Paths { +func NewPaths(config *jsonschema.Config) *Paths { p := Paths{} - p.Config = jsonschema.LoadConfigFile(StaticPaths.ConfigFile) + p.Config = config p.Generated = newGeneratedPaths(p) p.JSON = newJSONPaths(p) @@ -30,13 +24,4 @@ func newPaths() *Paths { p.Scene = newScenePaths(p) p.SceneMarkers = newSceneMarkerPaths(p) return &p -} - -func ensureConfigFile() { - configFileExists, _ := utils.FileExists(StaticPaths.ConfigFile) // TODO: Verify JSON is correct. Pass verified - if configFileExists { - return - } - - panic("No config file found") } \ No newline at end of file diff --git a/manager/paths/paths_generated.go b/manager/paths/paths_generated.go index 7c252e1fb..e5d9c3e46 100644 --- a/manager/paths/paths_generated.go +++ b/manager/paths/paths_generated.go @@ -20,11 +20,6 @@ func newGeneratedPaths(p Paths) *generatedPaths { gp.Markers = filepath.Join(p.Config.Metadata, "markers") gp.Transcodes = filepath.Join(p.Config.Metadata, "transcodes") gp.Tmp = filepath.Join(p.Config.Metadata, "tmp") - - _ = utils.EnsureDir(gp.Screenshots) - _ = utils.EnsureDir(gp.Vtt) - _ = utils.EnsureDir(gp.Markers) - _ = utils.EnsureDir(gp.Transcodes) return &gp } diff --git a/manager/paths/paths_json.go b/manager/paths/paths_json.go index 2841265a3..e96ecba73 100644 --- a/manager/paths/paths_json.go +++ b/manager/paths/paths_json.go @@ -1,7 +1,6 @@ package paths import ( - "github.com/stashapp/stash/utils" "path/filepath" ) @@ -23,11 +22,6 @@ func newJSONPaths(p Paths) *jsonPaths { jp.Scenes = filepath.Join(p.Config.Metadata, "scenes") jp.Galleries = filepath.Join(p.Config.Metadata, "galleries") jp.Studios = filepath.Join(p.Config.Metadata, "studios") - - _ = utils.EnsureDir(jp.Performers) - _ = utils.EnsureDir(jp.Scenes) - _ = utils.EnsureDir(jp.Galleries) - _ = utils.EnsureDir(jp.Studios) return &jp } diff --git a/ui/setup/index.html b/ui/setup/index.html new file mode 100644 index 000000000..1947da862 --- /dev/null +++ b/ui/setup/index.html @@ -0,0 +1,35 @@ + + +
+ +