mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
155 lines
3.6 KiB
Go
155 lines
3.6 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package backend
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/httplib"
|
|
"github.com/hanzoai/git/modules/private"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/util"
|
|
|
|
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
|
)
|
|
|
|
// internalRepoRoutePath is the repo branch of the internal API, relative to
|
|
// setting.LocalURL — derived from private.RoutePrefix so a mount change reaches
|
|
// both the URL builder and the matcher below.
|
|
var internalRepoRoutePath = private.RoutePrefix[1:] + "/repo/"
|
|
|
|
// HTTP headers
|
|
const (
|
|
headerAccept = "Accept"
|
|
headerAuthorization = "Authorization"
|
|
headerInternalAuth = "X-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 + internalRepoRoutePath + 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, internalRepoRoutePath)
|
|
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
|
|
}
|