code.gitea.io/gitea@v1.21.7/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  	"io"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"strings"
    14  	"time"
    15  
    16  	"code.gitea.io/gitea/models/db"
    17  	"code.gitea.io/gitea/modules/json"
    18  	"code.gitea.io/gitea/modules/log"
    19  	"code.gitea.io/gitea/modules/setting"
    20  	"code.gitea.io/gitea/modules/storage"
    21  	"code.gitea.io/gitea/modules/util"
    22  
    23  	"gitea.com/go-chi/session"
    24  	"github.com/mholt/archiver/v3"
    25  	"github.com/urfave/cli/v2"
    26  )
    27  
    28  func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error {
    29  	if verbose {
    30  		log.Info("Adding file %s", customName)
    31  	}
    32  
    33  	return w.Write(archiver.File{
    34  		FileInfo: archiver.FileInfo{
    35  			FileInfo:   info,
    36  			CustomName: customName,
    37  		},
    38  		ReadCloser: r,
    39  	})
    40  }
    41  
    42  func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
    43  	file, err := os.Open(absPath)
    44  	if err != nil {
    45  		return err
    46  	}
    47  	defer file.Close()
    48  	fileInfo, err := file.Stat()
    49  	if err != nil {
    50  		return err
    51  	}
    52  
    53  	return addReader(w, file, fileInfo, filePath, verbose)
    54  }
    55  
    56  func isSubdir(upper, lower string) (bool, error) {
    57  	if relPath, err := filepath.Rel(upper, lower); err != nil {
    58  		return false, err
    59  	} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
    60  		return true, nil
    61  	}
    62  	return false, nil
    63  }
    64  
    65  type outputType struct {
    66  	Enum     []string
    67  	Default  string
    68  	selected string
    69  }
    70  
    71  func (o outputType) Join() string {
    72  	return strings.Join(o.Enum, ", ")
    73  }
    74  
    75  func (o *outputType) Set(value string) error {
    76  	for _, enum := range o.Enum {
    77  		if enum == value {
    78  			o.selected = value
    79  			return nil
    80  		}
    81  	}
    82  
    83  	return fmt.Errorf("allowed values are %s", o.Join())
    84  }
    85  
    86  func (o outputType) String() string {
    87  	if o.selected == "" {
    88  		return o.Default
    89  	}
    90  	return o.selected
    91  }
    92  
    93  var outputTypeEnum = &outputType{
    94  	Enum:    []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"},
    95  	Default: "zip",
    96  }
    97  
    98  // CmdDump represents the available dump sub-command.
    99  var CmdDump = &cli.Command{
   100  	Name:  "dump",
   101  	Usage: "Dump Gitea files and database",
   102  	Description: `Dump compresses all related files and database into zip file.
   103  It can be used for backup and capture Gitea server image to send to maintainer`,
   104  	Action: runDump,
   105  	Flags: []cli.Flag{
   106  		&cli.StringFlag{
   107  			Name:    "file",
   108  			Aliases: []string{"f"},
   109  			Value:   fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()),
   110  			Usage:   "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
   111  		},
   112  		&cli.BoolFlag{
   113  			Name:    "verbose",
   114  			Aliases: []string{"V"},
   115  			Usage:   "Show process details",
   116  		},
   117  		&cli.BoolFlag{
   118  			Name:    "quiet",
   119  			Aliases: []string{"q"},
   120  			Usage:   "Only display warnings and errors",
   121  		},
   122  		&cli.StringFlag{
   123  			Name:    "tempdir",
   124  			Aliases: []string{"t"},
   125  			Value:   os.TempDir(),
   126  			Usage:   "Temporary dir path",
   127  		},
   128  		&cli.StringFlag{
   129  			Name:    "database",
   130  			Aliases: []string{"d"},
   131  			Usage:   "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres",
   132  		},
   133  		&cli.BoolFlag{
   134  			Name:    "skip-repository",
   135  			Aliases: []string{"R"},
   136  			Usage:   "Skip the repository dumping",
   137  		},
   138  		&cli.BoolFlag{
   139  			Name:    "skip-log",
   140  			Aliases: []string{"L"},
   141  			Usage:   "Skip the log dumping",
   142  		},
   143  		&cli.BoolFlag{
   144  			Name:  "skip-custom-dir",
   145  			Usage: "Skip custom directory",
   146  		},
   147  		&cli.BoolFlag{
   148  			Name:  "skip-lfs-data",
   149  			Usage: "Skip LFS data",
   150  		},
   151  		&cli.BoolFlag{
   152  			Name:  "skip-attachment-data",
   153  			Usage: "Skip attachment data",
   154  		},
   155  		&cli.BoolFlag{
   156  			Name:  "skip-package-data",
   157  			Usage: "Skip package data",
   158  		},
   159  		&cli.BoolFlag{
   160  			Name:  "skip-index",
   161  			Usage: "Skip bleve index data",
   162  		},
   163  		&cli.GenericFlag{
   164  			Name:  "type",
   165  			Value: outputTypeEnum,
   166  			Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
   167  		},
   168  	},
   169  }
   170  
   171  func fatal(format string, args ...any) {
   172  	fmt.Fprintf(os.Stderr, format+"\n", args...)
   173  	log.Fatal(format, args...)
   174  }
   175  
   176  func runDump(ctx *cli.Context) error {
   177  	var file *os.File
   178  	fileName := ctx.String("file")
   179  	outType := ctx.String("type")
   180  	if fileName == "-" {
   181  		file = os.Stdout
   182  		setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
   183  	} else {
   184  		for _, suffix := range outputTypeEnum.Enum {
   185  			if strings.HasSuffix(fileName, "."+suffix) {
   186  				fileName = strings.TrimSuffix(fileName, "."+suffix)
   187  				break
   188  			}
   189  		}
   190  		fileName += "." + outType
   191  	}
   192  	setting.MustInstalled()
   193  
   194  	// make sure we are logging to the console no matter what the configuration tells us do to
   195  	// FIXME: don't use CfgProvider directly
   196  	if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil {
   197  		fatal("Setting logging mode to console failed: %v", err)
   198  	}
   199  	if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil {
   200  		fatal("Setting console logger to stderr failed: %v", err)
   201  	}
   202  
   203  	// Set loglevel to Warn if quiet-mode is requested
   204  	if ctx.Bool("quiet") {
   205  		if _, err := setting.CfgProvider.Section("log.console").NewKey("LEVEL", "Warn"); err != nil {
   206  			fatal("Setting console log-level failed: %v", err)
   207  		}
   208  	}
   209  
   210  	if !setting.InstallLock {
   211  		log.Error("Is '%s' really the right config path?\n", setting.CustomConf)
   212  		return fmt.Errorf("gitea is not initialized")
   213  	}
   214  	setting.LoadSettings() // cannot access session settings otherwise
   215  
   216  	verbose := ctx.Bool("verbose")
   217  	if verbose && ctx.Bool("quiet") {
   218  		return fmt.Errorf("--quiet and --verbose cannot both be set")
   219  	}
   220  
   221  	stdCtx, cancel := installSignals()
   222  	defer cancel()
   223  
   224  	err := db.InitEngine(stdCtx)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	if err := storage.Init(); err != nil {
   230  		return err
   231  	}
   232  
   233  	if file == nil {
   234  		file, err = os.Create(fileName)
   235  		if err != nil {
   236  			fatal("Unable to open %s: %v", fileName, err)
   237  		}
   238  	}
   239  	defer file.Close()
   240  
   241  	absFileName, err := filepath.Abs(fileName)
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	var iface any
   247  	if fileName == "-" {
   248  		iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType))
   249  	} else {
   250  		iface, err = archiver.ByExtension(fileName)
   251  	}
   252  	if err != nil {
   253  		fatal("Unable to get archiver for extension: %v", err)
   254  	}
   255  
   256  	w, _ := iface.(archiver.Writer)
   257  	if err := w.Create(file); err != nil {
   258  		fatal("Creating archiver.Writer failed: %v", err)
   259  	}
   260  	defer w.Close()
   261  
   262  	if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
   263  		log.Info("Skip dumping local repositories")
   264  	} else {
   265  		log.Info("Dumping local repositories... %s", setting.RepoRootPath)
   266  		if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil {
   267  			fatal("Failed to include repositories: %v", err)
   268  		}
   269  
   270  		if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
   271  			log.Info("Skip dumping LFS data")
   272  		} else if !setting.LFS.StartServer {
   273  			log.Info("LFS isn't enabled. Skip dumping LFS data")
   274  		} else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
   275  			info, err := object.Stat()
   276  			if err != nil {
   277  				return err
   278  			}
   279  
   280  			return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
   281  		}); err != nil {
   282  			fatal("Failed to dump LFS objects: %v", err)
   283  		}
   284  	}
   285  
   286  	tmpDir := ctx.String("tempdir")
   287  	if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
   288  		fatal("Path does not exist: %s", tmpDir)
   289  	}
   290  
   291  	dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql")
   292  	if err != nil {
   293  		fatal("Failed to create tmp file: %v", err)
   294  	}
   295  	defer func() {
   296  		_ = dbDump.Close()
   297  		if err := util.Remove(dbDump.Name()); err != nil {
   298  			log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
   299  		}
   300  	}()
   301  
   302  	targetDBType := ctx.String("database")
   303  	if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
   304  		log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
   305  	} else {
   306  		log.Info("Dumping database...")
   307  	}
   308  
   309  	if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
   310  		fatal("Failed to dump database: %v", err)
   311  	}
   312  
   313  	if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil {
   314  		fatal("Failed to include gitea-db.sql: %v", err)
   315  	}
   316  
   317  	if len(setting.CustomConf) > 0 {
   318  		log.Info("Adding custom configuration file from %s", setting.CustomConf)
   319  		if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil {
   320  			fatal("Failed to include specified app.ini: %v", err)
   321  		}
   322  	}
   323  
   324  	if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
   325  		log.Info("Skipping custom directory")
   326  	} else {
   327  		customDir, err := os.Stat(setting.CustomPath)
   328  		if err == nil && customDir.IsDir() {
   329  			if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
   330  				if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
   331  					fatal("Failed to include custom: %v", err)
   332  				}
   333  			} else {
   334  				log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath)
   335  			}
   336  		} else {
   337  			log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath)
   338  		}
   339  	}
   340  
   341  	isExist, err := util.IsExist(setting.AppDataPath)
   342  	if err != nil {
   343  		log.Error("Unable to check if %s exists. Error: %v", setting.AppDataPath, err)
   344  	}
   345  	if isExist {
   346  		log.Info("Packing data directory...%s", setting.AppDataPath)
   347  
   348  		var excludes []string
   349  		if setting.SessionConfig.OriginalProvider == "file" {
   350  			var opts session.Options
   351  			if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
   352  				return err
   353  			}
   354  			excludes = append(excludes, opts.ProviderConfig)
   355  		}
   356  
   357  		if ctx.IsSet("skip-index") && ctx.Bool("skip-index") {
   358  			excludes = append(excludes, setting.Indexer.RepoPath)
   359  			excludes = append(excludes, setting.Indexer.IssuePath)
   360  		}
   361  
   362  		excludes = append(excludes, setting.RepoRootPath)
   363  		excludes = append(excludes, setting.LFS.Storage.Path)
   364  		excludes = append(excludes, setting.Attachment.Storage.Path)
   365  		excludes = append(excludes, setting.Packages.Storage.Path)
   366  		excludes = append(excludes, setting.Log.RootPath)
   367  		excludes = append(excludes, absFileName)
   368  		if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
   369  			fatal("Failed to include data directory: %v", err)
   370  		}
   371  	}
   372  
   373  	if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
   374  		log.Info("Skip dumping attachment data")
   375  	} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
   376  		info, err := object.Stat()
   377  		if err != nil {
   378  			return err
   379  		}
   380  
   381  		return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
   382  	}); err != nil {
   383  		fatal("Failed to dump attachments: %v", err)
   384  	}
   385  
   386  	if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
   387  		log.Info("Skip dumping package data")
   388  	} else if !setting.Packages.Enabled {
   389  		log.Info("Packages isn't enabled. Skip dumping package data")
   390  	} else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
   391  		info, err := object.Stat()
   392  		if err != nil {
   393  			return err
   394  		}
   395  
   396  		return addReader(w, object, info, path.Join("data", "packages", objPath), verbose)
   397  	}); err != nil {
   398  		fatal("Failed to dump packages: %v", err)
   399  	}
   400  
   401  	// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
   402  	// ensuring that it's clear the dump is skipped whether the directory's initialized
   403  	// yet or not.
   404  	if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
   405  		log.Info("Skip dumping log files")
   406  	} else {
   407  		isExist, err := util.IsExist(setting.Log.RootPath)
   408  		if err != nil {
   409  			log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
   410  		}
   411  		if isExist {
   412  			if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil {
   413  				fatal("Failed to include log: %v", err)
   414  			}
   415  		}
   416  	}
   417  
   418  	if fileName != "-" {
   419  		if err = w.Close(); err != nil {
   420  			_ = util.Remove(fileName)
   421  			fatal("Failed to save %s: %v", fileName, err)
   422  		}
   423  
   424  		if err := os.Chmod(fileName, 0o600); err != nil {
   425  			log.Info("Can't change file access permissions mask to 0600: %v", err)
   426  		}
   427  	}
   428  
   429  	if fileName != "-" {
   430  		log.Info("Finish dumping in file %s", fileName)
   431  	} else {
   432  		log.Info("Finish dumping to stdout")
   433  	}
   434  
   435  	return nil
   436  }
   437  
   438  // addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
   439  func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
   440  	absPath, err := filepath.Abs(absPath)
   441  	if err != nil {
   442  		return err
   443  	}
   444  	dir, err := os.Open(absPath)
   445  	if err != nil {
   446  		return err
   447  	}
   448  	defer dir.Close()
   449  
   450  	files, err := dir.Readdir(0)
   451  	if err != nil {
   452  		return err
   453  	}
   454  	for _, file := range files {
   455  		currentAbsPath := filepath.Join(absPath, file.Name())
   456  		currentInsidePath := path.Join(insidePath, file.Name())
   457  		if file.IsDir() {
   458  			if !util.SliceContainsString(excludeAbsPath, currentAbsPath) {
   459  				if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
   460  					return err
   461  				}
   462  				if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
   463  					return err
   464  				}
   465  			}
   466  		} else {
   467  			// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
   468  			shouldAdd := file.Mode().IsRegular()
   469  			if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
   470  				target, err := filepath.EvalSymlinks(currentAbsPath)
   471  				if err != nil {
   472  					return err
   473  				}
   474  				targetStat, err := os.Stat(target)
   475  				if err != nil {
   476  					return err
   477  				}
   478  				shouldAdd = targetStat.Mode().IsRegular()
   479  			}
   480  			if shouldAdd {
   481  				if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
   482  					return err
   483  				}
   484  			}
   485  		}
   486  	}
   487  	return nil
   488  }