mirror of https://github.com/go-gitea/gitea.git
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.
149 lines
3.3 KiB
149 lines
3.3 KiB
// 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 |
|
}
|
|
|