batch.go 4.9 KB

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