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