batch.go 4.7 KB

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