Setup improvements (#3504)

* Improve setup redirects
* Add network database warning
* Add cache directory to setup
This commit is contained in:
DingDongSoLong4 2023-03-06 23:28:19 +02:00 committed by GitHub
parent 42fde9bc9f
commit 71e1451c94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 168 additions and 47 deletions

View file

@ -6,6 +6,8 @@ input SetupInput {
databaseFile: String! databaseFile: String!
"""Empty to indicate default""" """Empty to indicate default"""
generatedLocation: String! generatedLocation: String!
"""Empty to indicate default"""
cacheLocation: String!
} }
enum StreamingResolutionEnum { enum StreamingResolutionEnum {

View file

@ -100,6 +100,8 @@ type SetupInput struct {
DatabaseFile string `json:"databaseFile"` DatabaseFile string `json:"databaseFile"`
// Empty to indicate default // Empty to indicate default
GeneratedLocation string `json:"generatedLocation"` GeneratedLocation string `json:"generatedLocation"`
// Empty to indicate default
CacheLocation string `json:"cacheLocation"`
} }
type Manager struct { type Manager struct {
@ -588,6 +590,9 @@ func setSetupDefaults(input *SetupInput) {
if input.GeneratedLocation == "" { if input.GeneratedLocation == "" {
input.GeneratedLocation = filepath.Join(configDir, "generated") input.GeneratedLocation = filepath.Join(configDir, "generated")
} }
if input.CacheLocation == "" {
input.CacheLocation = filepath.Join(configDir, "cache")
}
if input.DatabaseFile == "" { if input.DatabaseFile == "" {
input.DatabaseFile = filepath.Join(configDir, "stash-go.sqlite") 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) 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 // set the configuration
if !c.HasOverride(config.Database) { if !c.HasOverride(config.Database) {
s.Config.Set(config.Database, input.DatabaseFile) s.Config.Set(config.Database, input.DatabaseFile)

View file

@ -1,5 +1,11 @@
import React, { Suspense, useEffect, useState } from "react"; 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 { IntlProvider, CustomFormats } from "react-intl";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import cloneDeep from "lodash-es/cloneDeep"; import cloneDeep from "lodash-es/cloneDeep";
@ -30,7 +36,7 @@ import { InteractiveProvider } from "./hooks/Interactive/context";
import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog"; import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog";
import { IUIConfig } from "./core/config"; import { IUIConfig } from "./core/config";
import { releaseNotes } from "./docs/en/ReleaseNotes"; import { releaseNotes } from "./docs/en/ReleaseNotes";
import { getPlatformURL, getBaseURL } from "./core/createClient"; import { getPlatformURL } from "./core/createClient";
import { lazyComponent } from "./utils/lazyComponent"; import { lazyComponent } from "./utils/lazyComponent";
const Performers = lazyComponent( const Performers = lazyComponent(
@ -126,6 +132,8 @@ export const App: React.FC = () => {
setLocale(); setLocale();
}, [language]); }, [language]);
const location = useLocation();
const history = useHistory();
const setupMatch = useRouteMatch(["/setup", "/migrate"]); const setupMatch = useRouteMatch(["/setup", "/migrate"]);
// redirect to setup or migrate as needed // redirect to setup or migrate as needed
@ -134,27 +142,24 @@ export const App: React.FC = () => {
return; return;
} }
const baseURL = getBaseURL(); const { status } = systemStatusData.systemStatus;
if ( if (
window.location.pathname !== baseURL + "setup" && location.pathname !== "/setup" &&
systemStatusData.systemStatus.status === GQL.SystemStatusEnum.Setup status === GQL.SystemStatusEnum.Setup
) { ) {
// redirect to setup page // redirect to setup page
const newURL = new URL("setup", window.location.origin + baseURL); history.push("/setup");
window.location.href = newURL.toString();
} }
if ( if (
window.location.pathname !== baseURL + "migrate" && location.pathname !== "/migrate" &&
systemStatusData.systemStatus.status === status === GQL.SystemStatusEnum.NeedsMigration
GQL.SystemStatusEnum.NeedsMigration
) { ) {
// redirect to setup page // redirect to migrate page
const newURL = new URL("migrate", window.location.origin + baseURL); history.push("/migrate");
window.location.href = newURL.toString();
} }
}, [systemStatusData]); }, [systemStatusData, setupMatch, history, location]);
function maybeRenderNavbar() { function maybeRenderNavbar() {
// don't render navbar for setup views // don't render navbar for setup views
@ -200,7 +205,7 @@ export const App: React.FC = () => {
} }
function maybeRenderReleaseNotes() { function maybeRenderReleaseNotes() {
if (setupMatch || config.loading || config.error) { if (setupMatch || !systemStatusData || config.loading || config.error) {
return; return;
} }

View file

@ -116,6 +116,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
onChange={(v) => saveGeneral({ generatedPath: v })} onChange={(v) => saveGeneral({ generatedPath: v })}
/> />
<StringSetting
id="cache-path"
headingID="config.general.cache_path_head"
subHeadingID="config.general.cache_location"
value={general.cachePath ?? undefined}
onChange={(v) => saveGeneral({ cachePath: v })}
/>
<StringSetting <StringSetting
id="scrapers-path" id="scrapers-path"
headingID="config.general.scrapers_path.heading" headingID="config.general.scrapers_path.heading"
@ -132,14 +140,6 @@ export const SettingsConfigurationPanel: React.FC = () => {
onChange={(v) => saveGeneral({ metadataPath: v })} onChange={(v) => saveGeneral({ metadataPath: v })}
/> />
<StringSetting
id="cache-path"
headingID="config.general.cache_path_head"
subHeadingID="config.general.cache_location"
value={general.cachePath ?? undefined}
onChange={(v) => saveGeneral({ cachePath: v })}
/>
<StringSetting <StringSetting
id="custom-performer-image-location" id="custom-performer-image-location"
headingID="config.ui.performers.options.image_location.heading" headingID="config.ui.performers.options.image_location.heading"

View file

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { Button, Card, Container, Form } from "react-bootstrap"; import { Button, Card, Container, Form } from "react-bootstrap";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { getBaseURL } from "src/core/createClient"; import { useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useSystemStatus, mutateMigrate } from "src/core/StashService"; import { useSystemStatus, mutateMigrate } from "src/core/StashService";
import { migrationNotes } from "src/docs/en/MigrationNotes"; import { migrationNotes } from "src/docs/en/MigrationNotes";
@ -15,6 +15,7 @@ export const Migrate: React.FC = () => {
const [migrateError, setMigrateError] = useState(""); const [migrateError, setMigrateError] = useState("");
const intl = useIntl(); const intl = useIntl();
const history = useHistory();
// if database path includes path separators, then this is passed through // if database path includes path separators, then this is passed through
// to the migration path. Extract the base name of the database file. // 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 systemStatus.systemStatus.status !== GQL.SystemStatusEnum.NeedsMigration
) { ) {
// redirect to main page // redirect to main page
const newURL = new URL("/", window.location.toString()); history.push("/");
window.location.href = newURL.toString();
return <LoadingIndicator />; return <LoadingIndicator />;
} }
@ -122,8 +122,7 @@ export const Migrate: React.FC = () => {
backupPath: backupPath ?? "", backupPath: backupPath ?? "",
}); });
const newURL = new URL("", window.location.origin + getBaseURL()); history.push("/");
window.location.href = newURL.toString();
} catch (e) { } catch (e) {
if (e instanceof Error) setMigrateError(e.message ?? e.toString()); if (e instanceof Error) setMigrateError(e.message ?? e.toString());
setMigrateLoading(false); setMigrateLoading(false);

View file

@ -14,7 +14,7 @@ import {
useConfigureUI, useConfigureUI,
useSystemStatus, useSystemStatus,
} from "src/core/StashService"; } from "src/core/StashService";
import { Link } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { ConfigurationContext } from "src/hooks/Config"; import { ConfigurationContext } from "src/hooks/Config";
import StashConfiguration from "../Settings/StashConfiguration"; import StashConfiguration from "../Settings/StashConfiguration";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
@ -37,14 +37,18 @@ export const Setup: React.FC = () => {
const [configLocation, setConfigLocation] = useState(""); const [configLocation, setConfigLocation] = useState("");
const [stashes, setStashes] = useState<GQL.StashConfig[]>([]); const [stashes, setStashes] = useState<GQL.StashConfig[]>([]);
const [showStashAlert, setShowStashAlert] = useState(false); const [showStashAlert, setShowStashAlert] = useState(false);
const [generatedLocation, setGeneratedLocation] = useState("");
const [databaseFile, setDatabaseFile] = useState(""); const [databaseFile, setDatabaseFile] = useState("");
const [generatedLocation, setGeneratedLocation] = useState("");
const [cacheLocation, setCacheLocation] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [setupError, setSetupError] = useState(""); const [setupError, setSetupError] = useState("");
const intl = useIntl(); 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(); 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) { if (d) {
setGeneratedLocation(d); setGeneratedLocation(d);
} }
setShowGeneratedDialog(false); setShowGeneratedSelectDialog(false);
} }
function maybeRenderGeneratedSelectDialog() { function maybeRenderGeneratedSelectDialog() {
if (!showGeneratedDialog) { if (!showGeneratedSelectDialog) {
return; return;
} }
return <FolderSelectDialog onClose={onGeneratedClosed} />; return <FolderSelectDialog onClose={onGeneratedSelectClosed} />;
} }
function maybeRenderGenerated() { function maybeRenderGenerated() {
@ -279,7 +283,64 @@ export const Setup: React.FC = () => {
<Button <Button
variant="secondary" variant="secondary"
className="text-input" className="text-input"
onClick={() => setShowGeneratedDialog(true)} onClick={() => setShowGeneratedSelectDialog(true)}
>
<Icon icon={faEllipsisH} />
</Button>
</InputGroup.Append>
</InputGroup>
</Form.Group>
);
}
}
function onCacheSelectClosed(d?: string) {
if (d) {
setCacheLocation(d);
}
setShowCacheSelectDialog(false);
}
function maybeRenderCacheSelectDialog() {
if (!showCacheSelectDialog) {
return;
}
return <FolderSelectDialog onClose={onCacheSelectClosed} />;
}
function maybeRenderCache() {
if (!configuration?.general.cachePath) {
return (
<Form.Group id="cache">
<h3>
<FormattedMessage id="setup.paths.where_can_stash_store_cache_files" />
</h3>
<p>
<FormattedMessage
id="setup.paths.where_can_stash_store_cache_files_description"
values={{
code: (chunks: string) => <code>{chunks}</code>,
}}
/>
</p>
<InputGroup>
<Form.Control
className="text-input"
value={cacheLocation}
placeholder={intl.formatMessage({
id: "setup.paths.path_to_cache_directory_empty_for_default",
})}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCacheLocation(e.currentTarget.value)
}
/>
<InputGroup.Append>
<Button
variant="secondary"
className="text-input"
onClick={() => setShowCacheSelectDialog(true)}
> >
<Icon icon={faEllipsisH} /> <Icon icon={faEllipsisH} />
</Button> </Button>
@ -328,6 +389,13 @@ export const Setup: React.FC = () => {
code: (chunks: string) => <code>{chunks}</code>, code: (chunks: string) => <code>{chunks}</code>,
}} }}
/> />
<br />
<FormattedMessage
id="setup.paths.where_can_stash_store_its_database_warning"
values={{
strong: (chunks: string) => <strong>{chunks}</strong>,
}}
/>
</p> </p>
<Form.Control <Form.Control
className="text-input" className="text-input"
@ -341,6 +409,7 @@ export const Setup: React.FC = () => {
/> />
</Form.Group> </Form.Group>
{maybeRenderGenerated()} {maybeRenderGenerated()}
{maybeRenderCache()}
</section> </section>
<section className="mt-5"> <section className="mt-5">
<div className="d-flex justify-content-center"> <div className="d-flex justify-content-center">
@ -404,6 +473,7 @@ export const Setup: React.FC = () => {
configLocation, configLocation,
databaseFile, databaseFile,
generatedLocation, generatedLocation,
cacheLocation,
stashes, stashes,
}); });
// Set lastNoteSeen to hide release notes dialog // Set lastNoteSeen to hide release notes dialog
@ -473,6 +543,20 @@ export const Setup: React.FC = () => {
</code> </code>
</dd> </dd>
</dl> </dl>
<dl>
<dt>
<FormattedMessage id="setup.confirm.cache_directory" />
</dt>
<dd>
<code>
{cacheLocation !== ""
? cacheLocation
: intl.formatMessage({
id: "setup.confirm.default_cache_location",
})}
</code>
</dd>
</dl>
</section> </section>
<section className="mt-5"> <section className="mt-5">
<div className="d-flex justify-content-center"> <div className="d-flex justify-content-center">
@ -592,7 +676,7 @@ export const Setup: React.FC = () => {
<section className="mt-5"> <section className="mt-5">
<div className="d-flex justify-content-center"> <div className="d-flex justify-content-center">
<Link to="/settings?tab=library"> <Link to="/settings?tab=library">
<Button variant="success mx-2 p-5" onClick={() => goBack(2)}> <Button variant="success mx-2 p-5">
<FormattedMessage id="actions.finish" /> <FormattedMessage id="actions.finish" />
</Button> </Button>
</Link> </Link>
@ -616,12 +700,12 @@ export const Setup: React.FC = () => {
} }
if ( if (
step === 0 &&
systemStatus && systemStatus &&
systemStatus.systemStatus.status !== GQL.SystemStatusEnum.Setup systemStatus.systemStatus.status !== GQL.SystemStatusEnum.Setup
) { ) {
// redirect to main page // redirect to main page
const newURL = new URL("/", window.location.toString()); history.push("/");
window.location.href = newURL.toString();
return <LoadingIndicator />; return <LoadingIndicator />;
} }
@ -654,6 +738,7 @@ export const Setup: React.FC = () => {
return ( return (
<Container> <Container>
{maybeRenderGeneratedSelectDialog()} {maybeRenderGeneratedSelectDialog()}
{maybeRenderCacheSelectDialog()}
<h1 className="text-center"> <h1 className="text-center">
<FormattedMessage id="setup.stash_setup_wizard" /> <FormattedMessage id="setup.stash_setup_wizard" />
</h1> </h1>

View file

@ -331,14 +331,22 @@ export const mutateSetup = (input: GQL.SetupInput) =>
client.mutate<GQL.SetupMutation>({ client.mutate<GQL.SetupMutation>({
mutation: GQL.SetupDocument, mutation: GQL.SetupDocument,
variables: { input }, variables: { input },
refetchQueries: getQueryNames([GQL.ConfigurationDocument]), refetchQueries: getQueryNames([
update: deleteCache([GQL.ConfigurationDocument]), GQL.ConfigurationDocument,
GQL.SystemStatusDocument,
]),
update: deleteCache([GQL.ConfigurationDocument, GQL.SystemStatusDocument]),
}); });
export const mutateMigrate = (input: GQL.MigrateInput) => export const mutateMigrate = (input: GQL.MigrateInput) =>
client.mutate<GQL.MigrateMutation>({ client.mutate<GQL.MigrateMutation>({
mutation: GQL.MigrateDocument, mutation: GQL.MigrateDocument,
variables: { input }, variables: { input },
refetchQueries: getQueryNames([
GQL.ConfigurationDocument,
GQL.SystemStatusDocument,
]),
update: deleteCache([GQL.ConfigurationDocument, GQL.SystemStatusDocument]),
}); });
export const useDirectory = (path?: string) => export const useDirectory = (path?: string) =>

View file

@ -120,8 +120,8 @@ export const createClient = () => {
const platformUrl = getPlatformURL(); const platformUrl = getPlatformURL();
const wsPlatformUrl = getPlatformURL(true); const wsPlatformUrl = getPlatformURL(true);
const url = `${platformUrl.toString()}graphql`; const url = `${platformUrl}graphql`;
const wsUrl = `${wsPlatformUrl.toString()}graphql`; const wsUrl = `${wsPlatformUrl}graphql`;
const httpLink = createUploadLink({ uri: url }); const httpLink = createUploadLink({ uri: url });
@ -140,7 +140,7 @@ export const createClient = () => {
if (networkError && (networkError as ServerError).statusCode === 401) { if (networkError && (networkError as ServerError).statusCode === 401) {
// redirect to login page // redirect to login page
const newURL = new URL( const newURL = new URL(
`${window.STASH_BASE_URL}login`, `${getBaseURL()}login`,
window.location.toString() window.location.toString()
); );
newURL.searchParams.append("returnURL", window.location.href); newURL.searchParams.append("returnURL", window.location.href);

View file

@ -334,7 +334,7 @@
"heading": "Scrapers Path" "heading": "Scrapers Path"
}, },
"scraping": "Scraping", "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_desc": "Comma-delimited list of file extensions that will be identified as videos.",
"video_ext_head": "Video Extensions", "video_ext_head": "Video Extensions",
"video_head": "Video" "video_head": "Video"
@ -1027,8 +1027,10 @@
"setup": { "setup": {
"confirm": { "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.", "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:", "configuration_file_location": "Configuration file location:",
"database_file_path": "Database file path", "database_file_path": "Database file path",
"default_cache_location": "<path containing configuration file>/cache",
"default_db_location": "<path containing configuration file>/stash-go.sqlite", "default_db_location": "<path containing configuration file>/stash-go.sqlite",
"default_generated_content_location": "<path containing configuration file>/generated", "default_generated_content_location": "<path containing configuration file>/generated",
"generated_directory": "Generated directory", "generated_directory": "Generated directory",
@ -1064,12 +1066,16 @@
}, },
"paths": { "paths": {
"database_filename_empty_for_default": "database filename (empty for default)", "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)", "path_to_generated_directory_empty_for_default": "path to generated directory (empty for default)",
"set_up_your_paths": "Set up your paths", "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?", "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 <code>cache</code> 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": "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 <code>stash-go.sqlite</code> 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 <code>stash-go.sqlite</code> 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 <strong>unsupported</strong>! 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": "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 <code>generated</code> 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_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 <code>generated</code> 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?", "where_is_your_porn_located": "Where is your porn located?",