From 77ffb65681420a597f8156f22d4fac772d38ab45 Mon Sep 17 00:00:00 2001 From: Stash Dev Date: Mon, 11 Feb 2019 02:49:39 -0800 Subject: [PATCH] Added an onboarding flow --- README.md | 42 +++------------ api/server.go | 87 +++++++++++++++++++++++++++++++- main.go | 6 ++- manager/jsonschema/config.go | 16 ++++-- manager/manager.go | 45 ++++++++++++++++- manager/paths/paths.go | 19 +------ manager/paths/paths_generated.go | 5 -- manager/paths/paths_json.go | 6 --- ui/setup/index.html | 35 +++++++++++++ ui/setup/milligram.min.css | 11 ++++ 10 files changed, 203 insertions(+), 69 deletions(-) create mode 100644 ui/setup/index.html create mode 100755 ui/setup/milligram.min.css 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 @@ + + + + + Stash + + + + + + + +
+
+
+ + + + + + + + + + + +
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/ui/setup/milligram.min.css b/ui/setup/milligram.min.css new file mode 100755 index 000000000..85f877b9f --- /dev/null +++ b/ui/setup/milligram.min.css @@ -0,0 +1,11 @@ +/*! + * Milligram v1.3.0 + * https://milligram.github.io + * + * Copyright (c) 2017 CJ Patoilo + * Licensed under the MIT license + */ + +*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:0.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#9b4dca}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#9b4dca}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#9b4dca}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #9b4dca;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#9b4dca;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#9b4dca;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} + +/*# sourceMappingURL=milligram.min.css.map */ \ No newline at end of file