gitea源码

dump.go 9.8KB

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