| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 | // Copyright 2020 The Gogs Authors. All rights reserved.// Use of this source code is governed by a MIT-style// license that can be found in the LICENSE file.package lfsimport (	"fmt"	"net/http"	jsoniter "github.com/json-iterator/go"	"gopkg.in/macaron.v1"	log "unknwon.dev/clog/v2"	"gogs.io/gogs/internal/conf"	"gogs.io/gogs/internal/database"	"gogs.io/gogs/internal/lfsutil"	"gogs.io/gogs/internal/strutil")// POST /{owner}/{repo}.git/info/lfs/object/batchfunc serveBatch(store Store) macaron.Handler {	return func(c *macaron.Context, owner *database.User, repo *database.Repository) {		var request batchRequest		defer func() { _ = c.Req.Request.Body.Close() }()		err := jsoniter.NewDecoder(c.Req.Request.Body).Decode(&request)		if err != nil {			responseJSON(c.Resp, http.StatusBadRequest, responseError{				Message: strutil.ToUpperFirst(err.Error()),			})			return		}		// NOTE: We only support basic transfer as of now.		transfer := transferBasic		// Example: https://try.gogs.io/gogs/gogs.git/info/lfs/object/basic		baseHref := fmt.Sprintf("%s%s/%s.git/info/lfs/objects/basic", conf.Server.ExternalURL, owner.Name, repo.Name)		objects := make([]batchObject, 0, len(request.Objects))		switch request.Operation {		case basicOperationUpload:			for _, obj := range request.Objects {				var actions batchActions				if lfsutil.ValidOID(obj.Oid) {					actions = batchActions{						Upload: &batchAction{							Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),							Header: map[string]string{								// NOTE: git-lfs v2.5.0 sets the Content-Type based on the uploaded file.								// This ensures that the client always uses the designated value for the header.								"Content-Type": "application/octet-stream",							},						},						Verify: &batchAction{							Href: fmt.Sprintf("%s/verify", baseHref),						},					}				} else {					actions = batchActions{						Error: &batchError{							Code:    http.StatusUnprocessableEntity,							Message: "Object has invalid oid",						},					}				}				objects = append(objects, batchObject{					Oid:     obj.Oid,					Size:    obj.Size,					Actions: actions,				})			}		case basicOperationDownload:			oids := make([]lfsutil.OID, 0, len(request.Objects))			for _, obj := range request.Objects {				oids = append(oids, obj.Oid)			}			stored, err := store.GetLFSObjectsByOIDs(c.Req.Context(), repo.ID, oids...)			if err != nil {				internalServerError(c.Resp)				log.Error("Failed to get objects [repo_id: %d, oids: %v]: %v", repo.ID, oids, err)				return			}			storedSet := make(map[lfsutil.OID]*database.LFSObject, len(stored))			for _, obj := range stored {				storedSet[obj.OID] = obj			}			for _, obj := range request.Objects {				var actions batchActions				if stored := storedSet[obj.Oid]; stored != nil {					if stored.Size != obj.Size {						actions.Error = &batchError{							Code:    http.StatusUnprocessableEntity,							Message: "Object size mismatch",						}					} else {						actions.Download = &batchAction{							Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),						}					}				} else {					actions.Error = &batchError{						Code:    http.StatusNotFound,						Message: "Object does not exist",					}				}				objects = append(objects, batchObject{					Oid:     obj.Oid,					Size:    obj.Size,					Actions: actions,				})			}		default:			responseJSON(c.Resp, http.StatusBadRequest, responseError{				Message: "Operation not recognized",			})			return		}		responseJSON(c.Resp, http.StatusOK, batchResponse{			Transfer: transfer,			Objects:  objects,		})	}}// batchRequest defines the request payload for the batch endpoint.type batchRequest struct {	Operation string `json:"operation"`	Objects   []struct {		Oid  lfsutil.OID `json:"oid"`		Size int64       `json:"size"`	} `json:"objects"`}type batchError struct {	Code    int    `json:"code"`	Message string `json:"message"`}type batchAction struct {	Href   string            `json:"href"`	Header map[string]string `json:"header,omitempty"`}type batchActions struct {	Download *batchAction `json:"download,omitempty"`	Upload   *batchAction `json:"upload,omitempty"`	Verify   *batchAction `json:"verify,omitempty"`	Error    *batchError  `json:"error,omitempty"`}type batchObject struct {	Oid     lfsutil.OID  `json:"oid"`	Size    int64        `json:"size"`	Actions batchActions `json:"actions"`}// batchResponse defines the response payload for the batch endpoint.type batchResponse struct {	Transfer string        `json:"transfer"`	Objects  []batchObject `json:"objects"`}type responseError struct {	Message string `json:"message"`}const contentType = "application/vnd.git-lfs+json"func responseJSON(w http.ResponseWriter, status int, v any) {	w.Header().Set("Content-Type", contentType)	w.WriteHeader(status)	err := jsoniter.NewEncoder(w).Encode(v)	if err != nil {		log.Error("Failed to encode JSON: %v", err)		return	}}
 |