You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

168 lines
4.4 KiB

  1. package main
  2. import (
  3. "archive/tar"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "hash/crc64"
  8. "net/http"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. "github.com/go-redis/redis/v8"
  15. "github.com/gorilla/mux"
  16. )
  17. type DumpChunkName struct {
  18. Key string `json:"key"`
  19. Distance int `json:"distance"`
  20. Cursor int64 `json:"cursor"`
  21. Checksum uint64 `json:"checksum"`
  22. }
  23. func (that *GlobalBackfeedManager) HandleDump(res http.ResponseWriter, req *http.Request) {
  24. vars := mux.Vars(req)
  25. key := vars["key"]
  26. if key == "" {
  27. key = "*:*:*"
  28. }
  29. if strings.Count(key, ":") < 2 {
  30. WriteResponse(res, http.StatusBadRequest, fmt.Errorf("invalid key format"))
  31. return
  32. }
  33. lock := sync.Mutex{}
  34. keys := []string{}
  35. if err := that.BackfeedRedis.ForEachShard(req.Context(), func(ctx context.Context, client *redis.Client) error {
  36. cursor := uint64(0)
  37. var shardKeys []string
  38. for {
  39. var err error
  40. var keysBatch []string
  41. keysBatch, cursor, err = client.Scan(ctx, cursor, key, 1000).Result()
  42. if err != nil && err != redis.Nil {
  43. return err
  44. }
  45. shardKeys = append(shardKeys, keysBatch...)
  46. if cursor == 0 {
  47. break
  48. }
  49. }
  50. lock.Lock()
  51. defer lock.Unlock()
  52. keys = append(keys, shardKeys...)
  53. return nil
  54. }); err != nil && err != redis.Nil {
  55. WriteResponse(res, http.StatusInternalServerError, err)
  56. return
  57. }
  58. sort.Strings(keys)
  59. hasJsonAcceptHeader := false
  60. for _, accept := range strings.Split(req.Header.Get("Accept"), ",") {
  61. accept = strings.TrimSpace(accept)
  62. if accept == "application/json" || strings.HasPrefix(accept, "application/json;") {
  63. hasJsonAcceptHeader = true
  64. break
  65. }
  66. }
  67. if hasJsonAcceptHeader {
  68. WriteResponse(res, http.StatusOK, keys)
  69. return
  70. }
  71. if len(keys) == 0 {
  72. WriteResponse(res, http.StatusNoContent, nil)
  73. return
  74. }
  75. tarWriter := tar.NewWriter(res)
  76. defer tarWriter.Close()
  77. pipe := that.BackfeedRedis.Pipeline()
  78. for _, key := range keys {
  79. cursor := int64(0)
  80. for i := 0; ; i++ {
  81. rawResResult := pipe.Do(req.Context(), "bf.scandump", key, cursor)
  82. tsStringResult := that.BackfeedRedis.HGet(req.Context(), ":last_ts", key)
  83. _, err := pipe.Exec(req.Context())
  84. if err != nil && err != redis.Nil {
  85. WriteResponse(res, http.StatusInternalServerError, err)
  86. return
  87. }
  88. rawRes, err := rawResResult.Result()
  89. if err != nil && err != redis.Nil {
  90. WriteResponse(res, http.StatusInternalServerError, err)
  91. return
  92. }
  93. if rawRes == nil {
  94. break
  95. }
  96. resSlice, ok := rawRes.([]any)
  97. if !ok {
  98. WriteResponse(res, http.StatusInternalServerError, fmt.Errorf("unexpected response type: %T", rawRes))
  99. return
  100. }
  101. if len(resSlice) != 2 {
  102. WriteResponse(res, http.StatusInternalServerError, fmt.Errorf("unexpected response length: %d", len(resSlice)))
  103. return
  104. }
  105. cursor, ok = resSlice[0].(int64)
  106. if !ok {
  107. WriteResponse(res, http.StatusInternalServerError, fmt.Errorf("unexpected response first element type: %T", resSlice[0]))
  108. return
  109. }
  110. chunkString, ok := resSlice[1].(string)
  111. if !ok {
  112. WriteResponse(res, http.StatusInternalServerError, fmt.Errorf("unexpected response second element type: %T", resSlice[1]))
  113. return
  114. }
  115. chunk := []byte(chunkString)
  116. lastAccess := time.Time{}
  117. tsString, err := tsStringResult.Result()
  118. if err == nil && tsString != "" {
  119. ts, err := strconv.ParseInt(tsString, 10, 64)
  120. if err == nil {
  121. lastAccess = time.Unix(ts, 0)
  122. }
  123. }
  124. nameStruct := DumpChunkName{
  125. Key: key,
  126. Cursor: cursor,
  127. Distance: i,
  128. Checksum: crc64.Checksum(chunk, crc64.MakeTable(crc64.ECMA)),
  129. }
  130. name, err := json.Marshal(nameStruct)
  131. if err != nil {
  132. WriteResponse(res, http.StatusInternalServerError, err)
  133. return
  134. }
  135. if err := tarWriter.WriteHeader(&tar.Header{
  136. Typeflag: tar.TypeReg,
  137. Name: string(name),
  138. Size: int64(len(chunk)),
  139. Mode: 0600,
  140. ModTime: lastAccess,
  141. AccessTime: lastAccess,
  142. ChangeTime: lastAccess,
  143. PAXRecords: map[string]string{
  144. "ARCHIVETEAM.bffchunk.key": key,
  145. "ARCHIVETEAM.bffchunk.cursor": fmt.Sprintf("%d", cursor),
  146. "ARCHIVETEAM.bffchunk.distance": fmt.Sprintf("%d", i),
  147. "ARCHIVETEAM.bffchunk.checksum": fmt.Sprintf("%d", nameStruct.Checksum),
  148. },
  149. Format: tar.FormatPAX,
  150. }); err != nil {
  151. WriteResponse(res, http.StatusInternalServerError, err)
  152. }
  153. if _, err := tarWriter.Write(chunk); err != nil {
  154. WriteResponse(res, http.StatusInternalServerError, err)
  155. }
  156. if cursor == 0 && len(chunk) == 0 {
  157. break
  158. }
  159. }
  160. }
  161. }