code.gitea.io/gitea@v1.21.7/cmd/dump.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2016 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package cmd 6 7 import ( 8 "fmt" 9 "io" 10 "os" 11 "path" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "code.gitea.io/gitea/models/db" 17 "code.gitea.io/gitea/modules/json" 18 "code.gitea.io/gitea/modules/log" 19 "code.gitea.io/gitea/modules/setting" 20 "code.gitea.io/gitea/modules/storage" 21 "code.gitea.io/gitea/modules/util" 22 23 "gitea.com/go-chi/session" 24 "github.com/mholt/archiver/v3" 25 "github.com/urfave/cli/v2" 26 ) 27 28 func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error { 29 if verbose { 30 log.Info("Adding file %s", customName) 31 } 32 33 return w.Write(archiver.File{ 34 FileInfo: archiver.FileInfo{ 35 FileInfo: info, 36 CustomName: customName, 37 }, 38 ReadCloser: r, 39 }) 40 } 41 42 func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error { 43 file, err := os.Open(absPath) 44 if err != nil { 45 return err 46 } 47 defer file.Close() 48 fileInfo, err := file.Stat() 49 if err != nil { 50 return err 51 } 52 53 return addReader(w, file, fileInfo, filePath, verbose) 54 } 55 56 func isSubdir(upper, lower string) (bool, error) { 57 if relPath, err := filepath.Rel(upper, lower); err != nil { 58 return false, err 59 } else if relPath == "." || !strings.HasPrefix(relPath, ".") { 60 return true, nil 61 } 62 return false, nil 63 } 64 65 type outputType struct { 66 Enum []string 67 Default string 68 selected string 69 } 70 71 func (o outputType) Join() string { 72 return strings.Join(o.Enum, ", ") 73 } 74 75 func (o *outputType) Set(value string) error { 76 for _, enum := range o.Enum { 77 if enum == value { 78 o.selected = value 79 return nil 80 } 81 } 82 83 return fmt.Errorf("allowed values are %s", o.Join()) 84 } 85 86 func (o outputType) String() string { 87 if o.selected == "" { 88 return o.Default 89 } 90 return o.selected 91 } 92 93 var outputTypeEnum = &outputType{ 94 Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}, 95 Default: "zip", 96 } 97 98 // CmdDump represents the available dump sub-command. 99 var CmdDump = &cli.Command{ 100 Name: "dump", 101 Usage: "Dump Gitea files and database", 102 Description: `Dump compresses all related files and database into zip file. 103 It can be used for backup and capture Gitea server image to send to maintainer`, 104 Action: runDump, 105 Flags: []cli.Flag{ 106 &cli.StringFlag{ 107 Name: "file", 108 Aliases: []string{"f"}, 109 Value: fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()), 110 Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.", 111 }, 112 &cli.BoolFlag{ 113 Name: "verbose", 114 Aliases: []string{"V"}, 115 Usage: "Show process details", 116 }, 117 &cli.BoolFlag{ 118 Name: "quiet", 119 Aliases: []string{"q"}, 120 Usage: "Only display warnings and errors", 121 }, 122 &cli.StringFlag{ 123 Name: "tempdir", 124 Aliases: []string{"t"}, 125 Value: os.TempDir(), 126 Usage: "Temporary dir path", 127 }, 128 &cli.StringFlag{ 129 Name: "database", 130 Aliases: []string{"d"}, 131 Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", 132 }, 133 &cli.BoolFlag{ 134 Name: "skip-repository", 135 Aliases: []string{"R"}, 136 Usage: "Skip the repository dumping", 137 }, 138 &cli.BoolFlag{ 139 Name: "skip-log", 140 Aliases: []string{"L"}, 141 Usage: "Skip the log dumping", 142 }, 143 &cli.BoolFlag{ 144 Name: "skip-custom-dir", 145 Usage: "Skip custom directory", 146 }, 147 &cli.BoolFlag{ 148 Name: "skip-lfs-data", 149 Usage: "Skip LFS data", 150 }, 151 &cli.BoolFlag{ 152 Name: "skip-attachment-data", 153 Usage: "Skip attachment data", 154 }, 155 &cli.BoolFlag{ 156 Name: "skip-package-data", 157 Usage: "Skip package data", 158 }, 159 &cli.BoolFlag{ 160 Name: "skip-index", 161 Usage: "Skip bleve index data", 162 }, 163 &cli.GenericFlag{ 164 Name: "type", 165 Value: outputTypeEnum, 166 Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()), 167 }, 168 }, 169 } 170 171 func fatal(format string, args ...any) { 172 fmt.Fprintf(os.Stderr, format+"\n", args...) 173 log.Fatal(format, args...) 174 } 175 176 func runDump(ctx *cli.Context) error { 177 var file *os.File 178 fileName := ctx.String("file") 179 outType := ctx.String("type") 180 if fileName == "-" { 181 file = os.Stdout 182 setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) 183 } else { 184 for _, suffix := range outputTypeEnum.Enum { 185 if strings.HasSuffix(fileName, "."+suffix) { 186 fileName = strings.TrimSuffix(fileName, "."+suffix) 187 break 188 } 189 } 190 fileName += "." + outType 191 } 192 setting.MustInstalled() 193 194 // make sure we are logging to the console no matter what the configuration tells us do to 195 // FIXME: don't use CfgProvider directly 196 if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil { 197 fatal("Setting logging mode to console failed: %v", err) 198 } 199 if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil { 200 fatal("Setting console logger to stderr failed: %v", err) 201 } 202 203 // Set loglevel to Warn if quiet-mode is requested 204 if ctx.Bool("quiet") { 205 if _, err := setting.CfgProvider.Section("log.console").NewKey("LEVEL", "Warn"); err != nil { 206 fatal("Setting console log-level failed: %v", err) 207 } 208 } 209 210 if !setting.InstallLock { 211 log.Error("Is '%s' really the right config path?\n", setting.CustomConf) 212 return fmt.Errorf("gitea is not initialized") 213 } 214 setting.LoadSettings() // cannot access session settings otherwise 215 216 verbose := ctx.Bool("verbose") 217 if verbose && ctx.Bool("quiet") { 218 return fmt.Errorf("--quiet and --verbose cannot both be set") 219 } 220 221 stdCtx, cancel := installSignals() 222 defer cancel() 223 224 err := db.InitEngine(stdCtx) 225 if err != nil { 226 return err 227 } 228 229 if err := storage.Init(); err != nil { 230 return err 231 } 232 233 if file == nil { 234 file, err = os.Create(fileName) 235 if err != nil { 236 fatal("Unable to open %s: %v", fileName, err) 237 } 238 } 239 defer file.Close() 240 241 absFileName, err := filepath.Abs(fileName) 242 if err != nil { 243 return err 244 } 245 246 var iface any 247 if fileName == "-" { 248 iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType)) 249 } else { 250 iface, err = archiver.ByExtension(fileName) 251 } 252 if err != nil { 253 fatal("Unable to get archiver for extension: %v", err) 254 } 255 256 w, _ := iface.(archiver.Writer) 257 if err := w.Create(file); err != nil { 258 fatal("Creating archiver.Writer failed: %v", err) 259 } 260 defer w.Close() 261 262 if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") { 263 log.Info("Skip dumping local repositories") 264 } else { 265 log.Info("Dumping local repositories... %s", setting.RepoRootPath) 266 if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil { 267 fatal("Failed to include repositories: %v", err) 268 } 269 270 if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") { 271 log.Info("Skip dumping LFS data") 272 } else if !setting.LFS.StartServer { 273 log.Info("LFS isn't enabled. Skip dumping LFS data") 274 } else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error { 275 info, err := object.Stat() 276 if err != nil { 277 return err 278 } 279 280 return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose) 281 }); err != nil { 282 fatal("Failed to dump LFS objects: %v", err) 283 } 284 } 285 286 tmpDir := ctx.String("tempdir") 287 if _, err := os.Stat(tmpDir); os.IsNotExist(err) { 288 fatal("Path does not exist: %s", tmpDir) 289 } 290 291 dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql") 292 if err != nil { 293 fatal("Failed to create tmp file: %v", err) 294 } 295 defer func() { 296 _ = dbDump.Close() 297 if err := util.Remove(dbDump.Name()); err != nil { 298 log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err) 299 } 300 }() 301 302 targetDBType := ctx.String("database") 303 if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { 304 log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) 305 } else { 306 log.Info("Dumping database...") 307 } 308 309 if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil { 310 fatal("Failed to dump database: %v", err) 311 } 312 313 if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil { 314 fatal("Failed to include gitea-db.sql: %v", err) 315 } 316 317 if len(setting.CustomConf) > 0 { 318 log.Info("Adding custom configuration file from %s", setting.CustomConf) 319 if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil { 320 fatal("Failed to include specified app.ini: %v", err) 321 } 322 } 323 324 if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") { 325 log.Info("Skipping custom directory") 326 } else { 327 customDir, err := os.Stat(setting.CustomPath) 328 if err == nil && customDir.IsDir() { 329 if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is { 330 if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil { 331 fatal("Failed to include custom: %v", err) 332 } 333 } else { 334 log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath) 335 } 336 } else { 337 log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath) 338 } 339 } 340 341 isExist, err := util.IsExist(setting.AppDataPath) 342 if err != nil { 343 log.Error("Unable to check if %s exists. Error: %v", setting.AppDataPath, err) 344 } 345 if isExist { 346 log.Info("Packing data directory...%s", setting.AppDataPath) 347 348 var excludes []string 349 if setting.SessionConfig.OriginalProvider == "file" { 350 var opts session.Options 351 if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil { 352 return err 353 } 354 excludes = append(excludes, opts.ProviderConfig) 355 } 356 357 if ctx.IsSet("skip-index") && ctx.Bool("skip-index") { 358 excludes = append(excludes, setting.Indexer.RepoPath) 359 excludes = append(excludes, setting.Indexer.IssuePath) 360 } 361 362 excludes = append(excludes, setting.RepoRootPath) 363 excludes = append(excludes, setting.LFS.Storage.Path) 364 excludes = append(excludes, setting.Attachment.Storage.Path) 365 excludes = append(excludes, setting.Packages.Storage.Path) 366 excludes = append(excludes, setting.Log.RootPath) 367 excludes = append(excludes, absFileName) 368 if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil { 369 fatal("Failed to include data directory: %v", err) 370 } 371 } 372 373 if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") { 374 log.Info("Skip dumping attachment data") 375 } else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error { 376 info, err := object.Stat() 377 if err != nil { 378 return err 379 } 380 381 return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose) 382 }); err != nil { 383 fatal("Failed to dump attachments: %v", err) 384 } 385 386 if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") { 387 log.Info("Skip dumping package data") 388 } else if !setting.Packages.Enabled { 389 log.Info("Packages isn't enabled. Skip dumping package data") 390 } else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error { 391 info, err := object.Stat() 392 if err != nil { 393 return err 394 } 395 396 return addReader(w, object, info, path.Join("data", "packages", objPath), verbose) 397 }); err != nil { 398 fatal("Failed to dump packages: %v", err) 399 } 400 401 // Doesn't check if LogRootPath exists before processing --skip-log intentionally, 402 // ensuring that it's clear the dump is skipped whether the directory's initialized 403 // yet or not. 404 if ctx.IsSet("skip-log") && ctx.Bool("skip-log") { 405 log.Info("Skip dumping log files") 406 } else { 407 isExist, err := util.IsExist(setting.Log.RootPath) 408 if err != nil { 409 log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err) 410 } 411 if isExist { 412 if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil { 413 fatal("Failed to include log: %v", err) 414 } 415 } 416 } 417 418 if fileName != "-" { 419 if err = w.Close(); err != nil { 420 _ = util.Remove(fileName) 421 fatal("Failed to save %s: %v", fileName, err) 422 } 423 424 if err := os.Chmod(fileName, 0o600); err != nil { 425 log.Info("Can't change file access permissions mask to 0600: %v", err) 426 } 427 } 428 429 if fileName != "-" { 430 log.Info("Finish dumping in file %s", fileName) 431 } else { 432 log.Info("Finish dumping to stdout") 433 } 434 435 return nil 436 } 437 438 // addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath 439 func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error { 440 absPath, err := filepath.Abs(absPath) 441 if err != nil { 442 return err 443 } 444 dir, err := os.Open(absPath) 445 if err != nil { 446 return err 447 } 448 defer dir.Close() 449 450 files, err := dir.Readdir(0) 451 if err != nil { 452 return err 453 } 454 for _, file := range files { 455 currentAbsPath := filepath.Join(absPath, file.Name()) 456 currentInsidePath := path.Join(insidePath, file.Name()) 457 if file.IsDir() { 458 if !util.SliceContainsString(excludeAbsPath, currentAbsPath) { 459 if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil { 460 return err 461 } 462 if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil { 463 return err 464 } 465 } 466 } else { 467 // only copy regular files and symlink regular files, skip non-regular files like socket/pipe/... 468 shouldAdd := file.Mode().IsRegular() 469 if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink { 470 target, err := filepath.EvalSymlinks(currentAbsPath) 471 if err != nil { 472 return err 473 } 474 targetStat, err := os.Stat(target) 475 if err != nil { 476 return err 477 } 478 shouldAdd = targetStat.Mode().IsRegular() 479 } 480 if shouldAdd { 481 if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil { 482 return err 483 } 484 } 485 } 486 } 487 return nil 488 }