stash/internal/manager/backup.go
WithoutPants c15e6a5b63
Include blobs in backup (#6586)
* Optionally backup blobs into zip
* Add backup dialog
2026-02-20 09:13:55 +11:00

185 lines
4.9 KiB
Go

package manager
import (
"archive/zip"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
)
type databaseBackupZip struct {
*zip.Writer
}
func (z *databaseBackupZip) zipFileRename(fn, outDir, outFn string) error {
p := filepath.Join(outDir, outFn)
p = filepath.ToSlash(p)
f, err := z.Create(p)
if err != nil {
return fmt.Errorf("error creating zip entry for %s: %v", fn, err)
}
i, err := os.Open(fn)
if err != nil {
return fmt.Errorf("error opening %s: %v", fn, err)
}
defer i.Close()
if _, err := io.Copy(f, i); err != nil {
return fmt.Errorf("error writing %s to zip: %v", fn, err)
}
return nil
}
func (z *databaseBackupZip) zipFile(fn, outDir string) error {
return z.zipFileRename(fn, outDir, filepath.Base(fn))
}
func (s *Manager) BackupDatabase(download bool, includeBlobs bool) (string, string, error) {
var backupPath string
var backupName string
// if we include blobs, then the output is a zip file
// if not, using the same backup logic as before, which creates a sqlite file
if !includeBlobs || s.Config.GetBlobsStorage() != config.BlobStorageTypeFilesystem {
return s.backupDatabaseOnly(download)
}
// use tmp directory for the backup
backupDir := s.Paths.Generated.Tmp
if err := fsutil.EnsureDir(backupDir); err != nil {
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
}
f, err := os.CreateTemp(backupDir, "backup*.sqlite")
if err != nil {
return "", "", err
}
backupPath = f.Name()
backupName = s.Database.DatabaseBackupPath("")
f.Close()
// delete the temp file so that the backup operation can create it
if err := os.Remove(backupPath); err != nil {
return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err)
}
if err := s.Database.Backup(backupPath); err != nil {
return "", "", err
}
// create a zip file
zipFileDir := s.Paths.Generated.Downloads
if !download {
zipFileDir = s.Config.GetBackupDirectoryPathOrDefault()
if zipFileDir != "" {
if err := fsutil.EnsureDir(zipFileDir); err != nil {
return "", "", fmt.Errorf("could not create backup directory %v: %w", zipFileDir, err)
}
}
}
zipFileName := backupName + ".zip"
zipFilePath := filepath.Join(zipFileDir, zipFileName)
logger.Debugf("Preparing zip file for database backup at %v", zipFilePath)
zf, err := os.Create(zipFilePath)
if err != nil {
return "", "", fmt.Errorf("could not create zip file %v: %w", zipFilePath, err)
}
defer zf.Close()
z := databaseBackupZip{
Writer: zip.NewWriter(zf),
}
defer z.Close()
// move the database file into the zip
dbFn := filepath.Base(s.Config.GetDatabasePath())
if err := z.zipFileRename(backupPath, "", dbFn); err != nil {
return "", "", fmt.Errorf("could not add database backup to zip file: %w", err)
}
if err := os.Remove(backupPath); err != nil {
return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err)
}
// walk the blobs directory and add files to the zip
blobsDir := s.Config.GetBlobsPath()
err = filepath.WalkDir(blobsDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
// calculate out dir by removing the blobsDir prefix from the path
outDir := filepath.Join("blobs", strings.TrimPrefix(filepath.Dir(path), blobsDir))
if err := z.zipFile(path, outDir); err != nil {
return fmt.Errorf("could not add blob %v to zip file: %w", path, err)
}
return nil
})
if err != nil {
return "", "", fmt.Errorf("error walking blobs directory: %w", err)
}
return zipFilePath, zipFileName, nil
}
func (s *Manager) backupDatabaseOnly(download bool) (string, string, error) {
var backupPath string
var backupName string
if download {
backupDir := s.Paths.Generated.Downloads
if err := fsutil.EnsureDir(backupDir); err != nil {
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
}
f, err := os.CreateTemp(backupDir, "backup*.sqlite")
if err != nil {
return "", "", err
}
backupPath = f.Name()
backupName = s.Database.DatabaseBackupPath("")
f.Close()
// delete the temp file so that the backup operation can create it
if err := os.Remove(backupPath); err != nil {
return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err)
}
} else {
backupDir := s.Config.GetBackupDirectoryPathOrDefault()
if backupDir != "" {
if err := fsutil.EnsureDir(backupDir); err != nil {
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
}
}
backupPath = s.Database.DatabaseBackupPath(backupDir)
backupName = filepath.Base(backupPath)
}
err := s.Database.Backup(backupPath)
if err != nil {
return "", "", err
}
return backupPath, backupName, nil
}