gitea源码

dumper.go 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package dump
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "io/fs"
  10. "os"
  11. "path"
  12. "path/filepath"
  13. "slices"
  14. "strings"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/setting"
  17. "code.gitea.io/gitea/modules/timeutil"
  18. "github.com/mholt/archives"
  19. )
  20. var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}
  21. // PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType"
  22. func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) {
  23. if argFile == "" && argType == "" {
  24. outType = SupportedOutputTypes[0]
  25. outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
  26. } else if argFile == "" {
  27. outType = argType
  28. outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
  29. } else if argType == "" {
  30. if filepath.Ext(outFileName) == "" {
  31. outType = SupportedOutputTypes[0]
  32. outFileName = argFile
  33. } else {
  34. for _, t := range SupportedOutputTypes {
  35. if strings.HasSuffix(argFile, "."+t) {
  36. outFileName = argFile
  37. outType = t
  38. }
  39. }
  40. }
  41. } else {
  42. outFileName, outType = argFile, argType
  43. }
  44. if !slices.Contains(SupportedOutputTypes, outType) {
  45. return "", ""
  46. }
  47. return outFileName, outType
  48. }
  49. func IsSubdir(upper, lower string) (bool, error) {
  50. if relPath, err := filepath.Rel(upper, lower); err != nil {
  51. return false, err
  52. } else if relPath == "." || !strings.HasPrefix(relPath, ".") {
  53. return true, nil
  54. }
  55. return false, nil
  56. }
  57. type Dumper struct {
  58. Verbose bool
  59. jobs chan archives.ArchiveAsyncJob
  60. errArchiveAsync chan error
  61. errArchiveJob chan error
  62. globalExcludeAbsPaths []string
  63. }
  64. func NewDumper(ctx context.Context, format string, output io.Writer) (*Dumper, error) {
  65. d := &Dumper{
  66. jobs: make(chan archives.ArchiveAsyncJob, 1),
  67. errArchiveAsync: make(chan error, 1),
  68. errArchiveJob: make(chan error, 1),
  69. }
  70. // TODO: in the future, we could completely drop the "mholt/archives" dependency.
  71. // Then we only need to support "zip" and ".tar.gz" natively, and let users provide custom command line tools
  72. // like "zstd" or "xz" with compression-level arguments.
  73. var comp archives.ArchiverAsync
  74. switch format {
  75. case "zip":
  76. comp = archives.Zip{}
  77. case "tar":
  78. comp = archives.Tar{}
  79. case "tar.sz":
  80. comp = archives.CompressedArchive{Compression: archives.Sz{}, Archival: archives.Tar{}}
  81. case "tar.gz":
  82. comp = archives.CompressedArchive{Compression: archives.Gz{}, Archival: archives.Tar{}}
  83. case "tar.xz":
  84. comp = archives.CompressedArchive{Compression: archives.Xz{}, Archival: archives.Tar{}}
  85. case "tar.bz2":
  86. comp = archives.CompressedArchive{Compression: archives.Bz2{}, Archival: archives.Tar{}}
  87. case "tar.br":
  88. comp = archives.CompressedArchive{Compression: archives.Brotli{}, Archival: archives.Tar{}}
  89. case "tar.lz4":
  90. comp = archives.CompressedArchive{Compression: archives.Lz4{}, Archival: archives.Tar{}}
  91. case "tar.zst":
  92. comp = archives.CompressedArchive{Compression: archives.Zstd{}, Archival: archives.Tar{}}
  93. default:
  94. return nil, fmt.Errorf("unsupported format: %s", format)
  95. }
  96. go func() {
  97. d.errArchiveAsync <- comp.ArchiveAsync(ctx, output, d.jobs)
  98. close(d.errArchiveAsync)
  99. }()
  100. return d, nil
  101. }
  102. func (dumper *Dumper) runArchiveJob(job archives.ArchiveAsyncJob) error {
  103. dumper.jobs <- job
  104. select {
  105. case err := <-dumper.errArchiveAsync:
  106. if err == nil {
  107. return errors.New("archiver has been closed")
  108. }
  109. return err
  110. case err := <-dumper.errArchiveJob:
  111. return err
  112. }
  113. }
  114. // AddFileByPath adds a file by its filesystem path
  115. func (dumper *Dumper) AddFileByPath(filePath, absPath string) error {
  116. if dumper.Verbose {
  117. log.Info("Adding local file %s", filePath)
  118. }
  119. fileInfo, err := os.Stat(absPath)
  120. if err != nil {
  121. return err
  122. }
  123. archiveFileInfo := archives.FileInfo{
  124. FileInfo: fileInfo,
  125. NameInArchive: filePath,
  126. Open: func() (fs.File, error) { return os.Open(absPath) },
  127. }
  128. return dumper.runArchiveJob(archives.ArchiveAsyncJob{
  129. File: archiveFileInfo,
  130. Result: dumper.errArchiveJob,
  131. })
  132. }
  133. type readerFile struct {
  134. r io.Reader
  135. info os.FileInfo
  136. }
  137. var _ fs.File = (*readerFile)(nil)
  138. func (f *readerFile) Stat() (fs.FileInfo, error) { return f.info, nil }
  139. func (f *readerFile) Read(bytes []byte) (int, error) { return f.r.Read(bytes) }
  140. func (f *readerFile) Close() error { return nil }
  141. // AddFileByReader adds a file's contents from a Reader
  142. func (dumper *Dumper) AddFileByReader(r io.Reader, info os.FileInfo, customName string) error {
  143. if dumper.Verbose {
  144. log.Info("Adding storage file %s", customName)
  145. }
  146. fileInfo := archives.FileInfo{
  147. FileInfo: info,
  148. NameInArchive: customName,
  149. Open: func() (fs.File, error) { return &readerFile{r, info}, nil },
  150. }
  151. return dumper.runArchiveJob(archives.ArchiveAsyncJob{
  152. File: fileInfo,
  153. Result: dumper.errArchiveJob,
  154. })
  155. }
  156. func (dumper *Dumper) Close() error {
  157. close(dumper.jobs)
  158. return <-dumper.errArchiveAsync
  159. }
  160. func (dumper *Dumper) normalizeFilePath(absPath string) string {
  161. absPath = filepath.Clean(absPath)
  162. if setting.IsWindows {
  163. absPath = strings.ToLower(absPath)
  164. }
  165. return absPath
  166. }
  167. func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) {
  168. for _, absPath := range absPaths {
  169. dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath))
  170. }
  171. }
  172. func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool {
  173. norm := dumper.normalizeFilePath(absPath)
  174. return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm)
  175. }
  176. func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error {
  177. excludes = slices.Clone(excludes)
  178. for i := range excludes {
  179. excludes[i] = dumper.normalizeFilePath(excludes[i])
  180. }
  181. return dumper.addFileOrDir(insidePath, absPath, excludes)
  182. }
  183. func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error {
  184. absPath, err := filepath.Abs(absPath)
  185. if err != nil {
  186. return err
  187. }
  188. dir, err := os.Open(absPath)
  189. if err != nil {
  190. return err
  191. }
  192. defer dir.Close()
  193. files, err := dir.Readdir(0)
  194. if err != nil {
  195. return err
  196. }
  197. for _, file := range files {
  198. currentAbsPath := filepath.Join(absPath, file.Name())
  199. if dumper.shouldExclude(currentAbsPath, excludes) {
  200. continue
  201. }
  202. currentInsidePath := path.Join(insidePath, file.Name())
  203. if file.IsDir() {
  204. if err := dumper.AddFileByPath(currentInsidePath, currentAbsPath); err != nil {
  205. return err
  206. }
  207. if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil {
  208. return err
  209. }
  210. } else {
  211. // only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
  212. shouldAdd := file.Mode().IsRegular()
  213. if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
  214. target, err := filepath.EvalSymlinks(currentAbsPath)
  215. if err != nil {
  216. return err
  217. }
  218. targetStat, err := os.Stat(target)
  219. if err != nil {
  220. return err
  221. }
  222. shouldAdd = targetStat.Mode().IsRegular()
  223. }
  224. if shouldAdd {
  225. if err = dumper.AddFileByPath(currentInsidePath, currentAbsPath); err != nil {
  226. return err
  227. }
  228. }
  229. }
  230. }
  231. return nil
  232. }