mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add backup database functionality (#1069)
This commit is contained in:
@@ -36,4 +36,8 @@ mutation MigrateHashNaming {
|
||||
|
||||
mutation StopJob {
|
||||
stopJob
|
||||
}
|
||||
}
|
||||
|
||||
mutation BackupDatabase($input: BackupDatabaseInput!) {
|
||||
backupDatabase(input: $input)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -92,3 +92,7 @@ input ImportObjectsInput {
|
||||
duplicateBehaviour: ImportDuplicateEnum!
|
||||
missingRefBehaviour: ImportMissingRefEnum!
|
||||
}
|
||||
|
||||
input BackupDatabaseInput {
|
||||
download: Boolean
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user