batch.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. package lfs
  2. import (
  3. "fmt"
  4. "net/http"
  5. jsoniter "github.com/json-iterator/go"
  6. "github.com/flamego/flamego"
  7. log "unknwon.dev/clog/v2"
  8. "gogs.io/gogs/internal/conf"
  9. "gogs.io/gogs/internal/database"
  10. "gogs.io/gogs/internal/lfsutil"
  11. "gogs.io/gogs/internal/strutil"
  12. )
  13. // POST /{owner}/{repo}.git/info/lfs/object/batch
  14. func serveBatch(store Store) macaron.Handler {
  15. return func(c *macaron.Context, owner *database.User, repo *database.Repository) {
  16. var request batchRequest
  17. defer func() { _ = c.Req.Request.Body.Close() }()
  18. err := jsoniter.NewDecoder(c.Req.Request.Body).Decode(&request)
  19. if err != nil {
  20. responseJSON(c.Resp, http.StatusBadRequest, responseError{
  21. Message: strutil.ToUpperFirst(err.Error()),
  22. })
  23. return
  24. }
  25. // NOTE: We only support basic transfer as of now.
  26. transfer := transferBasic
  27. // Example: https://try.gogs.io/gogs/gogs.git/info/lfs/object/basic
  28. baseHref := fmt.Sprintf("%s%s/%s.git/info/lfs/objects/basic", conf.Server.ExternalURL, owner.Name, repo.Name)
  29. objects := make([]batchObject, 0, len(request.Objects))
  30. switch request.Operation {
  31. case basicOperationUpload:
  32. for _, obj := range request.Objects {
  33. var actions batchActions
  34. if lfsutil.ValidOID(obj.Oid) {
  35. actions = batchActions{
  36. Upload: &batchAction{
  37. Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),
  38. Header: map[string]string{
  39. // NOTE: git-lfs v2.5.0 sets the Content-Type based on the uploaded file.
  40. // This ensures that the client always uses the designated value for the header.
  41. "Content-Type": "application/octet-stream",
  42. },
  43. },
  44. Verify: &batchAction{
  45. Href: fmt.Sprintf("%s/verify", baseHref),
  46. },
  47. }
  48. } else {
  49. actions = batchActions{
  50. Error: &batchError{
  51. Code: http.StatusUnprocessableEntity,
  52. Message: "Object has invalid oid",
  53. },
  54. }
  55. }
  56. objects = append(objects, batchObject{
  57. Oid: obj.Oid,
  58. Size: obj.Size,
  59. Actions: actions,
  60. })
  61. }
  62. case basicOperationDownload:
  63. oids := make([]lfsutil.OID, 0, len(request.Objects))
  64. for _, obj := range request.Objects {
  65. oids = append(oids, obj.Oid)
  66. }
  67. stored, err := store.GetLFSObjectsByOIDs(c.Req.Context(), repo.ID, oids...)
  68. if err != nil {
  69. internalServerError(c.Resp)
  70. log.Error("Failed to get objects [repo_id: %d, oids: %v]: %v", repo.ID, oids, err)
  71. return
  72. }
  73. storedSet := make(map[lfsutil.OID]*database.LFSObject, len(stored))
  74. for _, obj := range stored {
  75. storedSet[obj.OID] = obj
  76. }
  77. for _, obj := range request.Objects {
  78. var actions batchActions
  79. if stored := storedSet[obj.Oid]; stored != nil {
  80. if stored.Size != obj.Size {
  81. actions.Error = &batchError{
  82. Code: http.StatusUnprocessableEntity,
  83. Message: "Object size mismatch",
  84. }
  85. } else {
  86. actions.Download = &batchAction{
  87. Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),
  88. }
  89. }
  90. } else {
  91. actions.Error = &batchError{
  92. Code: http.StatusNotFound,
  93. Message: "Object does not exist",
  94. }
  95. }
  96. objects = append(objects, batchObject{
  97. Oid: obj.Oid,
  98. Size: obj.Size,
  99. Actions: actions,
  100. })
  101. }
  102. default:
  103. responseJSON(c.Resp, http.StatusBadRequest, responseError{
  104. Message: "Operation not recognized",
  105. })
  106. return
  107. }
  108. responseJSON(c.Resp, http.StatusOK, batchResponse{
  109. Transfer: transfer,
  110. Objects: objects,
  111. })
  112. }
  113. }
  114. // batchRequest defines the request payload for the batch endpoint.
  115. type batchRequest struct {
  116. Operation string `json:"operation"`
  117. Objects []struct {
  118. Oid lfsutil.OID `json:"oid"`
  119. Size int64 `json:"size"`
  120. } `json:"objects"`
  121. }
  122. type batchError struct {
  123. Code int `json:"code"`
  124. Message string `json:"message"`
  125. }
  126. type batchAction struct {
  127. Href string `json:"href"`
  128. Header map[string]string `json:"header,omitempty"`
  129. }
  130. type batchActions struct {
  131. Download *batchAction `json:"download,omitempty"`
  132. Upload *batchAction `json:"upload,omitempty"`
  133. Verify *batchAction `json:"verify,omitempty"`
  134. Error *batchError `json:"error,omitempty"`
  135. }
  136. type batchObject struct {
  137. Oid lfsutil.OID `json:"oid"`
  138. Size int64 `json:"size"`
  139. Actions batchActions `json:"actions"`
  140. }
  141. // batchResponse defines the response payload for the batch endpoint.
  142. type batchResponse struct {
  143. Transfer string `json:"transfer"`
  144. Objects []batchObject `json:"objects"`
  145. }
  146. type responseError struct {
  147. Message string `json:"message"`
  148. }
  149. const contentType = "application/vnd.git-lfs+json"
  150. func responseJSON(w http.ResponseWriter, status int, v any) {
  151. w.Header().Set("Content-Type", contentType)
  152. w.WriteHeader(status)
  153. err := jsoniter.NewEncoder(w).Encode(v)
  154. if err != nil {
  155. log.Error("Failed to encode JSON: %v", err)
  156. return
  157. }
  158. }