code.gitea.io/gitea@v1.22.3/cmd/dump.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2016 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package cmd
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	"code.gitea.io/gitea/modules/dump"
    16  	"code.gitea.io/gitea/modules/json"
    17  	"code.gitea.io/gitea/modules/log"
    18  	"code.gitea.io/gitea/modules/setting"
    19  	"code.gitea.io/gitea/modules/storage"
    20  	"code.gitea.io/gitea/modules/util"
    21  
    22  	"gitea.com/go-chi/session"
    23  	"github.com/mholt/archiver/v3"
    24  	"github.com/urfave/cli/v2"
    25  )
    26  
    27  // CmdDump represents the available dump sub-command.
    28  var CmdDump = &cli.Command{
    29  	Name:        "dump",
    30  	Usage:       "Dump Gitea files and database",
    31  	Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
    32  	Action:      runDump,
    33  	Flags: []cli.Flag{
    34  		&cli.StringFlag{
    35  			Name:    "file",
    36  			Aliases: []string{"f"},
    37  			Usage:   `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
    38  		},
    39  		&cli.BoolFlag{
    40  			Name:    "verbose",
    41  			Aliases: []string{"V"},
    42  			Usage:   "Show process details",
    43  		},
    44  		&cli.BoolFlag{
    45  			Name:    "quiet",
    46  			Aliases: []string{"q"},
    47  			Usage:   "Only display warnings and errors",
    48  		},
    49  		&cli.StringFlag{
    50  			Name:    "tempdir",
    51  			Aliases: []string{"t"},
    52  			Value:   os.TempDir(),
    53  			Usage:   "Temporary dir path",
    54  		},
    55  		&cli.StringFlag{
    56  			Name:    "database",
    57  			Aliases: []string{"d"},
    58  			Usage:   "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres",
    59  		},
    60  		&cli.BoolFlag{
    61  			Name:    "skip-repository",
    62  			Aliases: []string{"R"},
    63  			Usage:   "Skip the repository dumping",
    64  		},
    65  		&cli.BoolFlag{
    66  			Name:    "skip-log",
    67  			Aliases: []string{"L"},
    68  			Usage:   "Skip the log dumping",
    69  		},
    70  		&cli.BoolFlag{
    71  			Name:  "skip-custom-dir",
    72  			Usage: "Skip custom directory",
    73  		},
    74  		&cli.BoolFlag{
    75  			Name:  "skip-lfs-data",
    76  			Usage: "Skip LFS data",
    77  		},
    78  		&cli.BoolFlag{
    79  			Name:  "skip-attachment-data",
    80  			Usage: "Skip attachment data",
    81  		},
    82  		&cli.BoolFlag{
    83  			Name:  "skip-package-data",
    84  			Usage: "Skip package data",
    85  		},
    86  		&cli.BoolFlag{
    87  			Name:  "skip-index",
    88  			Usage: "Skip bleve index data",
    89  		},
    90  		&cli.BoolFlag{
    91  			Name:  "skip-db",
    92  			Usage: "Skip database",
    93  		},
    94  		&cli.StringFlag{
    95  			Name:  "type",
    96  			Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
    97  		},
    98  	},
    99  }
   100  
   101  func fatal(format string, args ...any) {
   102  	log.Fatal(format, args...)
   103  }
   104  
   105  func runDump(ctx *cli.Context) error {
   106  	setting.MustInstalled()
   107  
   108  	quite := ctx.Bool("quiet")
   109  	verbose := ctx.Bool("verbose")
   110  	if verbose && quite {
   111  		fatal("Option --quiet and --verbose cannot both be set")
   112  	}
   113  
   114  	// outFileName is either "-" or a file name (will be made absolute)
   115  	outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
   116  	if outType == "" {
   117  		fatal("Invalid output type")
   118  	}
   119  
   120  	outFile := os.Stdout
   121  	if outFileName != "-" {
   122  		var err error
   123  		if outFileName, err = filepath.Abs(outFileName); err != nil {
   124  			fatal("Unable to get absolute path of dump file: %v", err)
   125  		}
   126  		if exist, _ := util.IsExist(outFileName); exist {
   127  			fatal("Dump file %q exists", outFileName)
   128  		}
   129  		if outFile, err = os.Create(outFileName); err != nil {
   130  			fatal("Unable to create dump file %q: %v", outFileName, err)
   131  		}
   132  		defer outFile.Close()
   133  	}
   134  
   135  	setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
   136  
   137  	setting.DisableLoggerInit()
   138  	setting.LoadSettings() // cannot access session settings otherwise
   139  
   140  	stdCtx, cancel := installSignals()
   141  	defer cancel()
   142  
   143  	err := db.InitEngine(stdCtx)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	if err = storage.Init(); err != nil {
   149  		return err
   150  	}
   151  
   152  	archiverGeneric, err := archiver.ByExtension("." + outType)
   153  	if err != nil {
   154  		fatal("Unable to get archiver for extension: %v", err)
   155  	}
   156  
   157  	archiverWriter := archiverGeneric.(archiver.Writer)
   158  	if err := archiverWriter.Create(outFile); err != nil {
   159  		fatal("Creating archiver.Writer failed: %v", err)
   160  	}
   161  	defer archiverWriter.Close()
   162  
   163  	dumper := &dump.Dumper{
   164  		Writer:  archiverWriter,
   165  		Verbose: verbose,
   166  	}
   167  	dumper.GlobalExcludeAbsPath(outFileName)
   168  
   169  	if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
   170  		log.Info("Skip dumping local repositories")
   171  	} else {
   172  		log.Info("Dumping local repositories... %s", setting.RepoRootPath)
   173  		if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
   174  			fatal("Failed to include repositories: %v", err)
   175  		}
   176  
   177  		if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
   178  			log.Info("Skip dumping LFS data")
   179  		} else if !setting.LFS.StartServer {
   180  			log.Info("LFS isn't enabled. Skip dumping LFS data")
   181  		} else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
   182  			info, err := object.Stat()
   183  			if err != nil {
   184  				return err
   185  			}
   186  			return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
   187  		}); err != nil {
   188  			fatal("Failed to dump LFS objects: %v", err)
   189  		}
   190  	}
   191  
   192  	if ctx.Bool("skip-db") {
   193  		// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
   194  		dumper.GlobalExcludeAbsPath(setting.Database.Path)
   195  		log.Info("Skipping database")
   196  	} else {
   197  		tmpDir := ctx.String("tempdir")
   198  		if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
   199  			fatal("Path does not exist: %s", tmpDir)
   200  		}
   201  
   202  		dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql")
   203  		if err != nil {
   204  			fatal("Failed to create tmp file: %v", err)
   205  		}
   206  		defer func() {
   207  			_ = dbDump.Close()
   208  			if err := util.Remove(dbDump.Name()); err != nil {
   209  				log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
   210  			}
   211  		}()
   212  
   213  		targetDBType := ctx.String("database")
   214  		if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
   215  			log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
   216  		} else {
   217  			log.Info("Dumping database...")
   218  		}
   219  
   220  		if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
   221  			fatal("Failed to dump database: %v", err)
   222  		}
   223  
   224  		if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
   225  			fatal("Failed to include gitea-db.sql: %v", err)
   226  		}
   227  	}
   228  
   229  	log.Info("Adding custom configuration file from %s", setting.CustomConf)
   230  	if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
   231  		fatal("Failed to include specified app.ini: %v", err)
   232  	}
   233  
   234  	if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
   235  		log.Info("Skipping custom directory")
   236  	} else {
   237  		customDir, err := os.Stat(setting.CustomPath)
   238  		if err == nil && customDir.IsDir() {
   239  			if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
   240  				if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
   241  					fatal("Failed to include custom: %v", err)
   242  				}
   243  			} else {
   244  				log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath)
   245  			}
   246  		} else {
   247  			log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath)
   248  		}
   249  	}
   250  
   251  	isExist, err := util.IsExist(setting.AppDataPath)
   252  	if err != nil {
   253  		log.Error("Unable to check if %s exists. Error: %v", setting.AppDataPath, err)
   254  	}
   255  	if isExist {
   256  		log.Info("Packing data directory...%s", setting.AppDataPath)
   257  
   258  		var excludes []string
   259  		if setting.SessionConfig.OriginalProvider == "file" {
   260  			var opts session.Options
   261  			if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
   262  				return err
   263  			}
   264  			excludes = append(excludes, opts.ProviderConfig)
   265  		}
   266  
   267  		if ctx.IsSet("skip-index") && ctx.Bool("skip-index") {
   268  			excludes = append(excludes, setting.Indexer.RepoPath)
   269  			excludes = append(excludes, setting.Indexer.IssuePath)
   270  		}
   271  
   272  		excludes = append(excludes, setting.RepoRootPath)
   273  		excludes = append(excludes, setting.LFS.Storage.Path)
   274  		excludes = append(excludes, setting.Attachment.Storage.Path)
   275  		excludes = append(excludes, setting.Packages.Storage.Path)
   276  		excludes = append(excludes, setting.Log.RootPath)
   277  		if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
   278  			fatal("Failed to include data directory: %v", err)
   279  		}
   280  	}
   281  
   282  	if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
   283  		log.Info("Skip dumping attachment data")
   284  	} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
   285  		info, err := object.Stat()
   286  		if err != nil {
   287  			return err
   288  		}
   289  		return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
   290  	}); err != nil {
   291  		fatal("Failed to dump attachments: %v", err)
   292  	}
   293  
   294  	if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
   295  		log.Info("Skip dumping package data")
   296  	} else if !setting.Packages.Enabled {
   297  		log.Info("Packages isn't enabled. Skip dumping package data")
   298  	} else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
   299  		info, err := object.Stat()
   300  		if err != nil {
   301  			return err
   302  		}
   303  		return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
   304  	}); err != nil {
   305  		fatal("Failed to dump packages: %v", err)
   306  	}
   307  
   308  	// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
   309  	// ensuring that it's clear the dump is skipped whether the directory's initialized
   310  	// yet or not.
   311  	if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
   312  		log.Info("Skip dumping log files")
   313  	} else {
   314  		isExist, err := util.IsExist(setting.Log.RootPath)
   315  		if err != nil {
   316  			log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
   317  		}
   318  		if isExist {
   319  			if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
   320  				fatal("Failed to include log: %v", err)
   321  			}
   322  		}
   323  	}
   324  
   325  	if outFileName == "-" {
   326  		log.Info("Finish dumping to stdout")
   327  	} else {
   328  		if err = archiverWriter.Close(); err != nil {
   329  			_ = os.Remove(outFileName)
   330  			fatal("Failed to save %q: %v", outFileName, err)
   331  		}
   332  		if err = os.Chmod(outFileName, 0o600); err != nil {
   333  			log.Info("Can't change file access permissions mask to 0600: %v", err)
   334  		}
   335  		log.Info("Finish dumping in file %s", outFileName)
   336  	}
   337  	return nil
   338  }