mirror of
				https://github.com/photoprism/photoprism.git
				synced 2025-11-01 04:32:49 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			195 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package commands
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"path/filepath"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/dustin/go-humanize/english"
 | |
| 	"github.com/urfave/cli"
 | |
| 
 | |
| 	"github.com/photoprism/photoprism/internal/config"
 | |
| 	"github.com/photoprism/photoprism/internal/photoprism"
 | |
| 	"github.com/photoprism/photoprism/pkg/clean"
 | |
| 	"github.com/photoprism/photoprism/pkg/fs"
 | |
| )
 | |
| 
 | |
| const backupDescription = "A user-defined SQL dump FILENAME or - for stdout can be passed as the first argument. " +
 | |
| 	"The -i parameter can be omitted in this case.\n" +
 | |
| 	"   Make sure to run the command with exec -T when using Docker to prevent log messages from being sent to stdout.\n" +
 | |
| 	"   The index backup and album file paths are automatically detected if not specified explicitly."
 | |
| 
 | |
| // BackupCommand configures the backup cli command.
 | |
| var BackupCommand = cli.Command{
 | |
| 	Name:        "backup",
 | |
| 	Description: backupDescription,
 | |
| 	Usage:       "Creates an index SQL dump and optionally album YAML files organized by type",
 | |
| 	ArgsUsage:   "[filename.sql | -]",
 | |
| 	Flags:       backupFlags,
 | |
| 	Action:      backupAction,
 | |
| }
 | |
| 
 | |
| var backupFlags = []cli.Flag{
 | |
| 	cli.BoolFlag{
 | |
| 		Name:  "force, f",
 | |
| 		Usage: "replace existing files",
 | |
| 	},
 | |
| 	cli.BoolFlag{
 | |
| 		Name:  "albums, a",
 | |
| 		Usage: "create album YAML files organized by type",
 | |
| 	},
 | |
| 	cli.StringFlag{
 | |
| 		Name:  "albums-path",
 | |
| 		Usage: "custom album files `PATH`",
 | |
| 	},
 | |
| 	cli.BoolFlag{
 | |
| 		Name:  "index, i",
 | |
| 		Usage: "create index SQL dump",
 | |
| 	},
 | |
| 	cli.StringFlag{
 | |
| 		Name:  "index-path",
 | |
| 		Usage: "custom index backup `PATH`",
 | |
| 	},
 | |
| }
 | |
| 
 | |
| // backupAction creates a database backup.
 | |
| func backupAction(ctx *cli.Context) error {
 | |
| 	// Use command argument as backup file name.
 | |
| 	indexFileName := ctx.Args().First()
 | |
| 	indexPath := ctx.String("index-path")
 | |
| 	backupIndex := ctx.Bool("index") || indexFileName != "" || indexPath != ""
 | |
| 
 | |
| 	albumsPath := ctx.String("albums-path")
 | |
| 
 | |
| 	backupAlbums := ctx.Bool("albums") || albumsPath != ""
 | |
| 
 | |
| 	if !backupIndex && !backupAlbums {
 | |
| 		return cli.ShowSubcommandHelp(ctx)
 | |
| 	}
 | |
| 
 | |
| 	start := time.Now()
 | |
| 
 | |
| 	conf, err := InitConfig(ctx)
 | |
| 
 | |
| 	_, cancel := context.WithCancel(context.Background())
 | |
| 	defer cancel()
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	conf.RegisterDb()
 | |
| 	defer conf.Shutdown()
 | |
| 
 | |
| 	if backupIndex {
 | |
| 		// If empty, use default backup file name.
 | |
| 		if indexFileName == "" {
 | |
| 			if !fs.PathWritable(indexPath) {
 | |
| 				if indexPath != "" {
 | |
| 					log.Warnf("custom index backup path not writable, using default")
 | |
| 				}
 | |
| 
 | |
| 				indexPath = filepath.Join(conf.BackupPath(), conf.DatabaseDriver())
 | |
| 			}
 | |
| 
 | |
| 			backupFile := time.Now().UTC().Format("2006-01-02") + ".sql"
 | |
| 			indexFileName = filepath.Join(indexPath, backupFile)
 | |
| 		}
 | |
| 
 | |
| 		if indexFileName != "-" {
 | |
| 			if _, err := os.Stat(indexFileName); err == nil && !ctx.Bool("force") {
 | |
| 				return fmt.Errorf("SQL dump already exists: %s", indexFileName)
 | |
| 			} else if err == nil {
 | |
| 				log.Warnf("replacing existing SQL dump")
 | |
| 			}
 | |
| 
 | |
| 			// Create backup directory if not exists.
 | |
| 			if dir := filepath.Dir(indexFileName); dir != "." {
 | |
| 				if err := os.MkdirAll(dir, os.ModePerm); err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			log.Infof("writing SQL dump to %s", clean.Log(indexFileName))
 | |
| 		}
 | |
| 
 | |
| 		var cmd *exec.Cmd
 | |
| 
 | |
| 		switch conf.DatabaseDriver() {
 | |
| 		case config.MySQL, config.MariaDB:
 | |
| 			cmd = exec.Command(
 | |
| 				conf.MysqldumpBin(),
 | |
| 				"--protocol", "tcp",
 | |
| 				"-h", conf.DatabaseHost(),
 | |
| 				"-P", conf.DatabasePortString(),
 | |
| 				"-u", conf.DatabaseUser(),
 | |
| 				"-p"+conf.DatabasePassword(),
 | |
| 				conf.DatabaseName(),
 | |
| 			)
 | |
| 		case config.SQLite3:
 | |
| 			cmd = exec.Command(
 | |
| 				conf.SqliteBin(),
 | |
| 				conf.DatabaseDsn(),
 | |
| 				".dump",
 | |
| 			)
 | |
| 		default:
 | |
| 			return fmt.Errorf("unsupported database type: %s", conf.DatabaseDriver())
 | |
| 		}
 | |
| 
 | |
| 		// Fetch command output.
 | |
| 		var out bytes.Buffer
 | |
| 		var stderr bytes.Buffer
 | |
| 		cmd.Stdout = &out
 | |
| 		cmd.Stderr = &stderr
 | |
| 
 | |
| 		// Log exact command for debugging in trace mode.
 | |
| 		log.Trace(cmd.String())
 | |
| 
 | |
| 		// Run backup command.
 | |
| 		if err := cmd.Run(); err != nil {
 | |
| 			if stderr.String() != "" {
 | |
| 				return errors.New(stderr.String())
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if indexFileName == "-" {
 | |
| 			// Return output via stdout.
 | |
| 			fmt.Println(out.String())
 | |
| 		} else {
 | |
| 			// Write output to file.
 | |
| 			if err := os.WriteFile(indexFileName, []byte(out.String()), os.ModePerm); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if backupAlbums {
 | |
| 		if !fs.PathWritable(albumsPath) {
 | |
| 			if albumsPath != "" {
 | |
| 				log.Warnf("album files path not writable, using default")
 | |
| 			}
 | |
| 
 | |
| 			albumsPath = conf.AlbumsPath()
 | |
| 		}
 | |
| 
 | |
| 		log.Infof("saving albums in %s", clean.Log(albumsPath))
 | |
| 
 | |
| 		if count, err := photoprism.BackupAlbums(albumsPath, true); err != nil {
 | |
| 			return err
 | |
| 		} else {
 | |
| 			log.Infof("created %s", english.Plural(count, "YAML album file", "YAML album files"))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	elapsed := time.Since(start)
 | |
| 
 | |
| 	log.Infof("completed in %s", elapsed)
 | |
| 
 | |
| 	return nil
 | |
| }
 | 
