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.

locks.go 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. // Copyright 2017 The Gitea 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. "net/http"
  7. "strconv"
  8. "strings"
  9. "code.gitea.io/gitea/models"
  10. "code.gitea.io/gitea/modules/context"
  11. "code.gitea.io/gitea/modules/convert"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/setting"
  14. api "code.gitea.io/gitea/modules/structs"
  15. jsoniter "github.com/json-iterator/go"
  16. )
  17. //checkIsValidRequest check if it a valid request in case of bad request it write the response to ctx.
  18. func checkIsValidRequest(ctx *context.Context) bool {
  19. if !setting.LFS.StartServer {
  20. log.Debug("Attempt to access LFS server but LFS server is disabled")
  21. writeStatus(ctx, http.StatusNotFound)
  22. return false
  23. }
  24. if !MetaMatcher(ctx.Req) {
  25. log.Info("Attempt access LOCKs without accepting the correct media type: %s", metaMediaType)
  26. writeStatus(ctx, http.StatusBadRequest)
  27. return false
  28. }
  29. if !ctx.IsSigned {
  30. user, _, _, err := parseToken(ctx.Req.Header.Get("Authorization"))
  31. if err != nil {
  32. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
  33. writeStatus(ctx, http.StatusUnauthorized)
  34. return false
  35. }
  36. ctx.User = user
  37. }
  38. return true
  39. }
  40. func handleLockListOut(ctx *context.Context, repo *models.Repository, lock *models.LFSLock, err error) {
  41. if err != nil {
  42. if models.IsErrLFSLockNotExist(err) {
  43. ctx.JSON(http.StatusOK, api.LFSLockList{
  44. Locks: []*api.LFSLock{},
  45. })
  46. return
  47. }
  48. ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
  49. Message: "unable to list locks : Internal Server Error",
  50. })
  51. return
  52. }
  53. if repo.ID != lock.RepoID {
  54. ctx.JSON(http.StatusOK, api.LFSLockList{
  55. Locks: []*api.LFSLock{},
  56. })
  57. return
  58. }
  59. ctx.JSON(http.StatusOK, api.LFSLockList{
  60. Locks: []*api.LFSLock{convert.ToLFSLock(lock)},
  61. })
  62. }
  63. // GetListLockHandler list locks
  64. func GetListLockHandler(ctx *context.Context) {
  65. if !checkIsValidRequest(ctx) {
  66. // Status is written in checkIsValidRequest
  67. return
  68. }
  69. ctx.Resp.Header().Set("Content-Type", metaMediaType)
  70. rv := unpack(ctx)
  71. repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo)
  72. if err != nil {
  73. log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err)
  74. writeStatus(ctx, 404)
  75. return
  76. }
  77. repository.MustOwner()
  78. authenticated := authenticate(ctx, repository, rv.Authorization, false)
  79. if !authenticated {
  80. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
  81. ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
  82. Message: "You must have pull access to list locks",
  83. })
  84. return
  85. }
  86. cursor := ctx.QueryInt("cursor")
  87. if cursor < 0 {
  88. cursor = 0
  89. }
  90. limit := ctx.QueryInt("limit")
  91. if limit > setting.LFS.LocksPagingNum && setting.LFS.LocksPagingNum > 0 {
  92. limit = setting.LFS.LocksPagingNum
  93. } else if limit < 0 {
  94. limit = 0
  95. }
  96. id := ctx.Query("id")
  97. if id != "" { //Case where we request a specific id
  98. v, err := strconv.ParseInt(id, 10, 64)
  99. if err != nil {
  100. ctx.JSON(http.StatusBadRequest, api.LFSLockError{
  101. Message: "bad request : " + err.Error(),
  102. })
  103. return
  104. }
  105. lock, err := models.GetLFSLockByID(v)
  106. if err != nil && !models.IsErrLFSLockNotExist(err) {
  107. log.Error("Unable to get lock with ID[%s]: Error: %v", v, err)
  108. }
  109. handleLockListOut(ctx, repository, lock, err)
  110. return
  111. }
  112. path := ctx.Query("path")
  113. if path != "" { //Case where we request a specific id
  114. lock, err := models.GetLFSLock(repository, path)
  115. if err != nil && !models.IsErrLFSLockNotExist(err) {
  116. log.Error("Unable to get lock for repository %-v with path %s: Error: %v", repository, path, err)
  117. }
  118. handleLockListOut(ctx, repository, lock, err)
  119. return
  120. }
  121. //If no query params path or id
  122. lockList, err := models.GetLFSLockByRepoID(repository.ID, cursor, limit)
  123. if err != nil {
  124. log.Error("Unable to list locks for repository ID[%d]: Error: %v", repository.ID, err)
  125. ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
  126. Message: "unable to list locks : Internal Server Error",
  127. })
  128. return
  129. }
  130. lockListAPI := make([]*api.LFSLock, len(lockList))
  131. next := ""
  132. for i, l := range lockList {
  133. lockListAPI[i] = convert.ToLFSLock(l)
  134. }
  135. if limit > 0 && len(lockList) == limit {
  136. next = strconv.Itoa(cursor + 1)
  137. }
  138. ctx.JSON(http.StatusOK, api.LFSLockList{
  139. Locks: lockListAPI,
  140. Next: next,
  141. })
  142. }
  143. // PostLockHandler create lock
  144. func PostLockHandler(ctx *context.Context) {
  145. if !checkIsValidRequest(ctx) {
  146. // Status is written in checkIsValidRequest
  147. return
  148. }
  149. ctx.Resp.Header().Set("Content-Type", metaMediaType)
  150. userName := ctx.Params("username")
  151. repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
  152. authorization := ctx.Req.Header.Get("Authorization")
  153. repository, err := models.GetRepositoryByOwnerAndName(userName, repoName)
  154. if err != nil {
  155. log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
  156. writeStatus(ctx, 404)
  157. return
  158. }
  159. repository.MustOwner()
  160. authenticated := authenticate(ctx, repository, authorization, true)
  161. if !authenticated {
  162. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
  163. ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
  164. Message: "You must have push access to create locks",
  165. })
  166. return
  167. }
  168. var req api.LFSLockRequest
  169. bodyReader := ctx.Req.Body
  170. defer bodyReader.Close()
  171. json := jsoniter.ConfigCompatibleWithStandardLibrary
  172. dec := json.NewDecoder(bodyReader)
  173. if err := dec.Decode(&req); err != nil {
  174. log.Warn("Failed to decode lock request as json. Error: %v", err)
  175. writeStatus(ctx, 400)
  176. return
  177. }
  178. lock, err := models.CreateLFSLock(&models.LFSLock{
  179. Repo: repository,
  180. Path: req.Path,
  181. Owner: ctx.User,
  182. })
  183. if err != nil {
  184. if models.IsErrLFSLockAlreadyExist(err) {
  185. ctx.JSON(http.StatusConflict, api.LFSLockError{
  186. Lock: convert.ToLFSLock(lock),
  187. Message: "already created lock",
  188. })
  189. return
  190. }
  191. if models.IsErrLFSUnauthorizedAction(err) {
  192. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
  193. ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
  194. Message: "You must have push access to create locks : " + err.Error(),
  195. })
  196. return
  197. }
  198. log.Error("Unable to CreateLFSLock in repository %-v at %s for user %-v: Error: %v", repository, req.Path, ctx.User, err)
  199. ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
  200. Message: "internal server error : Internal Server Error",
  201. })
  202. return
  203. }
  204. ctx.JSON(http.StatusCreated, api.LFSLockResponse{Lock: convert.ToLFSLock(lock)})
  205. }
  206. // VerifyLockHandler list locks for verification
  207. func VerifyLockHandler(ctx *context.Context) {
  208. if !checkIsValidRequest(ctx) {
  209. // Status is written in checkIsValidRequest
  210. return
  211. }
  212. ctx.Resp.Header().Set("Content-Type", metaMediaType)
  213. userName := ctx.Params("username")
  214. repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
  215. authorization := ctx.Req.Header.Get("Authorization")
  216. repository, err := models.GetRepositoryByOwnerAndName(userName, repoName)
  217. if err != nil {
  218. log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
  219. writeStatus(ctx, 404)
  220. return
  221. }
  222. repository.MustOwner()
  223. authenticated := authenticate(ctx, repository, authorization, true)
  224. if !authenticated {
  225. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
  226. ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
  227. Message: "You must have push access to verify locks",
  228. })
  229. return
  230. }
  231. cursor := ctx.QueryInt("cursor")
  232. if cursor < 0 {
  233. cursor = 0
  234. }
  235. limit := ctx.QueryInt("limit")
  236. if limit > setting.LFS.LocksPagingNum && setting.LFS.LocksPagingNum > 0 {
  237. limit = setting.LFS.LocksPagingNum
  238. } else if limit < 0 {
  239. limit = 0
  240. }
  241. lockList, err := models.GetLFSLockByRepoID(repository.ID, cursor, limit)
  242. if err != nil {
  243. log.Error("Unable to list locks for repository ID[%d]: Error: %v", repository.ID, err)
  244. ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
  245. Message: "unable to list locks : Internal Server Error",
  246. })
  247. return
  248. }
  249. next := ""
  250. if limit > 0 && len(lockList) == limit {
  251. next = strconv.Itoa(cursor + 1)
  252. }
  253. lockOursListAPI := make([]*api.LFSLock, 0, len(lockList))
  254. lockTheirsListAPI := make([]*api.LFSLock, 0, len(lockList))
  255. for _, l := range lockList {
  256. if l.Owner.ID == ctx.User.ID {
  257. lockOursListAPI = append(lockOursListAPI, convert.ToLFSLock(l))
  258. } else {
  259. lockTheirsListAPI = append(lockTheirsListAPI, convert.ToLFSLock(l))
  260. }
  261. }
  262. ctx.JSON(http.StatusOK, api.LFSLockListVerify{
  263. Ours: lockOursListAPI,
  264. Theirs: lockTheirsListAPI,
  265. Next: next,
  266. })
  267. }
  268. // UnLockHandler delete locks
  269. func UnLockHandler(ctx *context.Context) {
  270. if !checkIsValidRequest(ctx) {
  271. // Status is written in checkIsValidRequest
  272. return
  273. }
  274. ctx.Resp.Header().Set("Content-Type", metaMediaType)
  275. userName := ctx.Params("username")
  276. repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
  277. authorization := ctx.Req.Header.Get("Authorization")
  278. repository, err := models.GetRepositoryByOwnerAndName(userName, repoName)
  279. if err != nil {
  280. log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
  281. writeStatus(ctx, 404)
  282. return
  283. }
  284. repository.MustOwner()
  285. authenticated := authenticate(ctx, repository, authorization, true)
  286. if !authenticated {
  287. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
  288. ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
  289. Message: "You must have push access to delete locks",
  290. })
  291. return
  292. }
  293. var req api.LFSLockDeleteRequest
  294. bodyReader := ctx.Req.Body
  295. defer bodyReader.Close()
  296. json := jsoniter.ConfigCompatibleWithStandardLibrary
  297. dec := json.NewDecoder(bodyReader)
  298. if err := dec.Decode(&req); err != nil {
  299. log.Warn("Failed to decode lock request as json. Error: %v", err)
  300. writeStatus(ctx, 400)
  301. return
  302. }
  303. lock, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, req.Force)
  304. if err != nil {
  305. if models.IsErrLFSUnauthorizedAction(err) {
  306. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
  307. ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
  308. Message: "You must have push access to delete locks : " + err.Error(),
  309. })
  310. return
  311. }
  312. log.Error("Unable to DeleteLFSLockByID[%d] by user %-v with force %t: Error: %v", ctx.ParamsInt64("lid"), ctx.User, req.Force, err)
  313. ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
  314. Message: "unable to delete lock : Internal Server Error",
  315. })
  316. return
  317. }
  318. ctx.JSON(http.StatusOK, api.LFSLockResponse{Lock: convert.ToLFSLock(lock)})
  319. }