mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Add backup database functionality (#1069)
This commit is contained in:
@@ -37,3 +37,7 @@ mutation MigrateHashNaming {
|
|||||||
mutation StopJob {
|
mutation StopJob {
|
||||||
stopJob
|
stopJob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation BackupDatabase($input: BackupDatabaseInput!) {
|
||||||
|
backupDatabase(input: $input)
|
||||||
|
}
|
||||||
|
|||||||
@@ -228,6 +228,9 @@ type Mutation {
|
|||||||
|
|
||||||
"""Submit fingerprints to stash-box instance"""
|
"""Submit fingerprints to stash-box instance"""
|
||||||
submitStashBoxFingerprints(input: StashBoxFingerprintSubmissionInput!): Boolean!
|
submitStashBoxFingerprints(input: StashBoxFingerprintSubmissionInput!): Boolean!
|
||||||
|
|
||||||
|
"""Backup the database. Optionally returns a link to download the database file"""
|
||||||
|
backupDatabase(input: BackupDatabaseInput!): String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Subscription {
|
||||||
|
|||||||
@@ -92,3 +92,7 @@ input ImportObjectsInput {
|
|||||||
duplicateBehaviour: ImportDuplicateEnum!
|
duplicateBehaviour: ImportDuplicateEnum!
|
||||||
missingRefBehaviour: ImportMissingRefEnum!
|
missingRefBehaviour: ImportMissingRefEnum!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input BackupDatabaseInput {
|
||||||
|
download: Boolean
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ func doMigrateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// perform database backup
|
// 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)
|
http.Error(w, fmt.Sprintf("error backing up database: %s", err), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"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"
|
||||||
"github.com/stashapp/stash/pkg/manager/config"
|
"github.com/stashapp/stash/pkg/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
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) {
|
func (r *mutationResolver) StopJob(ctx context.Context) (bool, error) {
|
||||||
return manager.GetInstance().Status.Stop(), nil
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup the database
|
// Backup the database. If db is nil, then uses the existing database
|
||||||
func Backup(backupPath string) error {
|
// connection.
|
||||||
db, err := sqlx.Connect(sqlite3Driver, "file:"+dbPath+"?_fk=true")
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("Open database %s failed:%s", dbPath, err)
|
return fmt.Errorf("Open database %s failed:%s", dbPath, err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
logger.Infof("Backing up database into: %s", backupPath)
|
logger.Infof("Backing up database into: %s", backupPath)
|
||||||
_, err = db.Exec(`VACUUM INTO "` + backupPath + `"`)
|
_, err := db.Exec(`VACUUM INTO "` + backupPath + `"`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Vacuum failed: %s", err)
|
return fmt.Errorf("Vacuum failed: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,6 @@ func (s *Scenes) Append(o interface{}) {
|
|||||||
*s = append(*s, o.(*Scene))
|
*s = append(*s, o.(*Scene))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Scenes) New() interface{} {
|
func (s *Scenes) New() interface{} {
|
||||||
return &Scene{}
|
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.
|
#### 💥 Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run.
|
||||||
|
|
||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Add backup database functionality to Settings/Tasks.
|
||||||
* Add gallery wall view.
|
* Add gallery wall view.
|
||||||
* Add organized flag for scenes, galleries and images.
|
* Add organized flag for scenes, galleries and images.
|
||||||
* Allow configuration of visible navbar items.
|
* Allow configuration of visible navbar items.
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import {
|
|||||||
mutateStopJob,
|
mutateStopJob,
|
||||||
usePlugins,
|
usePlugins,
|
||||||
mutateRunPluginTask,
|
mutateRunPluginTask,
|
||||||
|
mutateBackupDatabase,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
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 { GenerateButton } from "./GenerateButton";
|
||||||
import { ImportDialog } from "./ImportDialog";
|
import { ImportDialog } from "./ImportDialog";
|
||||||
import { ScanDialog } from "./ScanDialog";
|
import { ScanDialog } from "./ScanDialog";
|
||||||
@@ -30,6 +32,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
||||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState<boolean>(false);
|
const [isImportDialogOpen, setIsImportDialogOpen] = useState<boolean>(false);
|
||||||
const [isScanDialogOpen, setIsScanDialogOpen] = useState<boolean>(false);
|
const [isScanDialogOpen, setIsScanDialogOpen] = useState<boolean>(false);
|
||||||
|
const [isBackupRunning, setIsBackupRunning] = useState<boolean>(false);
|
||||||
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
|
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
|
||||||
const [stripFileExtension, setStripFileExtension] = useState<boolean>(false);
|
const [stripFileExtension, setStripFileExtension] = useState<boolean>(false);
|
||||||
const [scanGeneratePreviews, setScanGeneratePreviews] = useState<boolean>(
|
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() {
|
function renderPlugins() {
|
||||||
if (!plugins.data || !plugins.data.plugins) {
|
if (!plugins.data || !plugins.data.plugins) {
|
||||||
return;
|
return;
|
||||||
@@ -298,6 +320,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isBackupRunning) {
|
||||||
|
return <LoadingIndicator message="Backup up database" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderImportAlert()}
|
{renderImportAlert()}
|
||||||
@@ -478,6 +504,39 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</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()}
|
{renderPlugins()}
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|||||||
@@ -858,6 +858,12 @@ export const mutateImportObjects = (input: GQL.ImportObjectsInput) =>
|
|||||||
variables: { input },
|
variables: { input },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mutateBackupDatabase = (input: GQL.BackupDatabaseInput) =>
|
||||||
|
client.mutate<GQL.BackupDatabaseMutation>({
|
||||||
|
mutation: GQL.BackupDatabaseDocument,
|
||||||
|
variables: { input },
|
||||||
|
});
|
||||||
|
|
||||||
export const querySceneByPathRegex = (filter: GQL.FindFilterType) =>
|
export const querySceneByPathRegex = (filter: GQL.FindFilterType) =>
|
||||||
client.query<GQL.FindScenesByPathRegexQuery>({
|
client.query<GQL.FindScenesByPathRegexQuery>({
|
||||||
query: GQL.FindScenesByPathRegexDocument,
|
query: GQL.FindScenesByPathRegexDocument,
|
||||||
|
|||||||
Reference in New Issue
Block a user