Add backup database functionality (#1069)

This commit is contained in:
WithoutPants
2021-01-21 22:02:09 +11:00
committed by GitHub
parent 093b997eb1
commit 3b41894dbd
10 changed files with 137 additions and 12 deletions

View File

@@ -36,4 +36,8 @@ mutation MigrateHashNaming {
mutation StopJob {
stopJob
}
}
mutation BackupDatabase($input: BackupDatabaseInput!) {
backupDatabase(input: $input)
}

View File

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

View File

@@ -92,3 +92,7 @@ input ImportObjectsInput {
duplicateBehaviour: ImportDuplicateEnum!
missingRefBehaviour: ImportMissingRefEnum!
}
input BackupDatabaseInput {
download: Boolean
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<boolean>(false);
const [isImportDialogOpen, setIsImportDialogOpen] = useState<boolean>(false);
const [isScanDialogOpen, setIsScanDialogOpen] = useState<boolean>(false);
const [isBackupRunning, setIsBackupRunning] = useState<boolean>(false);
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
const [stripFileExtension, setStripFileExtension] = useState<boolean>(false);
const [scanGeneratePreviews, setScanGeneratePreviews] = useState<boolean>(
@@ -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 <LoadingIndicator message="Backup up database" />;
}
return (
<>
{renderImportAlert()}
@@ -478,6 +504,39 @@ export const SettingsTasksPanel: React.FC = () => {
</Form.Text>
</Form.Group>
<hr />
<h5>Backup</h5>
<Form.Group>
<Button
id="backup"
variant="secondary"
type="submit"
onClick={() => onBackup()}
>
Backup
</Button>
<Form.Text className="text-muted">
Performs a backup of the database to the same directory as the
database, with the filename format{" "}
<code>[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]</code>
</Form.Text>
</Form.Group>
<Form.Group>
<Button
id="backupDownload"
variant="secondary"
type="submit"
onClick={() => onBackup(true)}
>
Download Backup
</Button>
<Form.Text className="text-muted">
Performs a backup of the database and downloads the resulting file.
</Form.Text>
</Form.Group>
{renderPlugins()}
<hr />

View File

@@ -858,6 +858,12 @@ export const mutateImportObjects = (input: GQL.ImportObjectsInput) =>
variables: { input },
});
export const mutateBackupDatabase = (input: GQL.BackupDatabaseInput) =>
client.mutate<GQL.BackupDatabaseMutation>({
mutation: GQL.BackupDatabaseDocument,
variables: { input },
});
export const querySceneByPathRegex = (filter: GQL.FindFilterType) =>
client.query<GQL.FindScenesByPathRegexQuery>({
query: GQL.FindScenesByPathRegexDocument,