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 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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.BoolFlag{
  86. Name: "skip-db",
  87. Usage: "Skip database",
  88. },
  89. &cli.StringFlag{
  90. Name: "type",
  91. Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
  92. },
  93. },
  94. }
  95. func fatal(format string, args ...any) {
  96. log.Fatal(format, args...)
  97. }
  98. func runDump(ctx *cli.Context) error {
  99. setting.MustInstalled()
  100. quite := ctx.Bool("quiet")
  101. verbose := ctx.Bool("verbose")
  102. if verbose && quite {
  103. fatal("Option --quiet and --verbose cannot both be set")
  104. }
  105. // outFileName is either "-" or a file name (will be made absolute)
  106. outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
  107. if outType == "" {
  108. fatal("Invalid output type")
  109. }
  110. outFile := os.Stdout
  111. if outFileName != "-" {
  112. var err error
  113. if outFileName, err = filepath.Abs(outFileName); err != nil {
  114. fatal("Unable to get absolute path of dump file: %v", err)
  115. }
  116. if exist, _ := util.IsExist(outFileName); exist {
  117. fatal("Dump file %q exists", outFileName)
  118. }
  119. if outFile, err = os.Create(outFileName); err != nil {
  120. fatal("Unable to create dump file %q: %v", outFileName, err)
  121. }
  122. defer outFile.Close()
  123. }
  124. setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
  125. setting.DisableLoggerInit()
  126. setting.LoadSettings() // cannot access session settings otherwise
  127. stdCtx, cancel := installSignals()
  128. defer cancel()
  129. err := db.InitEngine(stdCtx)
  130. if err != nil {
  131. return err
  132. }
  133. if err = storage.Init(); err != nil {
  134. return err
  135. }
  136. archiverGeneric, err := archiver.ByExtension("." + outType)
  137. if err != nil {
  138. fatal("Unable to get archiver for extension: %v", err)
  139. }
  140. archiverWriter := archiverGeneric.(archiver.Writer)
  141. if err := archiverWriter.Create(outFile); err != nil {
  142. fatal("Creating archiver.Writer failed: %v", err)
  143. }
  144. defer archiverWriter.Close()
  145. dumper := &dump.Dumper{
  146. Writer: archiverWriter,
  147. Verbose: verbose,
  148. }
  149. dumper.GlobalExcludeAbsPath(outFileName)
  150. if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
  151. log.Info("Skip dumping local repositories")
  152. } else {
  153. log.Info("Dumping local repositories... %s", setting.RepoRootPath)
  154. if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
  155. fatal("Failed to include repositories: %v", err)
  156. }
  157. if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
  158. log.Info("Skip dumping LFS data")
  159. } else if !setting.LFS.StartServer {
  160. log.Info("LFS isn't enabled. Skip dumping LFS data")
  161. } else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
  162. info, err := object.Stat()
  163. if err != nil {
  164. return err
  165. }
  166. return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
  167. }); err != nil {
  168. fatal("Failed to dump LFS objects: %v", err)
  169. }
  170. }
  171. if ctx.Bool("skip-db") {
  172. // Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
  173. dumper.GlobalExcludeAbsPath(setting.Database.Path)
  174. log.Info("Skipping database")
  175. } else {
  176. tmpDir := ctx.String("tempdir")
  177. if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
  178. fatal("Path does not exist: %s", tmpDir)
  179. }
  180. dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql")
  181. if err != nil {
  182. fatal("Failed to create tmp file: %v", err)
  183. }
  184. defer func() {
  185. _ = dbDump.Close()
  186. if err := util.Remove(dbDump.Name()); err != nil {
  187. log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
  188. }
  189. }()
  190. targetDBType := ctx.String("database")
  191. if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
  192. log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
  193. } else {
  194. log.Info("Dumping database...")
  195. }
  196. if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
  197. fatal("Failed to dump database: %v", err)
  198. }
  199. if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
  200. fatal("Failed to include gitea-db.sql: %v", err)
  201. }
  202. }
  203. log.Info("Adding custom configuration file from %s", setting.CustomConf)
  204. if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
  205. fatal("Failed to include specified app.ini: %v", err)
  206. }
  207. if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
  208. log.Info("Skipping custom directory")
  209. } else {
  210. customDir, err := os.Stat(setting.CustomPath)
  211. if err == nil && customDir.IsDir() {
  212. if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
  213. if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
  214. fatal("Failed to include custom: %v", err)
  215. }
  216. } else {
  217. log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath)
  218. }
  219. } else {
  220. log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath)
  221. }
  222. }
  223. isExist, err := util.IsExist(setting.AppDataPath)
  224. if err != nil {
  225. log.Error("Unable to check if %s exists. Error: %v", setting.AppDataPath, err)
  226. }
  227. if isExist {
  228. log.Info("Packing data directory...%s", setting.AppDataPath)
  229. var excludes []string
  230. if setting.SessionConfig.OriginalProvider == "file" {
  231. var opts session.Options
  232. if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
  233. return err
  234. }
  235. excludes = append(excludes, opts.ProviderConfig)
  236. }
  237. if ctx.IsSet("skip-index") && ctx.Bool("skip-index") {
  238. excludes = append(excludes, setting.Indexer.RepoPath)
  239. excludes = append(excludes, setting.Indexer.IssuePath)
  240. }
  241. excludes = append(excludes, setting.RepoRootPath)
  242. excludes = append(excludes, setting.LFS.Storage.Path)
  243. excludes = append(excludes, setting.Attachment.Storage.Path)
  244. excludes = append(excludes, setting.Packages.Storage.Path)
  245. excludes = append(excludes, setting.Log.RootPath)
  246. if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
  247. fatal("Failed to include data directory: %v", err)
  248. }
  249. }
  250. if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
  251. log.Info("Skip dumping attachment data")
  252. } else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
  253. info, err := object.Stat()
  254. if err != nil {
  255. return err
  256. }
  257. return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
  258. }); err != nil {
  259. fatal("Failed to dump attachments: %v", err)
  260. }
  261. if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
  262. log.Info("Skip dumping package data")
  263. } else if !setting.Packages.Enabled {
  264. log.Info("Packages isn't enabled. Skip dumping package data")
  265. } else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
  266. info, err := object.Stat()
  267. if err != nil {
  268. return err
  269. }
  270. return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
  271. }); err != nil {
  272. fatal("Failed to dump packages: %v", err)
  273. }
  274. // Doesn't check if LogRootPath exists before processing --skip-log intentionally,
  275. // ensuring that it's clear the dump is skipped whether the directory's initialized
  276. // yet or not.
  277. if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
  278. log.Info("Skip dumping log files")
  279. } else {
  280. isExist, err := util.IsExist(setting.Log.RootPath)
  281. if err != nil {
  282. log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
  283. }
  284. if isExist {
  285. if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
  286. fatal("Failed to include log: %v", err)
  287. }
  288. }
  289. }
  290. if outFileName == "-" {
  291. log.Info("Finish dumping to stdout")
  292. } else {
  293. if err = archiverWriter.Close(); err != nil {
  294. _ = os.Remove(outFileName)
  295. fatal("Failed to save %q: %v", outFileName, err)
  296. }
  297. if err = os.Chmod(outFileName, 0o600); err != nil {
  298. log.Info("Can't change file access permissions mask to 0600: %v", err)
  299. }
  300. log.Info("Finish dumping in file %s", outFileName)
  301. }
  302. return nil
  303. }