code.gitea.io/gitea@v1.22.3/modules/dump/dumper.go (about)

     1  // Copyright 2024 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package dump
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"slices"
    13  	"strings"
    14  
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/setting"
    17  	"code.gitea.io/gitea/modules/timeutil"
    18  
    19  	"github.com/mholt/archiver/v3"
    20  )
    21  
    22  var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}
    23  
    24  // PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType"
    25  func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) {
    26  	if argFile == "" && argType == "" {
    27  		outType = SupportedOutputTypes[0]
    28  		outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
    29  	} else if argFile == "" {
    30  		outType = argType
    31  		outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
    32  	} else if argType == "" {
    33  		if filepath.Ext(outFileName) == "" {
    34  			outType = SupportedOutputTypes[0]
    35  			outFileName = argFile
    36  		} else {
    37  			for _, t := range SupportedOutputTypes {
    38  				if strings.HasSuffix(argFile, "."+t) {
    39  					outFileName = argFile
    40  					outType = t
    41  				}
    42  			}
    43  		}
    44  	} else {
    45  		outFileName, outType = argFile, argType
    46  	}
    47  	if !slices.Contains(SupportedOutputTypes, outType) {
    48  		return "", ""
    49  	}
    50  	return outFileName, outType
    51  }
    52  
    53  func IsSubdir(upper, lower string) (bool, error) {
    54  	if relPath, err := filepath.Rel(upper, lower); err != nil {
    55  		return false, err
    56  	} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
    57  		return true, nil
    58  	}
    59  	return false, nil
    60  }
    61  
    62  type Dumper struct {
    63  	Writer  archiver.Writer
    64  	Verbose bool
    65  
    66  	globalExcludeAbsPaths []string
    67  }
    68  
    69  func (dumper *Dumper) AddReader(r io.ReadCloser, info os.FileInfo, customName string) error {
    70  	if dumper.Verbose {
    71  		log.Info("Adding file %s", customName)
    72  	}
    73  
    74  	return dumper.Writer.Write(archiver.File{
    75  		FileInfo: archiver.FileInfo{
    76  			FileInfo:   info,
    77  			CustomName: customName,
    78  		},
    79  		ReadCloser: r,
    80  	})
    81  }
    82  
    83  func (dumper *Dumper) AddFile(filePath, absPath string) error {
    84  	file, err := os.Open(absPath)
    85  	if err != nil {
    86  		return err
    87  	}
    88  	defer file.Close()
    89  	fileInfo, err := file.Stat()
    90  	if err != nil {
    91  		return err
    92  	}
    93  	return dumper.AddReader(file, fileInfo, filePath)
    94  }
    95  
    96  func (dumper *Dumper) normalizeFilePath(absPath string) string {
    97  	absPath = filepath.Clean(absPath)
    98  	if setting.IsWindows {
    99  		absPath = strings.ToLower(absPath)
   100  	}
   101  	return absPath
   102  }
   103  
   104  func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) {
   105  	for _, absPath := range absPaths {
   106  		dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath))
   107  	}
   108  }
   109  
   110  func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool {
   111  	norm := dumper.normalizeFilePath(absPath)
   112  	return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm)
   113  }
   114  
   115  func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error {
   116  	excludes = slices.Clone(excludes)
   117  	for i := range excludes {
   118  		excludes[i] = dumper.normalizeFilePath(excludes[i])
   119  	}
   120  	return dumper.addFileOrDir(insidePath, absPath, excludes)
   121  }
   122  
   123  func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error {
   124  	absPath, err := filepath.Abs(absPath)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	dir, err := os.Open(absPath)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	defer dir.Close()
   133  
   134  	files, err := dir.Readdir(0)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	for _, file := range files {
   139  		currentAbsPath := filepath.Join(absPath, file.Name())
   140  		if dumper.shouldExclude(currentAbsPath, excludes) {
   141  			continue
   142  		}
   143  
   144  		currentInsidePath := path.Join(insidePath, file.Name())
   145  		if file.IsDir() {
   146  			if err := dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
   147  				return err
   148  			}
   149  			if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil {
   150  				return err
   151  			}
   152  		} else {
   153  			// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
   154  			shouldAdd := file.Mode().IsRegular()
   155  			if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
   156  				target, err := filepath.EvalSymlinks(currentAbsPath)
   157  				if err != nil {
   158  					return err
   159  				}
   160  				targetStat, err := os.Stat(target)
   161  				if err != nil {
   162  					return err
   163  				}
   164  				shouldAdd = targetStat.Mode().IsRegular()
   165  			}
   166  			if shouldAdd {
   167  				if err = dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
   168  					return err
   169  				}
   170  			}
   171  		}
   172  	}
   173  	return nil
   174  }