diff options
Diffstat (limited to 'modules/lfstransfer/backend')
-rw-r--r-- | modules/lfstransfer/backend/backend.go | 46 | ||||
-rw-r--r-- | modules/lfstransfer/backend/lock.go | 26 | ||||
-rw-r--r-- | modules/lfstransfer/backend/util.go | 106 | ||||
-rw-r--r-- | modules/lfstransfer/backend/util_test.go | 53 |
4 files changed, 142 insertions, 89 deletions
diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go index 2b1fe49fda..dd4108ea56 100644 --- a/modules/lfstransfer/backend/backend.go +++ b/modules/lfstransfer/backend/backend.go @@ -4,7 +4,6 @@ package backend import ( - "bytes" "context" "encoding/base64" "fmt" @@ -29,7 +28,7 @@ var Capabilities = []string{ "locking", } -var _ transfer.Backend = &GiteaBackend{} +var _ transfer.Backend = (*GiteaBackend)(nil) // GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API type GiteaBackend struct { @@ -48,7 +47,7 @@ func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (t return nil, err } server = server.JoinPath("api/internal/repo", repo, "info/lfs") - return &GiteaBackend{ctx: ctx, server: server, op: op, authToken: token, internalAuth: fmt.Sprintf("Bearer %s", setting.InternalToken), logger: logger}, nil + return &GiteaBackend{ctx: ctx, server: server, op: op, authToken: token, internalAuth: "Bearer " + setting.InternalToken, logger: logger}, nil } // Batch implements transfer.Backend @@ -71,24 +70,23 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans g.logger.Log("json marshal error", err) return nil, err } - url := g.server.JoinPath("objects/batch").String() headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + 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) } - defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { g.logger.Log("http read error", err) @@ -158,8 +156,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans return pointers, nil } -// Download implements transfer.Backend. The returned reader must be closed by the -// caller. +// 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 { @@ -181,31 +178,30 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, g.logger.Log("argument id incorrect") return nil, 0, transfer.ErrCorruptData } - url := action.Href headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeOctetStream, } - req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil) + req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodGet, headers, nil) resp, err := req.Response() if err != nil { - return nil, 0, err + 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) } - defer resp.Body.Close() - respBytes, err := io.ReadAll(resp.Body) + + respSize, err := strconv.ParseInt(resp.Header.Get("X-Gitea-LFS-Content-Length"), 10, 64) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to parse content length: %w", err) } - respSize := int64(len(respBytes)) - respBuf := io.NopCloser(bytes.NewBuffer(respBytes)) - return respBuf, respSize, nil + // transfer.Backend will check io.Closer interface and close this Body reader + return resp.Body, respSize, nil } -// StartUpload implements transfer.Backend. +// 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 { @@ -227,22 +223,20 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer g.logger.Log("argument id incorrect") return transfer.ErrCorruptData } - url := action.Href headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerContentType: mimeOctetStream, headerContentLength: strconv.FormatInt(size, 10), } - reqBytes, err := io.ReadAll(r) - if err != nil { - return err - } - req := newInternalRequest(g.ctx, url, http.MethodPut, headers, reqBytes) + + 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) } @@ -277,18 +271,18 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans // the server sent no verify action return transfer.SuccessStatus(), nil } - url := action.Href headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + 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) } diff --git a/modules/lfstransfer/backend/lock.go b/modules/lfstransfer/backend/lock.go index f094cce1db..2c3c16a9bb 100644 --- a/modules/lfstransfer/backend/lock.go +++ b/modules/lfstransfer/backend/lock.go @@ -5,6 +5,7 @@ package backend import ( "context" + "errors" "fmt" "io" "net/http" @@ -43,14 +44,13 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) { g.logger.Log("json marshal error", err) return nil, err } - url := g.server.String() headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + 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) @@ -75,7 +75,7 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) { if respBody.Lock == nil { g.logger.Log("api returned nil lock") - return nil, fmt.Errorf("api returned nil lock") + return nil, errors.New("api returned nil lock") } respLock := respBody.Lock owner := userUnknown @@ -95,14 +95,13 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error { g.logger.Log("json marshal error", err) return err } - url := g.server.JoinPath(lock.ID(), "unlock").String() headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + 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) @@ -176,16 +175,15 @@ func (g *giteaLockBackend) Range(cursor string, limit int, iter func(transfer.Lo } func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) { - urlq := g.server.JoinPath() // get a copy - urlq.RawQuery = v.Encode() - url := urlq.String() + 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 := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil) + req := newInternalRequestLFS(g.ctx, serverURLWithQuery.String(), http.MethodGet, headers, nil) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) @@ -266,7 +264,7 @@ func (g *giteaLock) CurrentUser() (string, error) { // AsLockSpec implements transfer.Lock func (g *giteaLock) AsLockSpec(ownerID bool) ([]string, error) { msgs := []string{ - fmt.Sprintf("lock %s", g.ID()), + "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()), @@ -288,9 +286,9 @@ func (g *giteaLock) AsLockSpec(ownerID bool) ([]string, error) { // AsArguments implements transfer.Lock func (g *giteaLock) AsArguments() []string { return []string{ - fmt.Sprintf("id=%s", g.ID()), - fmt.Sprintf("path=%s", g.Path()), - fmt.Sprintf("locked-at=%s", g.FormattedTimestamp()), - fmt.Sprintf("ownername=%s", g.OwnerName()), + "id=" + g.ID(), + "path=" + g.Path(), + "locked-at=" + g.FormattedTimestamp(), + "ownername=" + g.OwnerName(), } } diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go index cffefef375..afe02f799c 100644 --- a/modules/lfstransfer/backend/util.go +++ b/modules/lfstransfer/backend/util.go @@ -5,15 +5,16 @@ package backend import ( "context" - "crypto/tls" "fmt" - "net" + "io" "net/http" - "time" + "net/url" + "strings" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/proxyprotocol" + "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" ) @@ -60,8 +61,7 @@ const ( // Operations enum const ( - opNone = iota - opDownload + opDownload = iota + 1 opUpload ) @@ -89,53 +89,61 @@ func statusCodeToErr(code int) error { } } -func newInternalRequest(ctx context.Context, url, method string, headers map[string]string, body []byte) *httplib.Request { - req := httplib.NewRequest(url, method). - SetContext(ctx). - SetTimeout(10*time.Second, 60*time.Second). - SetTLSClientConfig(&tls.Config{ - InsecureSkipVerify: true, - }) - - if setting.Protocol == setting.HTTPUnix { - req.SetTransport(&http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr) - if err != nil { - return conn, err - } - if setting.LocalUseProxyProtocol { - if err = proxyprotocol.WriteLocalHeader(conn); err != nil { - _ = conn.Close() - return nil, err - } - } - return conn, err - }, - }) - } else if setting.LocalUseProxyProtocol { - req.SetTransport(&http.Transport{ - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, network, address) - if err != nil { - return conn, err - } - if err = proxyprotocol.WriteLocalHeader(conn); err != nil { - _ = conn.Close() - return nil, err - } - return conn, err - }, - }) +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) } - - req.Body(body) - + 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 } diff --git a/modules/lfstransfer/backend/util_test.go b/modules/lfstransfer/backend/util_test.go new file mode 100644 index 0000000000..408b53c369 --- /dev/null +++ b/modules/lfstransfer/backend/util_test.go @@ -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) + } +} |