diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 359229a4a..88ffb36dd 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -6,6 +6,8 @@ input SetupInput { databaseFile: String! """Empty to indicate default""" generatedLocation: String! + """Empty to indicate default""" + cacheLocation: String! } enum StreamingResolutionEnum { diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 53a00c1ba..d12930c2f 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -100,6 +100,8 @@ type SetupInput struct { DatabaseFile string `json:"databaseFile"` // Empty to indicate default GeneratedLocation string `json:"generatedLocation"` + // Empty to indicate default + CacheLocation string `json:"cacheLocation"` } type Manager struct { @@ -588,6 +590,9 @@ func setSetupDefaults(input *SetupInput) { 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") @@ -633,6 +638,17 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error { s.Config.Set(config.Generated, input.GeneratedLocation) } + // create the cache directory if it does not exist + if !c.HasOverride(config.Cache) { + if exists, _ := fsutil.DirExists(input.CacheLocation); !exists { + if err := os.Mkdir(input.CacheLocation, 0755); err != nil { + return fmt.Errorf("error creating cache directory: %v", err) + } + } + + s.Config.Set(config.Cache, input.CacheLocation) + } + // set the configuration if !c.HasOverride(config.Database) { s.Config.Set(config.Database, input.DatabaseFile) diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 10660e884..0833452ba 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -1,5 +1,11 @@ import React, { Suspense, useEffect, useState } from "react"; -import { Route, Switch, useRouteMatch } from "react-router-dom"; +import { + Route, + Switch, + useHistory, + useLocation, + useRouteMatch, +} from "react-router-dom"; import { IntlProvider, CustomFormats } from "react-intl"; import { Helmet } from "react-helmet"; import cloneDeep from "lodash-es/cloneDeep"; @@ -30,7 +36,7 @@ import { InteractiveProvider } from "./hooks/Interactive/context"; import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog"; import { IUIConfig } from "./core/config"; import { releaseNotes } from "./docs/en/ReleaseNotes"; -import { getPlatformURL, getBaseURL } from "./core/createClient"; +import { getPlatformURL } from "./core/createClient"; import { lazyComponent } from "./utils/lazyComponent"; const Performers = lazyComponent( @@ -126,6 +132,8 @@ export const App: React.FC = () => { setLocale(); }, [language]); + const location = useLocation(); + const history = useHistory(); const setupMatch = useRouteMatch(["/setup", "/migrate"]); // redirect to setup or migrate as needed @@ -134,27 +142,24 @@ export const App: React.FC = () => { return; } - const baseURL = getBaseURL(); + const { status } = systemStatusData.systemStatus; if ( - window.location.pathname !== baseURL + "setup" && - systemStatusData.systemStatus.status === GQL.SystemStatusEnum.Setup + location.pathname !== "/setup" && + status === GQL.SystemStatusEnum.Setup ) { // redirect to setup page - const newURL = new URL("setup", window.location.origin + baseURL); - window.location.href = newURL.toString(); + history.push("/setup"); } if ( - window.location.pathname !== baseURL + "migrate" && - systemStatusData.systemStatus.status === - GQL.SystemStatusEnum.NeedsMigration + location.pathname !== "/migrate" && + status === GQL.SystemStatusEnum.NeedsMigration ) { - // redirect to setup page - const newURL = new URL("migrate", window.location.origin + baseURL); - window.location.href = newURL.toString(); + // redirect to migrate page + history.push("/migrate"); } - }, [systemStatusData]); + }, [systemStatusData, setupMatch, history, location]); function maybeRenderNavbar() { // don't render navbar for setup views @@ -200,7 +205,7 @@ export const App: React.FC = () => { } function maybeRenderReleaseNotes() { - if (setupMatch || config.loading || config.error) { + if (setupMatch || !systemStatusData || config.loading || config.error) { return; } diff --git a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx index 108f9652b..b23f72027 100644 --- a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx @@ -116,6 +116,14 @@ export const SettingsConfigurationPanel: React.FC = () => { onChange={(v) => saveGeneral({ generatedPath: v })} /> + saveGeneral({ cachePath: v })} + /> + { onChange={(v) => saveGeneral({ metadataPath: v })} /> - saveGeneral({ cachePath: v })} - /> - { const [migrateError, setMigrateError] = useState(""); const intl = useIntl(); + const history = useHistory(); // if database path includes path separators, then this is passed through // to the migration path. Extract the base name of the database file. @@ -109,8 +110,7 @@ export const Migrate: React.FC = () => { systemStatus.systemStatus.status !== GQL.SystemStatusEnum.NeedsMigration ) { // redirect to main page - const newURL = new URL("/", window.location.toString()); - window.location.href = newURL.toString(); + history.push("/"); return ; } @@ -122,8 +122,7 @@ export const Migrate: React.FC = () => { backupPath: backupPath ?? "", }); - const newURL = new URL("", window.location.origin + getBaseURL()); - window.location.href = newURL.toString(); + history.push("/"); } catch (e) { if (e instanceof Error) setMigrateError(e.message ?? e.toString()); setMigrateLoading(false); diff --git a/ui/v2.5/src/components/Setup/Setup.tsx b/ui/v2.5/src/components/Setup/Setup.tsx index f8b116a14..d7b8e5c6f 100644 --- a/ui/v2.5/src/components/Setup/Setup.tsx +++ b/ui/v2.5/src/components/Setup/Setup.tsx @@ -14,7 +14,7 @@ import { useConfigureUI, useSystemStatus, } from "src/core/StashService"; -import { Link } from "react-router-dom"; +import { Link, useHistory } from "react-router-dom"; import { ConfigurationContext } from "src/hooks/Config"; import StashConfiguration from "../Settings/StashConfiguration"; import { Icon } from "../Shared/Icon"; @@ -37,14 +37,18 @@ export const Setup: React.FC = () => { const [configLocation, setConfigLocation] = useState(""); const [stashes, setStashes] = useState([]); const [showStashAlert, setShowStashAlert] = useState(false); - const [generatedLocation, setGeneratedLocation] = useState(""); const [databaseFile, setDatabaseFile] = useState(""); + const [generatedLocation, setGeneratedLocation] = useState(""); + const [cacheLocation, setCacheLocation] = useState(""); const [loading, setLoading] = useState(false); const [setupError, setSetupError] = useState(""); const intl = useIntl(); + const history = useHistory(); - const [showGeneratedDialog, setShowGeneratedDialog] = useState(false); + const [showGeneratedSelectDialog, setShowGeneratedSelectDialog] = + useState(false); + const [showCacheSelectDialog, setShowCacheSelectDialog] = useState(false); const { data: systemStatus, loading: statusLoading } = useSystemStatus(); @@ -233,20 +237,20 @@ export const Setup: React.FC = () => { ); } - function onGeneratedClosed(d?: string) { + function onGeneratedSelectClosed(d?: string) { if (d) { setGeneratedLocation(d); } - setShowGeneratedDialog(false); + setShowGeneratedSelectDialog(false); } function maybeRenderGeneratedSelectDialog() { - if (!showGeneratedDialog) { + if (!showGeneratedSelectDialog) { return; } - return ; + return ; } function maybeRenderGenerated() { @@ -279,7 +283,64 @@ export const Setup: React.FC = () => { + + + + ); + } + } + + function onCacheSelectClosed(d?: string) { + if (d) { + setCacheLocation(d); + } + + setShowCacheSelectDialog(false); + } + + function maybeRenderCacheSelectDialog() { + if (!showCacheSelectDialog) { + return; + } + + return ; + } + + function maybeRenderCache() { + if (!configuration?.general.cachePath) { + return ( + +

+ +

+

+ {chunks}, + }} + /> +

+ + ) => + setCacheLocation(e.currentTarget.value) + } + /> + + @@ -328,6 +389,13 @@ export const Setup: React.FC = () => { code: (chunks: string) => {chunks}, }} /> +
+ {chunks}, + }} + />

{ />
{maybeRenderGenerated()} + {maybeRenderCache()}
@@ -404,6 +473,7 @@ export const Setup: React.FC = () => { configLocation, databaseFile, generatedLocation, + cacheLocation, stashes, }); // Set lastNoteSeen to hide release notes dialog @@ -473,6 +543,20 @@ export const Setup: React.FC = () => { +
+
+ +
+
+ + {cacheLocation !== "" + ? cacheLocation + : intl.formatMessage({ + id: "setup.confirm.default_cache_location", + })} + +
+
@@ -592,7 +676,7 @@ export const Setup: React.FC = () => {
- @@ -616,12 +700,12 @@ export const Setup: React.FC = () => { } if ( + step === 0 && systemStatus && systemStatus.systemStatus.status !== GQL.SystemStatusEnum.Setup ) { // redirect to main page - const newURL = new URL("/", window.location.toString()); - window.location.href = newURL.toString(); + history.push("/"); return ; } @@ -654,6 +738,7 @@ export const Setup: React.FC = () => { return ( {maybeRenderGeneratedSelectDialog()} + {maybeRenderCacheSelectDialog()}

diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 5578c8c33..d5aaf80ea 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -331,14 +331,22 @@ export const mutateSetup = (input: GQL.SetupInput) => client.mutate({ mutation: GQL.SetupDocument, variables: { input }, - refetchQueries: getQueryNames([GQL.ConfigurationDocument]), - update: deleteCache([GQL.ConfigurationDocument]), + refetchQueries: getQueryNames([ + GQL.ConfigurationDocument, + GQL.SystemStatusDocument, + ]), + update: deleteCache([GQL.ConfigurationDocument, GQL.SystemStatusDocument]), }); export const mutateMigrate = (input: GQL.MigrateInput) => client.mutate({ mutation: GQL.MigrateDocument, variables: { input }, + refetchQueries: getQueryNames([ + GQL.ConfigurationDocument, + GQL.SystemStatusDocument, + ]), + update: deleteCache([GQL.ConfigurationDocument, GQL.SystemStatusDocument]), }); export const useDirectory = (path?: string) => diff --git a/ui/v2.5/src/core/createClient.ts b/ui/v2.5/src/core/createClient.ts index c76e61097..2fcdaf717 100644 --- a/ui/v2.5/src/core/createClient.ts +++ b/ui/v2.5/src/core/createClient.ts @@ -120,8 +120,8 @@ export const createClient = () => { const platformUrl = getPlatformURL(); const wsPlatformUrl = getPlatformURL(true); - const url = `${platformUrl.toString()}graphql`; - const wsUrl = `${wsPlatformUrl.toString()}graphql`; + const url = `${platformUrl}graphql`; + const wsUrl = `${wsPlatformUrl}graphql`; const httpLink = createUploadLink({ uri: url }); @@ -140,7 +140,7 @@ export const createClient = () => { if (networkError && (networkError as ServerError).statusCode === 401) { // redirect to login page const newURL = new URL( - `${window.STASH_BASE_URL}login`, + `${getBaseURL()}login`, window.location.toString() ); newURL.searchParams.append("returnURL", window.location.href); diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 7fcc59a62..da1964d36 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -334,7 +334,7 @@ "heading": "Scrapers Path" }, "scraping": "Scraping", - "sqlite_location": "File location for the SQLite database (requires restart)", + "sqlite_location": "File location for the SQLite database (requires restart). WARNING: storing the database on a different system to where the Stash server is run from (i.e. over the network) is unsupported!", "video_ext_desc": "Comma-delimited list of file extensions that will be identified as videos.", "video_ext_head": "Video Extensions", "video_head": "Video" @@ -1027,8 +1027,10 @@ "setup": { "confirm": { "almost_ready": "We're almost ready to complete the configuration. Please confirm the following settings. You can click back to change anything incorrect. If everything looks good, click Confirm to create your system.", + "cache_directory": "Cache directory", "configuration_file_location": "Configuration file location:", "database_file_path": "Database file path", + "default_cache_location": "/cache", "default_db_location": "/stash-go.sqlite", "default_generated_content_location": "/generated", "generated_directory": "Generated directory", @@ -1064,12 +1066,16 @@ }, "paths": { "database_filename_empty_for_default": "database filename (empty for default)", - "description": "Next up, we need to determine where to find your porn collection, where to store the stash database and generated files. These settings can be changed later if needed.", + "description": "Next up, we need to determine where to find your porn collection, and where to store the Stash database, generated files and cache files. These settings can be changed later if needed.", + "path_to_cache_directory_empty_for_default": "path to cache directory (empty for default)", "path_to_generated_directory_empty_for_default": "path to generated directory (empty for default)", "set_up_your_paths": "Set up your paths", "stash_alert": "No library paths have been selected. No media will be able to be scanned into Stash. Are you sure?", + "where_can_stash_store_cache_files": "Where can Stash store cache files?", + "where_can_stash_store_cache_files_description": "In order for some functionality like HLS live transcoding to function, Stash requires a cache directory for temporary files. By default, Stash will create a cache directory within the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.", "where_can_stash_store_its_database": "Where can Stash store its database?", - "where_can_stash_store_its_database_description": "Stash uses an sqlite database to store your porn metadata. By default, this will be created as stash-go.sqlite in the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) filename.", + "where_can_stash_store_its_database_description": "Stash uses an SQLite database to store your porn metadata. By default, this will be created as stash-go.sqlite in the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) filename.", + "where_can_stash_store_its_database_warning": "WARNING: storing the database on a different system to where Stash is run from (e.g. storing the database on a NAS while running the Stash server on another computer) is unsupported! SQLite is not intended for use across a network, and attempting to do so can very easily cause your entire database to become corrupted.", "where_can_stash_store_its_generated_content": "Where can Stash store its generated content?", "where_can_stash_store_its_generated_content_description": "In order to provide thumbnails, previews and sprites, Stash generates images and videos. This also includes transcodes for unsupported file formats. By default, Stash will create a generated directory within the directory containing your config file. If you want to change where this generated media will be stored, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.", "where_is_your_porn_located": "Where is your porn located?",