diff --git a/modules/httplib/request.go b/modules/httplib/request.go index 880d7ad3cb..267e276df3 100644 --- a/modules/httplib/request.go +++ b/modules/httplib/request.go @@ -99,10 +99,10 @@ func (r *Request) Param(key, value string) *Request { return r } -// Body adds request raw body. -// it supports string and []byte. +// Body adds request raw body. It supports string, []byte and io.Reader as body. func (r *Request) Body(data any) *Request { switch t := data.(type) { + case nil: // do nothing case string: bf := bytes.NewBufferString(t) r.req.Body = io.NopCloser(bf) @@ -111,6 +111,12 @@ func (r *Request) Body(data any) *Request { bf := bytes.NewBuffer(t) r.req.Body = io.NopCloser(bf) r.req.ContentLength = int64(len(t)) + case io.ReadCloser: + r.req.Body = t + case io.Reader: + r.req.Body = io.NopCloser(t) + default: + panic(fmt.Sprintf("unsupported request body type %T", t)) } return r } @@ -141,7 +147,7 @@ func (r *Request) getResponse() (*http.Response, error) { } } else if r.req.Method == "POST" && r.req.Body == nil && len(paramBody) > 0 { r.Header("Content-Type", "application/x-www-form-urlencoded") - r.Body(paramBody) + r.Body(paramBody) // string } var err error @@ -185,6 +191,7 @@ func (r *Request) getResponse() (*http.Response, error) { } // Response executes request client gets response manually. +// Caller MUST close the response body if no error occurs func (r *Request) Response() (*http.Response, error) { return r.getResponse() } diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go index 2b1fe49fda..540932b930 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 { @@ -78,17 +77,17 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, url, 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 +157,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 { @@ -187,25 +185,25 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeOctetStream, } - req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil) + req := newInternalRequestLFS(g.ctx, url, 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 { @@ -234,15 +232,14 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer 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, url, 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) } @@ -284,11 +281,12 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, url, 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..4b45658611 100644 --- a/modules/lfstransfer/backend/lock.go +++ b/modules/lfstransfer/backend/lock.go @@ -50,7 +50,7 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) { headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) @@ -102,7 +102,7 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error { headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) @@ -185,7 +185,7 @@ func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, er headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil) + req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go index cffefef375..f322d54257 100644 --- a/modules/lfstransfer/backend/util.go +++ b/modules/lfstransfer/backend/util.go @@ -5,15 +5,12 @@ package backend import ( "context" - "crypto/tls" "fmt" - "net" + "io" "net/http" - "time" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/proxyprotocol" - "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/private" "github.com/charmbracelet/git-lfs-transfer/transfer" ) @@ -89,53 +86,19 @@ 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 newInternalRequestLFS(ctx context.Context, url, method string, headers map[string]string, body any) *httplib.Request { + req := private.NewInternalRequest(ctx, url, method) 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/private/actions.go b/modules/private/actions.go index 311a283650..e68f2f85b0 100644 --- a/modules/private/actions.go +++ b/modules/private/actions.go @@ -17,7 +17,7 @@ type GenerateTokenRequest struct { func GenerateActionsRunnerToken(ctx context.Context, scope string) (*ResponseText, ResponseExtra) { reqURL := setting.LocalURL + "api/internal/actions/generate_actions_runner_token" - req := newInternalRequest(ctx, reqURL, "POST", GenerateTokenRequest{ + req := newInternalRequestAPI(ctx, reqURL, "POST", GenerateTokenRequest{ Scope: scope, }) diff --git a/modules/private/hook.go b/modules/private/hook.go index 745c200619..87d6549f9c 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -85,7 +85,7 @@ type HookProcReceiveRefResult struct { // HookPreReceive check whether the provided commits are allowed func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequest(ctx, reqURL, "POST", opts) + req := newInternalRequestAPI(ctx, reqURL, "POST", opts) req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) _, extra := requestJSONResp(req, &ResponseText{}) return extra @@ -94,7 +94,7 @@ func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOp // HookPostReceive updates services and users func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequest(ctx, reqURL, "POST", opts) + req := newInternalRequestAPI(ctx, reqURL, "POST", opts) req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) return requestJSONResp(req, &HookPostReceiveResult{}) } @@ -103,7 +103,7 @@ func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookO func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequest(ctx, reqURL, "POST", opts) + req := newInternalRequestAPI(ctx, reqURL, "POST", opts) req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) return requestJSONResp(req, &HookProcReceiveResult{}) } @@ -115,7 +115,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R url.PathEscape(repoName), url.PathEscape(branch), ) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") _, extra := requestJSONResp(req, &ResponseText{}) return extra } @@ -123,7 +123,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R // SSHLog sends ssh error log response func SSHLog(ctx context.Context, isErr bool, msg string) error { reqURL := setting.LocalURL + "api/internal/ssh/log" - req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) + req := newInternalRequestAPI(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) _, extra := requestJSONResp(req, &ResponseText{}) return extra.Error } diff --git a/modules/private/internal.go b/modules/private/internal.go index c7e7773524..3bd4eb06b1 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -34,7 +34,7 @@ func getClientIP() string { return strings.Fields(sshConnEnv)[0] } -func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request { +func NewInternalRequest(ctx context.Context, url, method string) *httplib.Request { if setting.InternalToken == "" { log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) @@ -82,13 +82,17 @@ Ensure you are running in the correct environment or set the correct configurati }, }) } + return req +} +func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request { + req := NewInternalRequest(ctx, url, method) if len(body) == 1 { req.Header("Content-Type", "application/json") jsonBytes, _ := json.Marshal(body[0]) req.Body(jsonBytes) } else if len(body) > 1 { - log.Fatal("Too many arguments for newInternalRequest") + log.Fatal("Too many arguments for newInternalRequestAPI") } req.SetTimeout(10*time.Second, 60*time.Second) diff --git a/modules/private/key.go b/modules/private/key.go index dcd1714856..114683b343 100644 --- a/modules/private/key.go +++ b/modules/private/key.go @@ -14,7 +14,7 @@ import ( func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") _, extra := requestJSONResp(req, &ResponseText{}) return extra.Error } @@ -24,7 +24,7 @@ func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { func AuthorizedPublicKeyByContent(ctx context.Context, content string) (*ResponseText, ResponseExtra) { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") req.Param("content", content) return requestJSONResp(req, &ResponseText{}) } diff --git a/modules/private/mail.go b/modules/private/mail.go index 08de5b7e28..3904e37bea 100644 --- a/modules/private/mail.go +++ b/modules/private/mail.go @@ -23,7 +23,7 @@ type Email struct { func SendEmail(ctx context.Context, subject, message string, to []string) (*ResponseText, ResponseExtra) { reqURL := setting.LocalURL + "api/internal/mail/send" - req := newInternalRequest(ctx, reqURL, "POST", Email{ + req := newInternalRequestAPI(ctx, reqURL, "POST", Email{ Subject: subject, Message: message, To: to, diff --git a/modules/private/manager.go b/modules/private/manager.go index 6055e553bd..e3d5ad57e0 100644 --- a/modules/private/manager.go +++ b/modules/private/manager.go @@ -18,21 +18,21 @@ import ( // Shutdown calls the internal shutdown function func Shutdown(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/shutdown" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Shutting down") } // Restart calls the internal restart function func Restart(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/restart" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Restarting") } // ReloadTemplates calls the internal reload-templates function func ReloadTemplates(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/reload-templates" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Reloaded") } @@ -45,7 +45,7 @@ type FlushOptions struct { // FlushQueues calls the internal flush-queues function func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/flush-queues" - req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) + req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) if timeout > 0 { req.SetReadWriteTimeout(timeout + 10*time.Second) } @@ -55,28 +55,28 @@ func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) R // PauseLogging pauses logging func PauseLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/pause-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Paused") } // ResumeLogging resumes logging func ResumeLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/resume-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Restarted") } // ReleaseReopenLogging releases and reopens logging files func ReleaseReopenLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Restarted") } // SetLogSQL sets database logging func SetLogSQL(ctx context.Context, on bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Log SQL setting set") } @@ -91,7 +91,7 @@ type LoggerOptions struct { // AddLogger adds a logger func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]any) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/add-logger" - req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{ + req := newInternalRequestAPI(ctx, reqURL, "POST", LoggerOptions{ Logger: logger, Writer: writer, Mode: mode, @@ -103,7 +103,7 @@ func AddLogger(ctx context.Context, logger, writer, mode string, config map[stri // RemoveLogger removes a logger func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer)) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Removed") } @@ -111,7 +111,7 @@ func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra { func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel)) - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") callback := func(resp *http.Response, extra *ResponseExtra) { _, extra.Error = io.Copy(out, resp.Body) } diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go index 496209d3cb..9c3a008142 100644 --- a/modules/private/restore_repo.go +++ b/modules/private/restore_repo.go @@ -24,7 +24,7 @@ type RestoreParams struct { func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/restore_repo" - req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{ + req := newInternalRequestAPI(ctx, reqURL, "POST", RestoreParams{ RepoDir: repoDir, OwnerName: ownerName, RepoName: repoName, diff --git a/modules/private/serv.go b/modules/private/serv.go index 480a446954..2ccc6c1129 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -23,7 +23,7 @@ type KeyAndOwner struct { // ServNoCommand returns information about the provided key func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID) - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{}) if extra.HasError() { return nil, nil, extra.Error @@ -58,6 +58,6 @@ func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, m reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) } } - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") return requestJSONResp(req, &ServCommandResults{}) } diff --git a/services/lfs/server.go b/services/lfs/server.go index a77623fdc1..c4866edaab 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -134,7 +134,9 @@ func DownloadHandler(ctx *context.Context) { } contentLength := toByte + 1 - fromByte - ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10)) + contentLengthStr := strconv.FormatInt(contentLength, 10) + ctx.Resp.Header().Set("Content-Length", contentLengthStr) + ctx.Resp.Header().Set("X-Gitea-LFS-Content-Length", contentLengthStr) // we need this header to make sure it won't be affected by reverse proxy or compression ctx.Resp.Header().Set("Content-Type", "application/octet-stream") filename := ctx.PathParam("filename")