123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- // Copyright 2014 The Gogs Authors. All rights reserved.
- // Copyright 2016 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package cmd
-
- import (
- "fmt"
- "os"
- "path"
- "path/filepath"
- "strings"
-
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/dump"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/util"
-
- "gitea.com/go-chi/session"
- "github.com/mholt/archiver/v3"
- "github.com/urfave/cli/v2"
- )
-
- // CmdDump represents the available dump sub-command.
- var CmdDump = &cli.Command{
- Name: "dump",
- Usage: "Dump Gitea files and database",
- 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`,
- Action: runDump,
- Flags: []cli.Flag{
- &cli.StringFlag{
- Name: "file",
- Aliases: []string{"f"},
- Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
- },
- &cli.BoolFlag{
- Name: "verbose",
- Aliases: []string{"V"},
- Usage: "Show process details",
- },
- &cli.BoolFlag{
- Name: "quiet",
- Aliases: []string{"q"},
- Usage: "Only display warnings and errors",
- },
- &cli.StringFlag{
- Name: "tempdir",
- Aliases: []string{"t"},
- Value: os.TempDir(),
- Usage: "Temporary dir path",
- },
- &cli.StringFlag{
- Name: "database",
- Aliases: []string{"d"},
- Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres",
- },
- &cli.BoolFlag{
- Name: "skip-repository",
- Aliases: []string{"R"},
- Usage: "Skip the repository dumping",
- },
- &cli.BoolFlag{
- Name: "skip-log",
- Aliases: []string{"L"},
- Usage: "Skip the log dumping",
- },
- &cli.BoolFlag{
- Name: "skip-custom-dir",
- Usage: "Skip custom directory",
- },
- &cli.BoolFlag{
- Name: "skip-lfs-data",
- Usage: "Skip LFS data",
- },
- &cli.BoolFlag{
- Name: "skip-attachment-data",
- Usage: "Skip attachment data",
- },
- &cli.BoolFlag{
- Name: "skip-package-data",
- Usage: "Skip package data",
- },
- &cli.BoolFlag{
- Name: "skip-index",
- Usage: "Skip bleve index data",
- },
- &cli.BoolFlag{
- Name: "skip-db",
- Usage: "Skip database",
- },
- &cli.StringFlag{
- Name: "type",
- Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
- },
- },
- }
-
- func fatal(format string, args ...any) {
- log.Fatal(format, args...)
- }
-
- func runDump(ctx *cli.Context) error {
- setting.MustInstalled()
-
- quite := ctx.Bool("quiet")
- verbose := ctx.Bool("verbose")
- if verbose && quite {
- fatal("Option --quiet and --verbose cannot both be set")
- }
-
- // outFileName is either "-" or a file name (will be made absolute)
- outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
- if outType == "" {
- fatal("Invalid output type")
- }
-
- outFile := os.Stdout
- if outFileName != "-" {
- var err error
- if outFileName, err = filepath.Abs(outFileName); err != nil {
- fatal("Unable to get absolute path of dump file: %v", err)
- }
- if exist, _ := util.IsExist(outFileName); exist {
- fatal("Dump file %q exists", outFileName)
- }
- if outFile, err = os.Create(outFileName); err != nil {
- fatal("Unable to create dump file %q: %v", outFileName, err)
- }
- defer outFile.Close()
- }
-
- setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
-
- setting.DisableLoggerInit()
- setting.LoadSettings() // cannot access session settings otherwise
-
- stdCtx, cancel := installSignals()
- defer cancel()
-
- err := db.InitEngine(stdCtx)
- if err != nil {
- return err
- }
-
- if err = storage.Init(); err != nil {
- return err
- }
-
- archiverGeneric, err := archiver.ByExtension("." + outType)
- if err != nil {
- fatal("Unable to get archiver for extension: %v", err)
- }
-
- archiverWriter := archiverGeneric.(archiver.Writer)
- if err := archiverWriter.Create(outFile); err != nil {
- fatal("Creating archiver.Writer failed: %v", err)
- }
- defer archiverWriter.Close()
-
- dumper := &dump.Dumper{
- Writer: archiverWriter,
- Verbose: verbose,
- }
- dumper.GlobalExcludeAbsPath(outFileName)
-
- if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
- log.Info("Skip dumping local repositories")
- } else {
- log.Info("Dumping local repositories... %s", setting.RepoRootPath)
- if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
- fatal("Failed to include repositories: %v", err)
- }
-
- if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
- log.Info("Skip dumping LFS data")
- } else if !setting.LFS.StartServer {
- log.Info("LFS isn't enabled. Skip dumping LFS data")
- } else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
- info, err := object.Stat()
- if err != nil {
- return err
- }
- return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
- }); err != nil {
- fatal("Failed to dump LFS objects: %v", err)
- }
- }
-
- if ctx.Bool("skip-db") {
- // Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
- dumper.GlobalExcludeAbsPath(setting.Database.Path)
- log.Info("Skipping database")
- } else {
- tmpDir := ctx.String("tempdir")
- if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
- fatal("Path does not exist: %s", tmpDir)
- }
-
- dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql")
- if err != nil {
- fatal("Failed to create tmp file: %v", err)
- }
- defer func() {
- _ = dbDump.Close()
- if err := util.Remove(dbDump.Name()); err != nil {
- log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
- }
- }()
-
- targetDBType := ctx.String("database")
- if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
- log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
- } else {
- log.Info("Dumping database...")
- }
-
- if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
- fatal("Failed to dump database: %v", err)
- }
-
- if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
- fatal("Failed to include gitea-db.sql: %v", err)
- }
- }
-
- log.Info("Adding custom configuration file from %s", setting.CustomConf)
- if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
- fatal("Failed to include specified app.ini: %v", err)
- }
-
- if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
- log.Info("Skipping custom directory")
- } else {
- customDir, err := os.Stat(setting.CustomPath)
- if err == nil && customDir.IsDir() {
- if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
- if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
- fatal("Failed to include custom: %v", err)
- }
- } else {
- log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath)
- }
- } else {
- log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath)
- }
- }
-
- isExist, err := util.IsExist(setting.AppDataPath)
- if err != nil {
- log.Error("Unable to check if %s exists. Error: %v", setting.AppDataPath, err)
- }
- if isExist {
- log.Info("Packing data directory...%s", setting.AppDataPath)
-
- var excludes []string
- if setting.SessionConfig.OriginalProvider == "file" {
- var opts session.Options
- if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
- return err
- }
- excludes = append(excludes, opts.ProviderConfig)
- }
-
- if ctx.IsSet("skip-index") && ctx.Bool("skip-index") {
- excludes = append(excludes, setting.Indexer.RepoPath)
- excludes = append(excludes, setting.Indexer.IssuePath)
- }
-
- excludes = append(excludes, setting.RepoRootPath)
- excludes = append(excludes, setting.LFS.Storage.Path)
- excludes = append(excludes, setting.Attachment.Storage.Path)
- excludes = append(excludes, setting.Packages.Storage.Path)
- excludes = append(excludes, setting.Log.RootPath)
- if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
- fatal("Failed to include data directory: %v", err)
- }
- }
-
- if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
- log.Info("Skip dumping attachment data")
- } else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
- info, err := object.Stat()
- if err != nil {
- return err
- }
- return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
- }); err != nil {
- fatal("Failed to dump attachments: %v", err)
- }
-
- if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
- log.Info("Skip dumping package data")
- } else if !setting.Packages.Enabled {
- log.Info("Packages isn't enabled. Skip dumping package data")
- } else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
- info, err := object.Stat()
- if err != nil {
- return err
- }
- return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
- }); err != nil {
- fatal("Failed to dump packages: %v", err)
- }
-
- // Doesn't check if LogRootPath exists before processing --skip-log intentionally,
- // ensuring that it's clear the dump is skipped whether the directory's initialized
- // yet or not.
- if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
- log.Info("Skip dumping log files")
- } else {
- isExist, err := util.IsExist(setting.Log.RootPath)
- if err != nil {
- log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
- }
- if isExist {
- if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
- fatal("Failed to include log: %v", err)
- }
- }
- }
-
- if outFileName == "-" {
- log.Info("Finish dumping to stdout")
- } else {
- if err = archiverWriter.Close(); err != nil {
- _ = os.Remove(outFileName)
- fatal("Failed to save %q: %v", outFileName, err)
- }
- if err = os.Chmod(outFileName, 0o600); err != nil {
- log.Info("Can't change file access permissions mask to 0600: %v", err)
- }
- log.Info("Finish dumping in file %s", outFileName)
- }
- return nil
- }
|