Added an onboarding flow

This commit is contained in:
Stash Dev 2019-02-11 02:49:39 -08:00
parent db42e43476
commit 77ffb65681
10 changed files with 203 additions and 69 deletions

View File

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

View File

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

View File

@ -12,6 +12,10 @@ import (
func main() {
managerInstance := manager.Initialize()
database.Initialize(managerInstance.StaticPaths.DatabaseFile)
api.Start()
blockForever()
}
func blockForever() {
select {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

35
ui/setup/index.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Stash</title>
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
<link rel="stylesheet" href="/setup/milligram.min.css">
</head>
<body>
<div class="container">
<form action="/init" method="POST">
<fieldset>
<label for="stash">Where is your porn located (mp4, wmv, zip, etc)?</label>
<input name="stash" type="text" placeholder="EX: C:\videos (Windows) or /User/StashApp/Videos (macOS / Linux)" />
<label for="metadata">Where would you like to save metadata? Metadata includes generated videos / images and backup JSON files.</label>
<input name="metadata" type="text" placeholder="EX: C:\stash\metadata (Windows) or /User/StashApp/stash/metadata (macOS / Linux)" />
<label for="cache">Where do you want to Stash to save cache / temporary files it might need to create?</label>
<input name="cache" type="text" placeholder="EX: C:\stash\cache (Windows) or /User/StashApp/stash/cache (macOS / Linux)" />
<input hidden name="downloads" value="">
<div>
<input class="button button-black" type="submit" value="Submit">
</div>
</fieldset>
</form>
</div>
</body>
</html>

11
ui/setup/milligram.min.css vendored Executable file

File diff suppressed because one or more lines are too long