1package web
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "io/fs"
9 "net/http"
10 "net/url"
11 "path"
12 "path/filepath"
13 "strconv"
14 "strings"
15
16 "github.com/charmbracelet/log"
17 "github.com/charmbracelet/soft-serve/pkg/access"
18 "github.com/charmbracelet/soft-serve/pkg/backend"
19 "github.com/charmbracelet/soft-serve/pkg/config"
20 "github.com/charmbracelet/soft-serve/pkg/db"
21 "github.com/charmbracelet/soft-serve/pkg/db/models"
22 "github.com/charmbracelet/soft-serve/pkg/lfs"
23 "github.com/charmbracelet/soft-serve/pkg/proto"
24 "github.com/charmbracelet/soft-serve/pkg/storage"
25 "github.com/charmbracelet/soft-serve/pkg/store"
26 "github.com/gorilla/mux"
27)
28
29// serviceLfsBatch handles a Git LFS batch requests.
30// https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md
31// TODO: support refname
32// POST: /<repo>.git/info/lfs/objects/batch
33func serviceLfsBatch(w http.ResponseWriter, r *http.Request) {
34 ctx := r.Context()
35 logger := log.FromContext(ctx).WithPrefix("http.lfs")
36
37 if !isLfs(r) {
38 logger.Errorf("invalid content type: %s", r.Header.Get("Content-Type"))
39 renderNotAcceptable(w)
40 return
41 }
42
43 var batchRequest lfs.BatchRequest
44 defer r.Body.Close() // nolint: errcheck
45 if err := json.NewDecoder(r.Body).Decode(&batchRequest); err != nil {
46 logger.Errorf("error decoding json: %s", err)
47 renderJSON(w, http.StatusUnprocessableEntity, lfs.ErrorResponse{
48 Message: "validation error in request: " + err.Error(),
49 })
50 return
51 }
52
53 // We only accept basic transfers for now
54 // Default to basic if no transfer is specified
55 if len(batchRequest.Transfers) > 0 {
56 var isBasic bool
57 for _, t := range batchRequest.Transfers {
58 if t == lfs.TransferBasic {
59 isBasic = true
60 break
61 }
62 }
63
64 if !isBasic {
65 renderJSON(w, http.StatusUnprocessableEntity, lfs.ErrorResponse{
66 Message: "unsupported transfer",
67 })
68 return
69 }
70 }
71
72 if len(batchRequest.Objects) == 0 {
73 renderJSON(w, http.StatusUnprocessableEntity, lfs.ErrorResponse{
74 Message: "no objects found",
75 })
76 return
77 }
78
79 name := mux.Vars(r)["repo"]
80 repo := proto.RepositoryFromContext(ctx)
81 if repo == nil {
82 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
83 Message: "repository not found",
84 })
85 return
86 }
87
88 cfg := config.FromContext(ctx)
89 dbx := db.FromContext(ctx)
90 datastore := store.FromContext(ctx)
91 // TODO: support S3 storage
92 repoID := strconv.FormatInt(repo.ID(), 10)
93 strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
94
95 baseHref := fmt.Sprintf("%s/%s/info/lfs/objects/basic", cfg.HTTP.PublicURL, name+".git")
96
97 var batchResponse lfs.BatchResponse
98 batchResponse.Transfer = lfs.TransferBasic
99 batchResponse.HashAlgo = lfs.HashAlgorithmSHA256
100
101 objects := make([]*lfs.ObjectResponse, 0, len(batchRequest.Objects))
102 // XXX: We don't support objects TTL for now, probably implement that with
103 // S3 using object "expires_at" & "expires_in"
104 switch batchRequest.Operation {
105 case lfs.OperationDownload:
106 for _, o := range batchRequest.Objects {
107 exist, err := strg.Exists(path.Join("objects", o.RelativePath()))
108 if err != nil && !errors.Is(err, fs.ErrNotExist) {
109 logger.Error("error getting object stat", "oid", o.Oid, "repo", name, "err", err)
110 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
111 Message: "internal server error",
112 })
113 return
114 }
115
116 obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), o.Oid)
117 if err != nil && !errors.Is(err, db.ErrRecordNotFound) {
118 logger.Error("error getting object from database", "oid", o.Oid, "repo", name, "err", err)
119 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
120 Message: "internal server error",
121 })
122 return
123 }
124
125 if !exist {
126 objects = append(objects, &lfs.ObjectResponse{
127 Pointer: o,
128 Error: &lfs.ObjectError{
129 Code: http.StatusNotFound,
130 Message: "object not found",
131 },
132 })
133 } else if obj.Size != o.Size {
134 objects = append(objects, &lfs.ObjectResponse{
135 Pointer: o,
136 Error: &lfs.ObjectError{
137 Code: http.StatusUnprocessableEntity,
138 Message: "size mismatch",
139 },
140 })
141 } else if o.IsValid() {
142 download := &lfs.Link{
143 Href: fmt.Sprintf("%s/%s", baseHref, o.Oid),
144 }
145 if auth := r.Header.Get("Authorization"); auth != "" {
146 download.Header = map[string]string{
147 "Authorization": auth,
148 }
149 }
150
151 objects = append(objects, &lfs.ObjectResponse{
152 Pointer: o,
153 Actions: map[string]*lfs.Link{
154 lfs.ActionDownload: download,
155 },
156 })
157
158 // If the object doesn't exist in the database, create it
159 if exist && obj.ID == 0 {
160 if err := datastore.CreateLFSObject(ctx, dbx, repo.ID(), o.Oid, o.Size); err != nil {
161 logger.Error("error creating object in datastore", "oid", o.Oid, "repo", name, "err", err)
162 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
163 Message: "internal server error",
164 })
165 return
166 }
167 }
168 } else {
169 logger.Error("invalid object", "oid", o.Oid, "repo", name)
170 objects = append(objects, &lfs.ObjectResponse{
171 Pointer: o,
172 Error: &lfs.ObjectError{
173 Code: http.StatusUnprocessableEntity,
174 Message: "invalid object",
175 },
176 })
177 }
178 }
179 case lfs.OperationUpload:
180 // Check authorization
181 accessLevel := access.FromContext(ctx)
182 if accessLevel < access.ReadWriteAccess {
183 askCredentials(w, r)
184 renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{
185 Message: "write access required",
186 })
187 return
188 }
189
190 // Object upload logic happens in the "basic" API route
191 for _, o := range batchRequest.Objects {
192 if !o.IsValid() {
193 objects = append(objects, &lfs.ObjectResponse{
194 Pointer: o,
195 Error: &lfs.ObjectError{
196 Code: http.StatusUnprocessableEntity,
197 Message: "invalid object",
198 },
199 })
200 } else {
201 upload := &lfs.Link{
202 Href: fmt.Sprintf("%s/%s", baseHref, o.Oid),
203 Header: map[string]string{
204 // NOTE: git-lfs v2.5.0 sets the Content-Type based on the uploaded file.
205 // This ensures that the client always uses the designated value for the header.
206 "Content-Type": "application/octet-stream",
207 },
208 }
209 verify := &lfs.Link{
210 Href: fmt.Sprintf("%s/verify", baseHref),
211 }
212 if auth := r.Header.Get("Authorization"); auth != "" {
213 upload.Header["Authorization"] = auth
214 verify.Header = map[string]string{
215 "Authorization": auth,
216 }
217 }
218
219 objects = append(objects, &lfs.ObjectResponse{
220 Pointer: o,
221 Actions: map[string]*lfs.Link{
222 lfs.ActionUpload: upload,
223 // Verify uploaded objects
224 // https://github.com/git-lfs/git-lfs/blob/main/docs/api/basic-transfers.md#verification
225 lfs.ActionVerify: verify,
226 },
227 })
228 }
229 }
230 default:
231 renderJSON(w, http.StatusUnprocessableEntity, lfs.ErrorResponse{
232 Message: "unsupported operation",
233 })
234 return
235 }
236
237 batchResponse.Objects = objects
238 renderJSON(w, http.StatusOK, batchResponse)
239}
240
241// serviceLfsBasic implements Git LFS basic transfer API
242// https://github.com/git-lfs/git-lfs/blob/main/docs/api/basic-transfers.md
243func serviceLfsBasic(w http.ResponseWriter, r *http.Request) {
244 switch r.Method {
245 case http.MethodGet:
246 serviceLfsBasicDownload(w, r)
247 case http.MethodPut:
248 serviceLfsBasicUpload(w, r)
249 }
250}
251
252// GET: /<repo>.git/info/lfs/objects/basic/<oid>
253func serviceLfsBasicDownload(w http.ResponseWriter, r *http.Request) {
254 ctx := r.Context()
255 oid := mux.Vars(r)["oid"]
256 repo := proto.RepositoryFromContext(ctx)
257 cfg := config.FromContext(ctx)
258 logger := log.FromContext(ctx).WithPrefix("http.lfs-basic")
259 datastore := store.FromContext(ctx)
260 dbx := db.FromContext(ctx)
261 repoID := strconv.FormatInt(repo.ID(), 10)
262 strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
263
264 obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), oid)
265 if err != nil && !errors.Is(err, db.ErrRecordNotFound) {
266 logger.Error("error getting object from database", "oid", oid, "repo", repo.Name(), "err", err)
267 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
268 Message: "internal server error",
269 })
270 return
271 }
272
273 pointer := lfs.Pointer{Oid: oid}
274 f, err := strg.Open(path.Join("objects", pointer.RelativePath()))
275 if err != nil {
276 logger.Error("error opening object", "oid", oid, "err", err)
277 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
278 Message: "object not found",
279 })
280 return
281 }
282
283 w.Header().Set("Content-Type", "application/octet-stream")
284 w.Header().Set("Content-Length", strconv.FormatInt(obj.Size, 10))
285 defer f.Close() // nolint: errcheck
286 if _, err := io.Copy(w, f); err != nil {
287 logger.Error("error copying object to response", "oid", oid, "err", err)
288 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
289 Message: "internal server error",
290 })
291 return
292 }
293}
294
295// PUT: /<repo>.git/info/lfs/objects/basic/<oid>
296func serviceLfsBasicUpload(w http.ResponseWriter, r *http.Request) {
297 if !isBinary(r) {
298 renderJSON(w, http.StatusUnsupportedMediaType, lfs.ErrorResponse{
299 Message: "invalid content type",
300 })
301 return
302 }
303
304 ctx := r.Context()
305 oid := mux.Vars(r)["oid"]
306 cfg := config.FromContext(ctx)
307 be := backend.FromContext(ctx)
308 dbx := db.FromContext(ctx)
309 datastore := store.FromContext(ctx)
310 logger := log.FromContext(ctx).WithPrefix("http.lfs-basic")
311 repo := proto.RepositoryFromContext(ctx)
312 repoID := strconv.FormatInt(repo.ID(), 10)
313 strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
314 name := mux.Vars(r)["repo"]
315
316 defer r.Body.Close() // nolint: errcheck
317 repo, err := be.Repository(ctx, name)
318 if err != nil {
319 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
320 Message: "repository not found",
321 })
322 return
323 }
324
325 // NOTE: Git LFS client will retry uploading the same object if there was a
326 // partial error, so we need to skip existing objects.
327 if _, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), oid); err == nil {
328 // Object exists, skip request
329 io.Copy(io.Discard, r.Body) // nolint: errcheck
330 renderStatus(http.StatusOK)(w, nil)
331 return
332 } else if !errors.Is(err, db.ErrRecordNotFound) {
333 logger.Error("error getting object", "oid", oid, "err", err)
334 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
335 Message: "internal server error",
336 })
337 return
338 }
339
340 pointer := lfs.Pointer{Oid: oid}
341 if _, err := strg.Put(path.Join("objects", pointer.RelativePath()), r.Body); err != nil {
342 logger.Error("error writing object", "oid", oid, "err", err)
343 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
344 Message: "internal server error",
345 })
346 return
347 }
348
349 size, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
350 if err != nil {
351 logger.Error("error parsing content length", "err", err)
352 renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
353 Message: "invalid content length",
354 })
355 return
356 }
357
358 if err := datastore.CreateLFSObject(ctx, dbx, repo.ID(), oid, size); err != nil {
359 logger.Error("error creating object", "oid", oid, "err", err)
360 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
361 Message: "internal server error",
362 })
363 return
364 }
365
366 renderStatus(http.StatusOK)(w, nil)
367}
368
369// POST: /<repo>.git/info/lfs/objects/basic/verify
370func serviceLfsBasicVerify(w http.ResponseWriter, r *http.Request) {
371 if !isLfs(r) {
372 renderNotAcceptable(w)
373 return
374 }
375
376 var pointer lfs.Pointer
377 ctx := r.Context()
378 logger := log.FromContext(ctx).WithPrefix("http.lfs-basic")
379 repo := proto.RepositoryFromContext(ctx)
380 if repo == nil {
381 logger.Error("error getting repository from context")
382 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
383 Message: "repository not found",
384 })
385 return
386 }
387
388 defer r.Body.Close() // nolint: errcheck
389 if err := json.NewDecoder(r.Body).Decode(&pointer); err != nil {
390 logger.Error("error decoding json", "err", err)
391 renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
392 Message: "invalid request: " + err.Error(),
393 })
394 return
395 }
396
397 cfg := config.FromContext(ctx)
398 dbx := db.FromContext(ctx)
399 datastore := store.FromContext(ctx)
400 repoID := strconv.FormatInt(repo.ID(), 10)
401 strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
402 if stat, err := strg.Stat(path.Join("objects", pointer.RelativePath())); err == nil {
403 // Verify object is in the database.
404 obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), pointer.Oid)
405 if err != nil {
406 if errors.Is(err, db.ErrRecordNotFound) {
407 logger.Error("object not found", "oid", pointer.Oid)
408 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
409 Message: "object not found",
410 })
411 return
412 }
413 logger.Error("error getting object", "oid", pointer.Oid, "err", err)
414 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
415 Message: "internal server error",
416 })
417 return
418 }
419
420 if obj.Size != pointer.Size {
421 renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
422 Message: "object size mismatch",
423 })
424 return
425 }
426
427 if pointer.IsValid() && stat.Size() == pointer.Size {
428 renderStatus(http.StatusOK)(w, nil)
429 return
430 }
431 } else if errors.Is(err, fs.ErrNotExist) {
432 logger.Error("file not found", "oid", pointer.Oid)
433 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
434 Message: "object not found",
435 })
436 return
437 } else {
438 logger.Error("error getting object", "oid", pointer.Oid, "err", err)
439 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
440 Message: "internal server error",
441 })
442 return
443 }
444}
445
446func serviceLfsLocks(w http.ResponseWriter, r *http.Request) {
447 switch r.Method {
448 case http.MethodGet:
449 serviceLfsLocksGet(w, r)
450 case http.MethodPost:
451 serviceLfsLocksCreate(w, r)
452 default:
453 renderMethodNotAllowed(w, r)
454 }
455}
456
457// POST: /<repo>.git/info/lfs/objects/locks
458func serviceLfsLocksCreate(w http.ResponseWriter, r *http.Request) {
459 if !isLfs(r) {
460 renderNotAcceptable(w)
461 return
462 }
463
464 ctx := r.Context()
465 logger := log.FromContext(ctx).WithPrefix("http.lfs-locks")
466
467 var req lfs.LockCreateRequest
468 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
469 logger.Error("error decoding json", "err", err)
470 renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
471 Message: "invalid request: " + err.Error(),
472 })
473 return
474 }
475
476 repo := proto.RepositoryFromContext(ctx)
477 if repo == nil {
478 logger.Error("error getting repository from context")
479 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
480 Message: "repository not found",
481 })
482 return
483 }
484
485 user := proto.UserFromContext(ctx)
486 if user == nil {
487 logger.Error("error getting user from context")
488 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
489 Message: "user not found",
490 })
491 return
492 }
493
494 dbx := db.FromContext(ctx)
495 datastore := store.FromContext(ctx)
496 if err := datastore.CreateLFSLockForUser(ctx, dbx, repo.ID(), user.ID(), req.Path, req.Ref.Name); err != nil {
497 err = db.WrapError(err)
498 if errors.Is(err, db.ErrDuplicateKey) {
499 errResp := lfs.LockResponse{
500 ErrorResponse: lfs.ErrorResponse{
501 Message: "lock already exists",
502 },
503 }
504 lock, err := datastore.GetLFSLockForUserPath(ctx, dbx, repo.ID(), user.ID(), req.Path)
505 if err == nil {
506 errResp.Lock = lfs.Lock{
507 ID: strconv.FormatInt(lock.ID, 10),
508 Path: lock.Path,
509 LockedAt: lock.CreatedAt,
510 }
511 lockOwner := lfs.Owner{
512 Name: user.Username(),
513 }
514 if lock.UserID != user.ID() {
515 owner, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
516 if err != nil {
517 logger.Error("error getting lock owner", "err", err)
518 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
519 Message: "internal server error",
520 })
521 return
522 }
523 handle, err := datastore.GetHandleByUserID(ctx, dbx, owner.ID)
524 if err != nil {
525 logger.Error("error getting lock owner handle", "err", err)
526 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
527 Message: "internal server error",
528 })
529 return
530 }
531 lockOwner.Name = handle.Handle
532 }
533 errResp.Lock.Owner = lockOwner
534 }
535 renderJSON(w, http.StatusConflict, errResp)
536 return
537 }
538 logger.Error("error creating lock", "err", err)
539 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
540 Message: "internal server error",
541 })
542 return
543 }
544
545 lock, err := datastore.GetLFSLockForUserPath(ctx, dbx, repo.ID(), user.ID(), req.Path)
546 if err != nil {
547 logger.Error("error getting lock", "err", err)
548 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
549 Message: "internal server error",
550 })
551 return
552 }
553
554 renderJSON(w, http.StatusCreated, lfs.LockResponse{
555 Lock: lfs.Lock{
556 ID: strconv.FormatInt(lock.ID, 10),
557 Path: lock.Path,
558 LockedAt: lock.CreatedAt,
559 Owner: lfs.Owner{
560 Name: user.Username(),
561 },
562 },
563 })
564}
565
566// GET: /<repo>.git/info/lfs/objects/locks
567func serviceLfsLocksGet(w http.ResponseWriter, r *http.Request) {
568 accept := r.Header.Get("Accept")
569 if !strings.HasPrefix(accept, lfs.MediaType) {
570 renderNotAcceptable(w)
571 return
572 }
573
574 parseLocksQuery := func(values url.Values) (path string, id int64, cursor int, limit int, refspec string) {
575 path = values.Get("path")
576 idStr := values.Get("id")
577 if idStr != "" {
578 id, _ = strconv.ParseInt(idStr, 10, 64)
579 }
580 cursorStr := values.Get("cursor")
581 if cursorStr != "" {
582 cursor, _ = strconv.Atoi(cursorStr)
583 }
584 limitStr := values.Get("limit")
585 if limitStr != "" {
586 limit, _ = strconv.Atoi(limitStr)
587 }
588 refspec = values.Get("refspec")
589 return
590 }
591
592 ctx := r.Context()
593 // TODO: respect refspec
594 path, id, cursor, limit, _ := parseLocksQuery(r.URL.Query())
595 if limit > 100 {
596 limit = 100
597 } else if limit <= 0 {
598 limit = lfs.DefaultLocksLimit
599 }
600
601 // cursor is the page number
602 if cursor <= 0 {
603 cursor = 1
604 }
605
606 logger := log.FromContext(ctx).WithPrefix("http.lfs-locks")
607 dbx := db.FromContext(ctx)
608 datastore := store.FromContext(ctx)
609 repo := proto.RepositoryFromContext(ctx)
610 if repo == nil {
611 logger.Error("error getting repository from context")
612 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
613 Message: "repository not found",
614 })
615 return
616 }
617
618 if id > 0 {
619 lock, err := datastore.GetLFSLockByID(ctx, dbx, id)
620 if err != nil {
621 if errors.Is(err, db.ErrRecordNotFound) {
622 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
623 Message: "lock not found",
624 })
625 return
626 }
627 logger.Error("error getting lock", "err", err)
628 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
629 Message: "internal server error",
630 })
631 return
632 }
633
634 owner, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
635 if err != nil {
636 logger.Error("error getting lock owner", "err", err)
637 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
638 Message: "internal server error",
639 })
640 return
641 }
642
643 handle, err := datastore.GetHandleByUserID(ctx, dbx, owner.ID)
644 if err != nil {
645 logger.Error("error getting lock owner handle", "err", err)
646 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
647 Message: "internal server error",
648 })
649 return
650 }
651
652 renderJSON(w, http.StatusOK, lfs.LockListResponse{
653 Locks: []lfs.Lock{
654 {
655 ID: strconv.FormatInt(lock.ID, 10),
656 Path: lock.Path,
657 LockedAt: lock.CreatedAt,
658 Owner: lfs.Owner{
659 Name: handle.Handle,
660 },
661 },
662 },
663 })
664 return
665 } else if path != "" {
666 lock, err := datastore.GetLFSLockForPath(ctx, dbx, repo.ID(), path)
667 if err != nil {
668 if errors.Is(err, db.ErrRecordNotFound) {
669 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
670 Message: "lock not found",
671 })
672 return
673 }
674 logger.Error("error getting lock", "err", err)
675 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
676 Message: "internal server error",
677 })
678 return
679 }
680
681 owner, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
682 if err != nil {
683 logger.Error("error getting lock owner", "err", err)
684 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
685 Message: "internal server error",
686 })
687 return
688 }
689
690 handle, err := datastore.GetHandleByUserID(ctx, dbx, owner.ID)
691 if err != nil {
692 logger.Error("error getting lock owner handle", "err", err)
693 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
694 Message: "internal server error",
695 })
696 return
697 }
698
699 renderJSON(w, http.StatusOK, lfs.LockListResponse{
700 Locks: []lfs.Lock{
701 {
702 ID: strconv.FormatInt(lock.ID, 10),
703 Path: lock.Path,
704 LockedAt: lock.CreatedAt,
705 Owner: lfs.Owner{
706 Name: handle.Handle,
707 },
708 },
709 },
710 })
711 return
712 }
713
714 locks, err := datastore.GetLFSLocks(ctx, dbx, repo.ID(), cursor, limit)
715 if err != nil {
716 logger.Error("error getting locks", "err", err)
717 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
718 Message: "internal server error",
719 })
720 return
721 }
722
723 lockList := make([]lfs.Lock, len(locks))
724 users := map[int64]userModel{}
725 for i, lock := range locks {
726 owner, ok := users[lock.UserID]
727 if !ok {
728 user, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
729 if err != nil {
730 logger.Error("error getting lock owner", "err", err)
731 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
732 Message: "internal server error",
733 })
734 return
735 }
736 handle, err := datastore.GetHandleByUserID(ctx, dbx, user.ID)
737 if err != nil {
738 logger.Error("error getting lock owner handle", "err", err)
739 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
740 Message: "internal server error",
741 })
742 return
743 }
744 users[lock.UserID] = userModel{User: user, Handle: handle}
745 }
746
747 lockList[i] = lfs.Lock{
748 ID: strconv.FormatInt(lock.ID, 10),
749 Path: lock.Path,
750 LockedAt: lock.CreatedAt,
751 Owner: lfs.Owner{
752 Name: owner.Handle.Handle,
753 },
754 }
755 }
756
757 resp := lfs.LockListResponse{
758 Locks: lockList,
759 }
760 if len(locks) == limit {
761 resp.NextCursor = strconv.Itoa(cursor + 1)
762 }
763
764 renderJSON(w, http.StatusOK, resp)
765}
766
767type userModel struct {
768 models.User
769 models.Handle
770}
771
772// POST: /<repo>.git/info/lfs/objects/locks/verify
773func serviceLfsLocksVerify(w http.ResponseWriter, r *http.Request) {
774 if !isLfs(r) {
775 renderNotAcceptable(w)
776 return
777 }
778
779 ctx := r.Context()
780 logger := log.FromContext(ctx).WithPrefix("http.lfs-locks")
781 repo := proto.RepositoryFromContext(ctx)
782 if repo == nil {
783 logger.Error("error getting repository from context")
784 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
785 Message: "repository not found",
786 })
787 return
788 }
789
790 var req lfs.LockVerifyRequest
791 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
792 logger.Error("error decoding request", "err", err)
793 renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
794 Message: "invalid request: " + err.Error(),
795 })
796 return
797 }
798
799 // TODO: refspec
800 cursor, _ := strconv.Atoi(req.Cursor)
801 if cursor <= 0 {
802 cursor = 1
803 }
804
805 limit := req.Limit
806 if limit > 100 {
807 limit = 100
808 } else if limit <= 0 {
809 limit = lfs.DefaultLocksLimit
810 }
811
812 dbx := db.FromContext(ctx)
813 datastore := store.FromContext(ctx)
814 user := proto.UserFromContext(ctx)
815 ours := make([]lfs.Lock, 0)
816 theirs := make([]lfs.Lock, 0)
817
818 var resp lfs.LockVerifyResponse
819 locks, err := datastore.GetLFSLocks(ctx, dbx, repo.ID(), cursor, limit)
820 if err != nil {
821 logger.Error("error getting locks", "err", err)
822 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
823 Message: "internal server error",
824 })
825 return
826 }
827
828 users := map[int64]userModel{}
829 for _, lock := range locks {
830 owner, ok := users[lock.UserID]
831 if !ok {
832 user, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
833 if err != nil {
834 logger.Error("error getting lock owner", "err", err)
835 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
836 Message: "internal server error",
837 })
838 return
839 }
840 handle, err := datastore.GetHandleByUserID(ctx, dbx, user.ID)
841 if err != nil {
842 logger.Error("error getting lock owner handle", "err", err)
843 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
844 Message: "internal server error",
845 })
846 return
847 }
848 users[lock.UserID] = userModel{User: user, Handle: handle}
849 }
850
851 l := lfs.Lock{
852 ID: strconv.FormatInt(lock.ID, 10),
853 Path: lock.Path,
854 LockedAt: lock.CreatedAt,
855 Owner: lfs.Owner{
856 Name: owner.Handle.Handle,
857 },
858 }
859
860 if user != nil && user.ID() == lock.UserID {
861 ours = append(ours, l)
862 } else {
863 theirs = append(theirs, l)
864 }
865 }
866
867 resp.Ours = ours
868 resp.Theirs = theirs
869
870 if len(locks) == limit {
871 resp.NextCursor = strconv.Itoa(cursor + 1)
872 }
873
874 renderJSON(w, http.StatusOK, resp)
875}
876
877// POST: /<repo>.git/info/lfs/objects/locks/:lockID/unlock
878func serviceLfsLocksDelete(w http.ResponseWriter, r *http.Request) {
879 if !isLfs(r) {
880 renderNotAcceptable(w)
881 return
882 }
883
884 ctx := r.Context()
885 logger := log.FromContext(ctx).WithPrefix("http.lfs-locks")
886 lockIDStr := mux.Vars(r)["lock_id"]
887 if lockIDStr == "" {
888 logger.Error("error getting lock id")
889 renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
890 Message: "invalid request",
891 })
892 return
893 }
894
895 lockID, err := strconv.ParseInt(lockIDStr, 10, 64)
896 if err != nil {
897 logger.Error("error parsing lock id", "err", err)
898 renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
899 Message: "invalid request",
900 })
901 return
902 }
903
904 var req lfs.LockDeleteRequest
905 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
906 logger.Error("error decoding request", "err", err)
907 renderJSON(w, http.StatusBadRequest, lfs.ErrorResponse{
908 Message: "invalid request: " + err.Error(),
909 })
910 return
911 }
912
913 dbx := db.FromContext(ctx)
914 datastore := store.FromContext(ctx)
915 repo := proto.RepositoryFromContext(ctx)
916 if repo == nil {
917 logger.Error("error getting repository from context")
918 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
919 Message: "repository not found",
920 })
921 return
922 }
923
924 // The lock being deleted
925 lock, err := datastore.GetLFSLockByID(ctx, dbx, lockID)
926 if err != nil {
927 logger.Error("error getting lock", "err", err)
928 renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
929 Message: "lock not found",
930 })
931 return
932 }
933
934 owner, err := datastore.GetUserByID(ctx, dbx, lock.UserID)
935 if err != nil {
936 logger.Error("error getting lock owner", "err", err)
937 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
938 Message: "internal server error",
939 })
940 return
941 }
942
943 handle, err := datastore.GetHandleByUserID(ctx, dbx, owner.ID)
944 if err != nil {
945 logger.Error("error getting lock owner handle", "err", err)
946 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
947 Message: "internal server error",
948 })
949 return
950 }
951
952 // Delete another user's lock
953 l := lfs.Lock{
954 ID: strconv.FormatInt(lock.ID, 10),
955 Path: lock.Path,
956 LockedAt: lock.CreatedAt,
957 Owner: lfs.Owner{
958 Name: handle.Handle,
959 },
960 }
961 if req.Force {
962 if err := datastore.DeleteLFSLock(ctx, dbx, repo.ID(), lockID); err != nil {
963 logger.Error("error deleting lock", "err", err)
964 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
965 Message: "internal server error",
966 })
967 return
968 }
969
970 renderJSON(w, http.StatusOK, l)
971 return
972 }
973
974 // Delete our own lock
975 user := proto.UserFromContext(ctx)
976 if user == nil {
977 logger.Error("error getting user from context")
978 renderJSON(w, http.StatusUnauthorized, lfs.ErrorResponse{
979 Message: "unauthorized",
980 })
981 return
982 }
983
984 if owner.ID != user.ID() {
985 logger.Error("error deleting another user's lock")
986 renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{
987 Message: "lock belongs to another user",
988 })
989 return
990 }
991
992 if err := datastore.DeleteLFSLock(ctx, dbx, repo.ID(), lockID); err != nil {
993 logger.Error("error deleting lock", "err", err)
994 renderJSON(w, http.StatusInternalServerError, lfs.ErrorResponse{
995 Message: "internal server error",
996 })
997 return
998 }
999
1000 renderJSON(w, http.StatusOK, lfs.LockResponse{Lock: l})
1001}
1002
1003// renderJSON renders a JSON response with the given status code and value. It
1004// also sets the Content-Type header to the JSON LFS media type (application/vnd.git-lfs+json).
1005func renderJSON(w http.ResponseWriter, statusCode int, v interface{}) {
1006 hdrLfs(w)
1007 w.WriteHeader(statusCode)
1008 if err := json.NewEncoder(w).Encode(v); err != nil {
1009 log.Error("error encoding json", "err", err)
1010 }
1011}
1012
1013func renderNotAcceptable(w http.ResponseWriter) {
1014 renderStatus(http.StatusNotAcceptable)(w, nil)
1015}
1016
1017func isLfs(r *http.Request) bool {
1018 contentType := r.Header.Get("Content-Type")
1019 accept := r.Header.Get("Accept")
1020 return strings.HasPrefix(contentType, lfs.MediaType) && strings.HasPrefix(accept, lfs.MediaType)
1021}
1022
1023func isBinary(r *http.Request) bool {
1024 contentType := r.Header.Get("Content-Type")
1025 return strings.HasPrefix(contentType, "application/octet-stream")
1026}
1027
1028func hdrLfs(w http.ResponseWriter) {
1029 w.Header().Set("Content-Type", lfs.MediaType)
1030 w.Header().Set("Accept", lfs.MediaType)
1031}