You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dump.go 9.7KB

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