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