diff --git a/graphql/documents/mutations/metadata.graphql b/graphql/documents/mutations/metadata.graphql index 0b94728af..e7346bca5 100644 --- a/graphql/documents/mutations/metadata.graphql +++ b/graphql/documents/mutations/metadata.graphql @@ -36,4 +36,8 @@ mutation MigrateHashNaming { mutation StopJob { stopJob -} \ No newline at end of file +} + +mutation BackupDatabase($input: BackupDatabaseInput!) { + backupDatabase(input: $input) +} diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index d69d4f77c..5fc6dd48b 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -226,8 +226,11 @@ type Mutation { stopJob: Boolean! - """ Submit fingerprints to stash-box instance """ + """Submit fingerprints to stash-box instance""" submitStashBoxFingerprints(input: StashBoxFingerprintSubmissionInput!): Boolean! + + """Backup the database. Optionally returns a link to download the database file""" + backupDatabase(input: BackupDatabaseInput!): String } type Subscription { diff --git a/graphql/schema/types/metadata.graphql b/graphql/schema/types/metadata.graphql index a62028c29..cd88bfad8 100644 --- a/graphql/schema/types/metadata.graphql +++ b/graphql/schema/types/metadata.graphql @@ -92,3 +92,7 @@ input ImportObjectsInput { duplicateBehaviour: ImportDuplicateEnum! missingRefBehaviour: ImportMissingRefEnum! } + +input BackupDatabaseInput { + download: Boolean +} \ No newline at end of file diff --git a/pkg/api/migrate.go b/pkg/api/migrate.go index 929f750fb..4a7bcddf8 100644 --- a/pkg/api/migrate.go +++ b/pkg/api/migrate.go @@ -60,7 +60,7 @@ func doMigrateHandler(w http.ResponseWriter, r *http.Request) { } // perform database backup - if err = database.Backup(backupPath); err != nil { + if err = database.Backup(database.DB, backupPath); err != nil { http.Error(w, fmt.Sprintf("error backing up database: %s", err), 500) return } diff --git a/pkg/api/resolver_mutation_metadata.go b/pkg/api/resolver_mutation_metadata.go index 5423289e2..c0b9d2057 100644 --- a/pkg/api/resolver_mutation_metadata.go +++ b/pkg/api/resolver_mutation_metadata.go @@ -2,11 +2,16 @@ package api import ( "context" + "io/ioutil" + "path/filepath" "time" + "github.com/stashapp/stash/pkg/database" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/manager" "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/utils" ) func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) { @@ -89,3 +94,42 @@ func (r *mutationResolver) JobStatus(ctx context.Context) (*models.MetadataUpdat func (r *mutationResolver) StopJob(ctx context.Context) (bool, error) { return manager.GetInstance().Status.Stop(), nil } + +func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.BackupDatabaseInput) (*string, error) { + // if download is true, then backup to temporary file and return a link + download := input.Download != nil && *input.Download + mgr := manager.GetInstance() + var backupPath string + if download { + utils.EnsureDir(mgr.Paths.Generated.Downloads) + f, err := ioutil.TempFile(mgr.Paths.Generated.Downloads, "backup*.sqlite") + if err != nil { + return nil, err + } + + backupPath = f.Name() + f.Close() + } else { + backupPath = database.DatabaseBackupPath() + } + + err := database.Backup(database.DB, backupPath) + if err != nil { + return nil, err + } + + if download { + downloadHash := mgr.DownloadStore.RegisterFile(backupPath, "", false) + logger.Debugf("Generated backup file %s with hash %s", backupPath, downloadHash) + + baseURL, _ := ctx.Value(BaseURLCtxKey).(string) + + fn := filepath.Base(database.DatabaseBackupPath()) + ret := baseURL + "/downloads/" + downloadHash + "/" + fn + return &ret, nil + } else { + logger.Infof("Successfully backed up database to: %s", backupPath) + } + + return nil, nil +} diff --git a/pkg/database/database.go b/pkg/database/database.go index 4fef0cedd..5dd642c71 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -99,16 +99,20 @@ func Reset(databasePath string) error { return nil } -// Backup the database -func Backup(backupPath string) error { - db, err := sqlx.Connect(sqlite3Driver, "file:"+dbPath+"?_fk=true") - if err != nil { - return fmt.Errorf("Open database %s failed:%s", dbPath, err) +// Backup the database. If db is nil, then uses the existing database +// connection. +func Backup(db *sqlx.DB, backupPath string) error { + if db == nil { + var err error + db, err = sqlx.Connect(sqlite3Driver, "file:"+dbPath+"?_fk=true") + if err != nil { + return fmt.Errorf("Open database %s failed:%s", dbPath, err) + } + defer db.Close() } - defer db.Close() logger.Infof("Backing up database into: %s", backupPath) - _, err = db.Exec(`VACUUM INTO "` + backupPath + `"`) + _, err := db.Exec(`VACUUM INTO "` + backupPath + `"`) if err != nil { return fmt.Errorf("Vacuum failed: %s", err) } diff --git a/pkg/models/model_scene.go b/pkg/models/model_scene.go index 5159a2b0a..3d1abbf76 100644 --- a/pkg/models/model_scene.go +++ b/pkg/models/model_scene.go @@ -102,6 +102,6 @@ func (s *Scenes) Append(o interface{}) { *s = append(*s, o.(*Scene)) } -func (m *Scenes) New() interface{} { +func (s *Scenes) New() interface{} { return &Scene{} } diff --git a/ui/v2.5/src/components/Changelog/versions/v050.md b/ui/v2.5/src/components/Changelog/versions/v050.md index 9cdcadf85..b0db953e3 100644 --- a/ui/v2.5/src/components/Changelog/versions/v050.md +++ b/ui/v2.5/src/components/Changelog/versions/v050.md @@ -1,6 +1,7 @@ #### 💥 Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run. ### ✨ New Features +* Add backup database functionality to Settings/Tasks. * Add gallery wall view. * Add organized flag for scenes, galleries and images. * Allow configuration of visible navbar items. diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index 89fddc33e..a8d7bd5de 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -13,10 +13,12 @@ import { mutateStopJob, usePlugins, mutateRunPluginTask, + mutateBackupDatabase, } from "src/core/StashService"; import { useToast } from "src/hooks"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; +import { LoadingIndicator, Modal } from "src/components/Shared"; +import { downloadFile } from "src/utils"; import { GenerateButton } from "./GenerateButton"; import { ImportDialog } from "./ImportDialog"; import { ScanDialog } from "./ScanDialog"; @@ -30,6 +32,7 @@ export const SettingsTasksPanel: React.FC = () => { const [isCleanAlertOpen, setIsCleanAlertOpen] = useState(false); const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); const [isScanDialogOpen, setIsScanDialogOpen] = useState(false); + const [isBackupRunning, setIsBackupRunning] = useState(false); const [useFileMetadata, setUseFileMetadata] = useState(false); const [stripFileExtension, setStripFileExtension] = useState(false); const [scanGeneratePreviews, setScanGeneratePreviews] = useState( @@ -276,6 +279,25 @@ export const SettingsTasksPanel: React.FC = () => { }); } + async function onBackup(download?: boolean) { + try { + setIsBackupRunning(true); + const ret = await mutateBackupDatabase({ + download, + }); + + // download the result + if (download && ret.data && ret.data.backupDatabase) { + const link = ret.data.backupDatabase; + downloadFile(link); + } + } catch (e) { + Toast.error(e); + } finally { + setIsBackupRunning(false); + } + } + function renderPlugins() { if (!plugins.data || !plugins.data.plugins) { return; @@ -298,6 +320,10 @@ export const SettingsTasksPanel: React.FC = () => { ); } + if (isBackupRunning) { + return ; + } + return ( <> {renderImportAlert()} @@ -478,6 +504,39 @@ export const SettingsTasksPanel: React.FC = () => { +
+ +
Backup
+ + + + Performs a backup of the database to the same directory as the + database, with the filename format{" "} + [origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS] + + + + + + + Performs a backup of the database and downloads the resulting file. + + + {renderPlugins()}
diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 4992fea16..6373b8ec9 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -858,6 +858,12 @@ export const mutateImportObjects = (input: GQL.ImportObjectsInput) => variables: { input }, }); +export const mutateBackupDatabase = (input: GQL.BackupDatabaseInput) => + client.mutate({ + mutation: GQL.BackupDatabaseDocument, + variables: { input }, + }); + export const querySceneByPathRegex = (filter: GQL.FindFilterType) => client.query({ query: GQL.FindScenesByPathRegexDocument,