first-commit
This commit is contained in:
295
modules/lfstransfer/backend/backend.go
Normal file
295
modules/lfstransfer/backend/backend.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
|
||||
// Version is the git-lfs-transfer protocol version number.
|
||||
const Version = "1"
|
||||
|
||||
// Capabilities is a list of Git LFS capabilities supported by this package.
|
||||
var Capabilities = []string{
|
||||
"version=" + Version,
|
||||
"locking",
|
||||
}
|
||||
|
||||
var _ transfer.Backend = (*GiteaBackend)(nil)
|
||||
|
||||
// GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API
|
||||
type GiteaBackend struct {
|
||||
ctx context.Context
|
||||
server *url.URL
|
||||
op string
|
||||
authToken string
|
||||
internalAuth string
|
||||
logger transfer.Logger
|
||||
}
|
||||
|
||||
func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (transfer.Backend, error) {
|
||||
// runServ guarantees repo will be in form [owner]/[name].git
|
||||
server, err := url.Parse(setting.LocalURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server = server.JoinPath("api/internal/repo", repo, "info/lfs")
|
||||
return &GiteaBackend{ctx: ctx, server: server, op: op, authToken: token, internalAuth: "Bearer " + setting.InternalToken, logger: logger}, nil
|
||||
}
|
||||
|
||||
// Batch implements transfer.Backend
|
||||
func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args transfer.Args) ([]transfer.BatchItem, error) {
|
||||
reqBody := lfs.BatchRequest{Operation: g.op}
|
||||
if transfer, ok := args[argTransfer]; ok {
|
||||
reqBody.Transfers = []string{transfer}
|
||||
}
|
||||
if ref, ok := args[argRefname]; ok {
|
||||
reqBody.Ref = &lfs.Reference{Name: ref}
|
||||
}
|
||||
reqBody.Objects = make([]lfs.Pointer, len(pointers))
|
||||
for i := range pointers {
|
||||
reqBody.Objects[i].Oid = pointers[i].Oid
|
||||
reqBody.Objects[i].Size = pointers[i].Size
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequestLFS(g.ctx, g.server.JoinPath("objects/batch").String(), http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request error", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
||||
return nil, statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
g.logger.Log("http read error", err)
|
||||
return nil, err
|
||||
}
|
||||
var respBody lfs.BatchResponse
|
||||
err = json.Unmarshal(respBytes, &respBody)
|
||||
if err != nil {
|
||||
g.logger.Log("json umarshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// rebuild slice, we can't rely on order in resp being the same as req
|
||||
pointers = pointers[:0]
|
||||
opNum := opMap[g.op]
|
||||
for _, obj := range respBody.Objects {
|
||||
pointer := transfer.Pointer{Oid: obj.Pointer.Oid, Size: obj.Pointer.Size}
|
||||
item := transfer.BatchItem{Pointer: pointer, Args: map[string]string{}}
|
||||
switch opNum {
|
||||
case opDownload:
|
||||
if action, ok := obj.Actions[actionDownload]; ok {
|
||||
item.Present = true
|
||||
idMap := obj.Actions
|
||||
idMapBytes, err := json.Marshal(idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
|
||||
item.Args[argID] = idMapStr
|
||||
if authHeader, ok := action.Header[headerAuthorization]; ok {
|
||||
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
|
||||
item.Args[argToken] = authHeaderB64
|
||||
}
|
||||
if action.ExpiresAt != nil {
|
||||
item.Args[argExpiresAt] = action.ExpiresAt.String()
|
||||
}
|
||||
} else {
|
||||
// must be an error, but the SSH protocol can't propagate individual errors
|
||||
g.logger.Log("object not found", obj.Pointer.Oid, obj.Pointer.Size)
|
||||
item.Present = false
|
||||
}
|
||||
case opUpload:
|
||||
if action, ok := obj.Actions[actionUpload]; ok {
|
||||
item.Present = false
|
||||
idMap := obj.Actions
|
||||
idMapBytes, err := json.Marshal(idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
|
||||
item.Args[argID] = idMapStr
|
||||
if authHeader, ok := action.Header[headerAuthorization]; ok {
|
||||
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
|
||||
item.Args[argToken] = authHeaderB64
|
||||
}
|
||||
if action.ExpiresAt != nil {
|
||||
item.Args[argExpiresAt] = action.ExpiresAt.String()
|
||||
}
|
||||
} else {
|
||||
item.Present = true
|
||||
}
|
||||
}
|
||||
pointers = append(pointers, item)
|
||||
}
|
||||
return pointers, nil
|
||||
}
|
||||
|
||||
// Download implements transfer.Backend. The returned reader must be closed by the caller.
|
||||
func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) {
|
||||
idMapStr, exists := args[argID]
|
||||
if !exists {
|
||||
return nil, 0, ErrMissingID
|
||||
}
|
||||
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
||||
if err != nil {
|
||||
g.logger.Log("base64 decode error", err)
|
||||
return nil, 0, transfer.ErrCorruptData
|
||||
}
|
||||
idMap := map[string]*lfs.Link{}
|
||||
err = json.Unmarshal(idMapBytes, &idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json unmarshal error", err)
|
||||
return nil, 0, transfer.ErrCorruptData
|
||||
}
|
||||
action, exists := idMap[actionDownload]
|
||||
if !exists {
|
||||
g.logger.Log("argument id incorrect")
|
||||
return nil, 0, transfer.ErrCorruptData
|
||||
}
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeOctetStream,
|
||||
}
|
||||
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodGet, headers, nil)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get response: %w", err)
|
||||
}
|
||||
// no need to close the body here by "defer resp.Body.Close()", see below
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, 0, statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
|
||||
respSize, err := strconv.ParseInt(resp.Header.Get("X-Gitea-LFS-Content-Length"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to parse content length: %w", err)
|
||||
}
|
||||
// transfer.Backend will check io.Closer interface and close this Body reader
|
||||
return resp.Body, respSize, nil
|
||||
}
|
||||
|
||||
// Upload implements transfer.Backend.
|
||||
func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer.Args) error {
|
||||
idMapStr, exists := args[argID]
|
||||
if !exists {
|
||||
return ErrMissingID
|
||||
}
|
||||
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
||||
if err != nil {
|
||||
g.logger.Log("base64 decode error", err)
|
||||
return transfer.ErrCorruptData
|
||||
}
|
||||
idMap := map[string]*lfs.Link{}
|
||||
err = json.Unmarshal(idMapBytes, &idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json unmarshal error", err)
|
||||
return transfer.ErrCorruptData
|
||||
}
|
||||
action, exists := idMap[actionUpload]
|
||||
if !exists {
|
||||
g.logger.Log("argument id incorrect")
|
||||
return transfer.ErrCorruptData
|
||||
}
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerContentType: mimeOctetStream,
|
||||
headerContentLength: strconv.FormatInt(size, 10),
|
||||
}
|
||||
|
||||
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPut, headers, nil)
|
||||
req.Body(r)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify implements transfer.Backend.
|
||||
func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (transfer.Status, error) {
|
||||
reqBody := lfs.Pointer{Oid: oid, Size: size}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return transfer.NewStatus(transfer.StatusInternalServerError), err
|
||||
}
|
||||
idMapStr, exists := args[argID]
|
||||
if !exists {
|
||||
return transfer.NewStatus(transfer.StatusBadRequest, "missing argument: id"), ErrMissingID
|
||||
}
|
||||
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
||||
if err != nil {
|
||||
g.logger.Log("base64 decode error", err)
|
||||
return transfer.NewStatus(transfer.StatusBadRequest, "corrupt argument: id"), transfer.ErrCorruptData
|
||||
}
|
||||
idMap := map[string]*lfs.Link{}
|
||||
err = json.Unmarshal(idMapBytes, &idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json unmarshal error", err)
|
||||
return transfer.NewStatus(transfer.StatusBadRequest, "corrupt argument: id"), transfer.ErrCorruptData
|
||||
}
|
||||
action, exists := idMap[actionVerify]
|
||||
if !exists {
|
||||
// the server sent no verify action
|
||||
return transfer.SuccessStatus(), nil
|
||||
}
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return transfer.NewStatus(transfer.StatusInternalServerError), err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return transfer.NewStatus(uint32(resp.StatusCode), http.StatusText(resp.StatusCode)), statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
return transfer.SuccessStatus(), nil
|
||||
}
|
||||
|
||||
// LockBackend implements transfer.Backend.
|
||||
func (g *GiteaBackend) LockBackend(_ transfer.Args) transfer.LockBackend {
|
||||
return newGiteaLockBackend(g)
|
||||
}
|
294
modules/lfstransfer/backend/lock.go
Normal file
294
modules/lfstransfer/backend/lock.go
Normal file
@@ -0,0 +1,294 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
lfslock "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
|
||||
var _ transfer.LockBackend = &giteaLockBackend{}
|
||||
|
||||
type giteaLockBackend struct {
|
||||
ctx context.Context
|
||||
g *GiteaBackend
|
||||
server *url.URL
|
||||
authToken string
|
||||
internalAuth string
|
||||
logger transfer.Logger
|
||||
}
|
||||
|
||||
func newGiteaLockBackend(g *GiteaBackend) transfer.LockBackend {
|
||||
server := g.server.JoinPath("locks")
|
||||
return &giteaLockBackend{ctx: g.ctx, g: g, server: server, authToken: g.authToken, internalAuth: g.internalAuth, logger: g.logger}
|
||||
}
|
||||
|
||||
// Create implements transfer.LockBackend
|
||||
func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
|
||||
reqBody := lfslock.LFSLockRequest{Path: path}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequestLFS(g.ctx, g.server.String(), http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request error", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
g.logger.Log("http read error", err)
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
||||
return nil, statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
var respBody lfslock.LFSLockResponse
|
||||
err = json.Unmarshal(respBytes, &respBody)
|
||||
if err != nil {
|
||||
g.logger.Log("json umarshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if respBody.Lock == nil {
|
||||
g.logger.Log("api returned nil lock")
|
||||
return nil, errors.New("api returned nil lock")
|
||||
}
|
||||
respLock := respBody.Lock
|
||||
owner := userUnknown
|
||||
if respLock.Owner != nil {
|
||||
owner = respLock.Owner.Name
|
||||
}
|
||||
lock := newGiteaLock(g, respLock.ID, respLock.Path, respLock.LockedAt, owner)
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
// Unlock implements transfer.LockBackend
|
||||
func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
|
||||
reqBody := lfslock.LFSLockDeleteRequest{}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return err
|
||||
}
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequestLFS(g.ctx, g.server.JoinPath(lock.ID(), "unlock").String(), http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request error", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
||||
return statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
// no need to read response
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromPath implements transfer.LockBackend
|
||||
func (g *giteaLockBackend) FromPath(path string) (transfer.Lock, error) {
|
||||
v := url.Values{
|
||||
argPath: []string{path},
|
||||
}
|
||||
|
||||
respLocks, _, err := g.queryLocks(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(respLocks) == 0 {
|
||||
return nil, transfer.ErrNotFound
|
||||
}
|
||||
return respLocks[0], nil
|
||||
}
|
||||
|
||||
// FromID implements transfer.LockBackend
|
||||
func (g *giteaLockBackend) FromID(id string) (transfer.Lock, error) {
|
||||
v := url.Values{
|
||||
argID: []string{id},
|
||||
}
|
||||
|
||||
respLocks, _, err := g.queryLocks(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(respLocks) == 0 {
|
||||
return nil, transfer.ErrNotFound
|
||||
}
|
||||
return respLocks[0], nil
|
||||
}
|
||||
|
||||
// Range implements transfer.LockBackend
|
||||
func (g *giteaLockBackend) Range(cursor string, limit int, iter func(transfer.Lock) error) (string, error) {
|
||||
v := url.Values{
|
||||
argLimit: []string{strconv.FormatInt(int64(limit), 10)},
|
||||
}
|
||||
if cursor != "" {
|
||||
v[argCursor] = []string{cursor}
|
||||
}
|
||||
|
||||
respLocks, cursor, err := g.queryLocks(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, lock := range respLocks {
|
||||
err := iter(lock)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return cursor, nil
|
||||
}
|
||||
|
||||
func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) {
|
||||
serverURLWithQuery := g.server.JoinPath() // get a copy
|
||||
serverURLWithQuery.RawQuery = v.Encode()
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequestLFS(g.ctx, serverURLWithQuery.String(), http.MethodGet, headers, nil)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request error", err)
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
g.logger.Log("http read error", err)
|
||||
return nil, "", err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
||||
return nil, "", statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
var respBody lfslock.LFSLockList
|
||||
err = json.Unmarshal(respBytes, &respBody)
|
||||
if err != nil {
|
||||
g.logger.Log("json umarshal error", err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
respLocks := make([]transfer.Lock, 0, len(respBody.Locks))
|
||||
for _, respLock := range respBody.Locks {
|
||||
owner := userUnknown
|
||||
if respLock.Owner != nil {
|
||||
owner = respLock.Owner.Name
|
||||
}
|
||||
lock := newGiteaLock(g, respLock.ID, respLock.Path, respLock.LockedAt, owner)
|
||||
respLocks = append(respLocks, lock)
|
||||
}
|
||||
return respLocks, respBody.Next, nil
|
||||
}
|
||||
|
||||
var _ transfer.Lock = &giteaLock{}
|
||||
|
||||
type giteaLock struct {
|
||||
g *giteaLockBackend
|
||||
id string
|
||||
path string
|
||||
lockedAt time.Time
|
||||
owner string
|
||||
}
|
||||
|
||||
func newGiteaLock(g *giteaLockBackend, id, path string, lockedAt time.Time, owner string) transfer.Lock {
|
||||
return &giteaLock{g: g, id: id, path: path, lockedAt: lockedAt, owner: owner}
|
||||
}
|
||||
|
||||
// Unlock implements transfer.Lock
|
||||
func (g *giteaLock) Unlock() error {
|
||||
return g.g.Unlock(g)
|
||||
}
|
||||
|
||||
// ID implements transfer.Lock
|
||||
func (g *giteaLock) ID() string {
|
||||
return g.id
|
||||
}
|
||||
|
||||
// Path implements transfer.Lock
|
||||
func (g *giteaLock) Path() string {
|
||||
return g.path
|
||||
}
|
||||
|
||||
// FormattedTimestamp implements transfer.Lock
|
||||
func (g *giteaLock) FormattedTimestamp() string {
|
||||
return g.lockedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// OwnerName implements transfer.Lock
|
||||
func (g *giteaLock) OwnerName() string {
|
||||
return g.owner
|
||||
}
|
||||
|
||||
func (g *giteaLock) CurrentUser() (string, error) {
|
||||
return userSelf, nil
|
||||
}
|
||||
|
||||
// AsLockSpec implements transfer.Lock
|
||||
func (g *giteaLock) AsLockSpec(ownerID bool) ([]string, error) {
|
||||
msgs := []string{
|
||||
"lock " + g.ID(),
|
||||
fmt.Sprintf("path %s %s", g.ID(), g.Path()),
|
||||
fmt.Sprintf("locked-at %s %s", g.ID(), g.FormattedTimestamp()),
|
||||
fmt.Sprintf("ownername %s %s", g.ID(), g.OwnerName()),
|
||||
}
|
||||
if ownerID {
|
||||
user, err := g.CurrentUser()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting current user: %w", err)
|
||||
}
|
||||
who := "theirs"
|
||||
if user == g.OwnerName() {
|
||||
who = "ours"
|
||||
}
|
||||
msgs = append(msgs, fmt.Sprintf("owner %s %s", g.ID(), who))
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// AsArguments implements transfer.Lock
|
||||
func (g *giteaLock) AsArguments() []string {
|
||||
return []string{
|
||||
"id=" + g.ID(),
|
||||
"path=" + g.Path(),
|
||||
"locked-at=" + g.FormattedTimestamp(),
|
||||
"ownername=" + g.OwnerName(),
|
||||
}
|
||||
}
|
149
modules/lfstransfer/backend/util.go
Normal file
149
modules/lfstransfer/backend/util.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
|
||||
// HTTP headers
|
||||
const (
|
||||
headerAccept = "Accept"
|
||||
headerAuthorization = "Authorization"
|
||||
headerGiteaInternalAuth = "X-Gitea-Internal-Auth"
|
||||
headerContentType = "Content-Type"
|
||||
headerContentLength = "Content-Length"
|
||||
)
|
||||
|
||||
// MIME types
|
||||
const (
|
||||
mimeGitLFS = "application/vnd.git-lfs+json"
|
||||
mimeOctetStream = "application/octet-stream"
|
||||
)
|
||||
|
||||
// SSH protocol action keys
|
||||
const (
|
||||
actionDownload = "download"
|
||||
actionUpload = "upload"
|
||||
actionVerify = "verify"
|
||||
)
|
||||
|
||||
// SSH protocol argument keys
|
||||
const (
|
||||
argCursor = "cursor"
|
||||
argExpiresAt = "expires-at"
|
||||
argID = "id"
|
||||
argLimit = "limit"
|
||||
argPath = "path"
|
||||
argRefname = "refname"
|
||||
argToken = "token"
|
||||
argTransfer = "transfer"
|
||||
)
|
||||
|
||||
// Default username constants
|
||||
const (
|
||||
userSelf = "(self)"
|
||||
userUnknown = "(unknown)"
|
||||
)
|
||||
|
||||
// Operations enum
|
||||
const (
|
||||
opDownload = iota + 1
|
||||
opUpload
|
||||
)
|
||||
|
||||
var opMap = map[string]int{
|
||||
"download": opDownload,
|
||||
"upload": opUpload,
|
||||
}
|
||||
|
||||
var ErrMissingID = fmt.Errorf("%w: missing id arg", transfer.ErrMissingData)
|
||||
|
||||
func statusCodeToErr(code int) error {
|
||||
switch code {
|
||||
case http.StatusBadRequest:
|
||||
return transfer.ErrParseError
|
||||
case http.StatusConflict:
|
||||
return transfer.ErrConflict
|
||||
case http.StatusForbidden:
|
||||
return transfer.ErrForbidden
|
||||
case http.StatusNotFound:
|
||||
return transfer.ErrNotFound
|
||||
case http.StatusUnauthorized:
|
||||
return transfer.ErrUnauthorized
|
||||
default:
|
||||
return fmt.Errorf("server returned status %v: %v", code, http.StatusText(code))
|
||||
}
|
||||
}
|
||||
|
||||
func toInternalLFSURL(s string) string {
|
||||
pos1 := strings.Index(s, "://")
|
||||
if pos1 == -1 {
|
||||
return ""
|
||||
}
|
||||
appSubURLWithSlash := setting.AppSubURL + "/"
|
||||
pos2 := strings.Index(s[pos1+3:], appSubURLWithSlash)
|
||||
if pos2 == -1 {
|
||||
return ""
|
||||
}
|
||||
routePath := s[pos1+3+pos2+len(appSubURLWithSlash):]
|
||||
fields := strings.SplitN(routePath, "/", 3)
|
||||
if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") {
|
||||
return ""
|
||||
}
|
||||
return setting.LocalURL + "api/internal/repo/" + routePath
|
||||
}
|
||||
|
||||
func isInternalLFSURL(s string) bool {
|
||||
if !strings.HasPrefix(s, setting.LocalURL) {
|
||||
return false
|
||||
}
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
routePath := util.PathJoinRelX(u.Path)
|
||||
subRoutePath, cut := strings.CutPrefix(routePath, "api/internal/repo/")
|
||||
if !cut {
|
||||
return false
|
||||
}
|
||||
fields := strings.SplitN(subRoutePath, "/", 3)
|
||||
if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func newInternalRequestLFS(ctx context.Context, internalURL, method string, headers map[string]string, body any) *httplib.Request {
|
||||
if !isInternalLFSURL(internalURL) {
|
||||
return nil
|
||||
}
|
||||
req := private.NewInternalRequest(ctx, internalURL, method)
|
||||
req.SetReadWriteTimeout(0)
|
||||
for k, v := range headers {
|
||||
req.Header(k, v)
|
||||
}
|
||||
switch body := body.(type) {
|
||||
case nil: // do nothing
|
||||
case []byte:
|
||||
req.Body(body) // []byte
|
||||
case io.Reader:
|
||||
req.Body(body) // io.Reader or io.ReadCloser
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported request body type %T", body))
|
||||
}
|
||||
return req
|
||||
}
|
53
modules/lfstransfer/backend/util_test.go
Normal file
53
modules/lfstransfer/backend/util_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToInternalLFSURL(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
|
||||
cases := []struct {
|
||||
url string
|
||||
expected string
|
||||
}{
|
||||
{"http://appurl/any", ""},
|
||||
{"http://appurl/sub/any", ""},
|
||||
{"http://appurl/sub/owner/repo/any", ""},
|
||||
{"http://appurl/sub/owner/repo/info/any", ""},
|
||||
{"http://appurl/sub/owner/repo/info/lfs/any", "http://localurl/api/internal/repo/owner/repo/info/lfs/any"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expected, toInternalLFSURL(c.url), c.url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInternalLFSURL(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")()
|
||||
defer test.MockVariableValue(&setting.InternalToken, "mock-token")()
|
||||
cases := []struct {
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
{"", false},
|
||||
{"http://otherurl/api/internal/repo/owner/repo/info/lfs/any", false},
|
||||
{"http://localurl/api/internal/repo/owner/repo/info/lfs/any", true},
|
||||
{"http://localurl/api/internal/repo/owner/repo/info", false},
|
||||
{"http://localurl/api/internal/misc/owner/repo/info/lfs/any", false},
|
||||
{"http://localurl/api/internal/owner/repo/info/lfs/any", false},
|
||||
{"http://localurl/api/internal/foo/bar", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
req := newInternalRequestLFS(t.Context(), c.url, "GET", nil, nil)
|
||||
assert.Equal(t, c.expected, req != nil, c.url)
|
||||
assert.Equal(t, c.expected, isInternalLFSURL(c.url), c.url)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user