dataDownloadArchiveService.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package services
  2. import (
  3. "archive/tar"
  4. "compress/gzip"
  5. "context"
  6. "fmt"
  7. "io"
  8. "path"
  9. "strings"
  10. "time"
  11. "fwh-toolbox-server/cache"
  12. minioRepo "fwh-toolbox-server/repositories/minio"
  13. mongoRepo "fwh-toolbox-server/repositories/mongo"
  14. )
  15. type DataDownloadArchiveEntry struct {
  16. Path string `json:"path"`
  17. Size int64 `json:"size"`
  18. Directory bool `json:"directory"`
  19. }
  20. func ListDataDownloadArchive(ctx context.Context, documentID string) ([]DataDownloadArchiveEntry, error) {
  21. lock, err := cache.Lock(ctx, "data-download:content:"+documentID, 10*time.Minute, 100*time.Millisecond, 10*time.Second)
  22. if err != nil {
  23. return nil, err
  24. }
  25. defer lock.SafeRelease(context.Background())
  26. reader, compressed, archive, err := openDataDownloadTarGz(ctx, documentID)
  27. if err != nil {
  28. return nil, err
  29. }
  30. defer reader.Close()
  31. defer compressed.Close()
  32. var entries []DataDownloadArchiveEntry
  33. for {
  34. header, err := archive.Next()
  35. if err == io.EOF {
  36. break
  37. }
  38. if err != nil {
  39. return nil, err
  40. }
  41. clean, err := safeArchiveEntry(header.Name)
  42. if err != nil {
  43. continue
  44. }
  45. entries = append(entries, DataDownloadArchiveEntry{Path: clean, Size: header.Size, Directory: header.FileInfo().IsDir()})
  46. }
  47. return entries, nil
  48. }
  49. func ReadDataDownloadArchiveEntry(ctx context.Context, documentID, entryPath string) ([]byte, error) {
  50. lock, err := cache.Lock(ctx, "data-download:content:"+documentID, 10*time.Minute, 100*time.Millisecond, 10*time.Second)
  51. if err != nil {
  52. return nil, err
  53. }
  54. defer lock.SafeRelease(context.Background())
  55. wanted, err := safeArchiveEntry(entryPath)
  56. if err != nil {
  57. return nil, err
  58. }
  59. reader, compressed, archive, err := openDataDownloadTarGz(ctx, documentID)
  60. if err != nil {
  61. return nil, err
  62. }
  63. defer reader.Close()
  64. defer compressed.Close()
  65. for {
  66. header, err := archive.Next()
  67. if err == io.EOF {
  68. return nil, fmt.Errorf("archive entry not found")
  69. }
  70. if err != nil {
  71. return nil, err
  72. }
  73. clean, cleanErr := safeArchiveEntry(header.Name)
  74. if cleanErr != nil || clean != wanted || header.FileInfo().IsDir() {
  75. continue
  76. }
  77. return io.ReadAll(io.LimitReader(archive, header.Size))
  78. }
  79. }
  80. func openDataDownloadTarGz(ctx context.Context, documentID string) (io.ReadCloser, *gzip.Reader, *tar.Reader, error) {
  81. document, err := mongoRepo.GetDataDownloadDocument(ctx, documentID)
  82. if err != nil {
  83. return nil, nil, nil, err
  84. }
  85. if document.BinaryObjectKey == "" || !(strings.HasSuffix(strings.ToLower(document.BinaryObjectKey), ".tar.gz") || strings.HasSuffix(strings.ToLower(document.BinaryObjectKey), ".tgz")) {
  86. return nil, nil, nil, fmt.Errorf("document is not a tar.gz archive")
  87. }
  88. reader, err := minioRepo.Open(ctx, document.BinaryObjectKey)
  89. if err != nil {
  90. return nil, nil, nil, err
  91. }
  92. gz, err := gzip.NewReader(reader)
  93. if err != nil {
  94. _ = reader.Close()
  95. return nil, nil, nil, err
  96. }
  97. return reader, gz, tar.NewReader(gz), nil
  98. }
  99. func safeArchiveEntry(value string) (string, error) {
  100. clean := strings.TrimPrefix(path.Clean("/"+strings.ReplaceAll(value, "\\", "/")), "/")
  101. if clean == "." || clean == "" || strings.HasPrefix(clean, "../") {
  102. return "", fmt.Errorf("invalid archive entry")
  103. }
  104. return clean, nil
  105. }