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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2016 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package cmd
  6. import (
  7. "encoding/json"
  8. "fmt"
  9. "io/ioutil"
  10. "os"
  11. "path"
  12. "path/filepath"
  13. "strings"
  14. "time"
  15. "code.gitea.io/gitea/models"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/setting"
  18. "code.gitea.io/gitea/modules/storage"
  19. "code.gitea.io/gitea/modules/util"
  20. "gitea.com/macaron/session"
  21. archiver "github.com/mholt/archiver/v3"
  22. "github.com/unknwon/com"
  23. "github.com/urfave/cli"
  24. )
  25. func addFile(w archiver.Writer, filePath string, absPath string, verbose bool) error {
  26. if verbose {
  27. log.Info("Adding file %s\n", filePath)
  28. }
  29. file, err := os.Open(absPath)
  30. if err != nil {
  31. return err
  32. }
  33. defer file.Close()
  34. fileInfo, err := file.Stat()
  35. if err != nil {
  36. return err
  37. }
  38. return w.Write(archiver.File{
  39. FileInfo: archiver.FileInfo{
  40. FileInfo: fileInfo,
  41. CustomName: filePath,
  42. },
  43. ReadCloser: file,
  44. })
  45. }
  46. func addRecursive(w archiver.Writer, dirPath string, absPath string, verbose bool) error {
  47. if verbose {
  48. log.Info("Adding dir %s\n", dirPath)
  49. }
  50. dir, err := os.Open(absPath)
  51. if err != nil {
  52. return fmt.Errorf("Could not open directory %s: %s", absPath, err)
  53. }
  54. defer dir.Close()
  55. files, err := dir.Readdir(0)
  56. if err != nil {
  57. return fmt.Errorf("Unable to list files in %s: %s", absPath, err)
  58. }
  59. if err := addFile(w, dirPath, absPath, false); err != nil {
  60. return err
  61. }
  62. for _, fileInfo := range files {
  63. if fileInfo.IsDir() {
  64. err = addRecursive(w, filepath.Join(dirPath, fileInfo.Name()), filepath.Join(absPath, fileInfo.Name()), verbose)
  65. } else {
  66. err = addFile(w, filepath.Join(dirPath, fileInfo.Name()), filepath.Join(absPath, fileInfo.Name()), verbose)
  67. }
  68. if err != nil {
  69. return err
  70. }
  71. }
  72. return nil
  73. }
  74. func isSubdir(upper string, lower string) (bool, error) {
  75. if relPath, err := filepath.Rel(upper, lower); err != nil {
  76. return false, err
  77. } else if relPath == "." || !strings.HasPrefix(relPath, ".") {
  78. return true, nil
  79. }
  80. return false, nil
  81. }
  82. type outputType struct {
  83. Enum []string
  84. Default string
  85. selected string
  86. }
  87. func (o outputType) Join() string {
  88. return strings.Join(o.Enum, ", ")
  89. }
  90. func (o *outputType) Set(value string) error {
  91. for _, enum := range o.Enum {
  92. if enum == value {
  93. o.selected = value
  94. return nil
  95. }
  96. }
  97. return fmt.Errorf("allowed values are %s", o.Join())
  98. }
  99. func (o outputType) String() string {
  100. if o.selected == "" {
  101. return o.Default
  102. }
  103. return o.selected
  104. }
  105. var outputTypeEnum = &outputType{
  106. Enum: []string{"zip", "tar", "tar.gz", "tar.xz", "tar.bz2"},
  107. Default: "zip",
  108. }
  109. // CmdDump represents the available dump sub-command.
  110. var CmdDump = cli.Command{
  111. Name: "dump",
  112. Usage: "Dump Gitea files and database",
  113. Description: `Dump compresses all related files and database into zip file.
  114. It can be used for backup and capture Gitea server image to send to maintainer`,
  115. Action: runDump,
  116. Flags: []cli.Flag{
  117. cli.StringFlag{
  118. Name: "file, f",
  119. Value: fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()),
  120. Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
  121. },
  122. cli.BoolFlag{
  123. Name: "verbose, V",
  124. Usage: "Show process details",
  125. },
  126. cli.StringFlag{
  127. Name: "tempdir, t",
  128. Value: os.TempDir(),
  129. Usage: "Temporary dir path",
  130. },
  131. cli.StringFlag{
  132. Name: "database, d",
  133. Usage: "Specify the database SQL syntax",
  134. },
  135. cli.BoolFlag{
  136. Name: "skip-repository, R",
  137. Usage: "Skip the repository dumping",
  138. },
  139. cli.BoolFlag{
  140. Name: "skip-log, L",
  141. Usage: "Skip the log dumping",
  142. },
  143. cli.GenericFlag{
  144. Name: "type",
  145. Value: outputTypeEnum,
  146. Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
  147. },
  148. },
  149. }
  150. func fatal(format string, args ...interface{}) {
  151. fmt.Fprintf(os.Stderr, format+"\n", args...)
  152. log.Fatal(format, args...)
  153. }
  154. func runDump(ctx *cli.Context) error {
  155. var file *os.File
  156. fileName := ctx.String("file")
  157. if fileName == "-" {
  158. file = os.Stdout
  159. err := log.DelLogger("console")
  160. if err != nil {
  161. fatal("Deleting default logger failed. Can not write to stdout: %v", err)
  162. }
  163. }
  164. setting.NewContext()
  165. // make sure we are logging to the console no matter what the configuration tells us do to
  166. if _, err := setting.Cfg.Section("log").NewKey("MODE", "console"); err != nil {
  167. fatal("Setting logging mode to console failed: %v", err)
  168. }
  169. if _, err := setting.Cfg.Section("log.console").NewKey("STDERR", "true"); err != nil {
  170. fatal("Setting console logger to stderr failed: %v", err)
  171. }
  172. if !setting.InstallLock {
  173. log.Error("Is '%s' really the right config path?\n", setting.CustomConf)
  174. return fmt.Errorf("gitea is not initialized")
  175. }
  176. setting.NewServices() // cannot access session settings otherwise
  177. err := models.SetEngine()
  178. if err != nil {
  179. return err
  180. }
  181. if err := storage.Init(); err != nil {
  182. return err
  183. }
  184. if file == nil {
  185. file, err = os.Create(fileName)
  186. if err != nil {
  187. fatal("Unable to open %s: %v", fileName, err)
  188. }
  189. }
  190. defer file.Close()
  191. verbose := ctx.Bool("verbose")
  192. outType := ctx.String("type")
  193. var iface interface{}
  194. if fileName == "-" {
  195. iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType))
  196. } else {
  197. iface, err = archiver.ByExtension(fileName)
  198. }
  199. if err != nil {
  200. fatal("Unable to get archiver for extension: %v", err)
  201. }
  202. w, _ := iface.(archiver.Writer)
  203. if err := w.Create(file); err != nil {
  204. fatal("Creating archiver.Writer failed: %v", err)
  205. }
  206. defer w.Close()
  207. if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
  208. log.Info("Skip dumping local repositories")
  209. } else {
  210. log.Info("Dumping local repositories... %s", setting.RepoRootPath)
  211. if err := addRecursive(w, "repos", setting.RepoRootPath, verbose); err != nil {
  212. fatal("Failed to include repositories: %v", err)
  213. }
  214. if err := storage.LFS.IterateObjects(func(objPath string, object storage.Object) error {
  215. info, err := object.Stat()
  216. if err != nil {
  217. return err
  218. }
  219. return w.Write(archiver.File{
  220. FileInfo: archiver.FileInfo{
  221. FileInfo: info,
  222. CustomName: path.Join("data", "lfs", objPath),
  223. },
  224. ReadCloser: object,
  225. })
  226. }); err != nil {
  227. fatal("Failed to dump LFS objects: %v", err)
  228. }
  229. }
  230. tmpDir := ctx.String("tempdir")
  231. if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
  232. fatal("Path does not exist: %s", tmpDir)
  233. }
  234. dbDump, err := ioutil.TempFile(tmpDir, "gitea-db.sql")
  235. if err != nil {
  236. fatal("Failed to create tmp file: %v", err)
  237. }
  238. defer func() {
  239. if err := util.Remove(dbDump.Name()); err != nil {
  240. log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
  241. }
  242. }()
  243. targetDBType := ctx.String("database")
  244. if len(targetDBType) > 0 && targetDBType != setting.Database.Type {
  245. log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
  246. } else {
  247. log.Info("Dumping database...")
  248. }
  249. if err := models.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
  250. fatal("Failed to dump database: %v", err)
  251. }
  252. if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil {
  253. fatal("Failed to include gitea-db.sql: %v", err)
  254. }
  255. if len(setting.CustomConf) > 0 {
  256. log.Info("Adding custom configuration file from %s", setting.CustomConf)
  257. if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil {
  258. fatal("Failed to include specified app.ini: %v", err)
  259. }
  260. }
  261. customDir, err := os.Stat(setting.CustomPath)
  262. if err == nil && customDir.IsDir() {
  263. if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
  264. if err := addRecursive(w, "custom", setting.CustomPath, verbose); err != nil {
  265. fatal("Failed to include custom: %v", err)
  266. }
  267. } else {
  268. log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath)
  269. }
  270. } else {
  271. log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath)
  272. }
  273. if com.IsExist(setting.AppDataPath) {
  274. log.Info("Packing data directory...%s", setting.AppDataPath)
  275. var excludes []string
  276. if setting.Cfg.Section("session").Key("PROVIDER").Value() == "file" {
  277. var opts session.Options
  278. if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
  279. return err
  280. }
  281. excludes = append(excludes, opts.ProviderConfig)
  282. }
  283. excludes = append(excludes, setting.RepoRootPath)
  284. excludes = append(excludes, setting.LFS.Path)
  285. excludes = append(excludes, setting.Attachment.Path)
  286. excludes = append(excludes, setting.LogRootPath)
  287. if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
  288. fatal("Failed to include data directory: %v", err)
  289. }
  290. }
  291. if err := storage.Attachments.IterateObjects(func(objPath string, object storage.Object) error {
  292. info, err := object.Stat()
  293. if err != nil {
  294. return err
  295. }
  296. return w.Write(archiver.File{
  297. FileInfo: archiver.FileInfo{
  298. FileInfo: info,
  299. CustomName: path.Join("data", "attachments", objPath),
  300. },
  301. ReadCloser: object,
  302. })
  303. }); err != nil {
  304. fatal("Failed to dump attachments: %v", err)
  305. }
  306. // Doesn't check if LogRootPath exists before processing --skip-log intentionally,
  307. // ensuring that it's clear the dump is skipped whether the directory's initialized
  308. // yet or not.
  309. if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
  310. log.Info("Skip dumping log files")
  311. } else if com.IsExist(setting.LogRootPath) {
  312. if err := addRecursive(w, "log", setting.LogRootPath, verbose); err != nil {
  313. fatal("Failed to include log: %v", err)
  314. }
  315. }
  316. if fileName != "-" {
  317. if err = w.Close(); err != nil {
  318. _ = util.Remove(fileName)
  319. fatal("Failed to save %s: %v", fileName, err)
  320. }
  321. if err := os.Chmod(fileName, 0600); err != nil {
  322. log.Info("Can't change file access permissions mask to 0600: %v", err)
  323. }
  324. }
  325. if fileName != "-" {
  326. log.Info("Finish dumping in file %s", fileName)
  327. } else {
  328. log.Info("Finish dumping to stdout")
  329. }
  330. return nil
  331. }
  332. func contains(slice []string, s string) bool {
  333. for _, v := range slice {
  334. if v == s {
  335. return true
  336. }
  337. }
  338. return false
  339. }
  340. // addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
  341. func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
  342. absPath, err := filepath.Abs(absPath)
  343. if err != nil {
  344. return err
  345. }
  346. dir, err := os.Open(absPath)
  347. if err != nil {
  348. return err
  349. }
  350. defer dir.Close()
  351. files, err := dir.Readdir(0)
  352. if err != nil {
  353. return err
  354. }
  355. for _, file := range files {
  356. currentAbsPath := path.Join(absPath, file.Name())
  357. currentInsidePath := path.Join(insidePath, file.Name())
  358. if file.IsDir() {
  359. if !contains(excludeAbsPath, currentAbsPath) {
  360. if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
  361. return err
  362. }
  363. if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
  364. return err
  365. }
  366. }
  367. } else {
  368. if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
  369. return err
  370. }
  371. }
  372. }
  373. return nil
  374. }