mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
- Keep the download URLs of a batch response in memory until shortly before they expire, so the GET that follows needs no second batch request. A kept URL that fails is replaced by a fresh one. - Cut off a download that isn't the object's size. - Copy an object whose content is already stored, such as for a fork, instead of downloading it, once the upstream has confirmed the repository has it. Artifacts are indexed by content hash for this.
559 lines
19 KiB
Go
559 lines
19 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// gitLFSPath separates a repository from the Git LFS API below it, as
|
|
// git-lfs derives the API URL from a remote: {remote}.git/info/lfs.
|
|
gitLFSPath = "/info/lfs/"
|
|
gitLFSMediaType = "application/vnd.git-lfs+json"
|
|
gitLFSTransfer = "basic"
|
|
gitLFSHashAlgo = "sha256"
|
|
// gitLFSObjectMediaType is what objects are served as.
|
|
gitLFSObjectMediaType = "application/octet-stream"
|
|
|
|
// gitLFSMaxBatchRequest bounds a client's batch request. git-lfs sends
|
|
// 100 objects per batch by default, a few kilobytes.
|
|
gitLFSMaxBatchRequest = 1 << 20
|
|
// gitLFSMaxBatchResponse bounds an upstream batch response, which holds
|
|
// an href and headers per object.
|
|
gitLFSMaxBatchResponse = 16 << 20
|
|
|
|
gitLFSOIDLength = 64
|
|
)
|
|
|
|
// lfsBatchRequest is a Git LFS batch API request.
|
|
type lfsBatchRequest struct {
|
|
Operation string `json:"operation"`
|
|
Transfers []string `json:"transfers,omitempty"`
|
|
Ref json.RawMessage `json:"ref,omitempty"`
|
|
Objects []lfsObject `json:"objects"`
|
|
HashAlgo string `json:"hash_algo,omitempty"`
|
|
}
|
|
|
|
type lfsObject struct {
|
|
OID string `json:"oid"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
// lfsBatchResponse is a Git LFS batch API response.
|
|
type lfsBatchResponse struct {
|
|
Transfer string `json:"transfer,omitempty"`
|
|
Objects []lfsObjectResponse `json:"objects"`
|
|
HashAlgo string `json:"hash_algo,omitempty"`
|
|
}
|
|
|
|
type lfsObjectResponse struct {
|
|
OID string `json:"oid"`
|
|
Size int64 `json:"size"`
|
|
Authenticated bool `json:"authenticated,omitempty"`
|
|
Actions map[string]lfsAction `json:"actions,omitempty"`
|
|
Error *lfsObjectError `json:"error,omitempty"`
|
|
}
|
|
|
|
type lfsAction struct {
|
|
Href string `json:"href"`
|
|
Header map[string]string `json:"header,omitempty"`
|
|
ExpiresIn int64 `json:"expires_in,omitempty"`
|
|
ExpiresAt string `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
type lfsObjectError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// lfsUpstreamError is a batch request the upstream answered with an error
|
|
// status.
|
|
type lfsUpstreamError struct {
|
|
status int
|
|
}
|
|
|
|
func (e *lfsUpstreamError) Error() string {
|
|
return fmt.Sprintf("LFS batch API answered %d", e.status)
|
|
}
|
|
|
|
// WithBaseURL sets the URL clients reach the proxy at, which LFS batch
|
|
// responses send them back to for each object.
|
|
func (h *GitHandler) WithBaseURL(baseURL string) *GitHandler {
|
|
h.baseURL = strings.TrimSuffix(baseURL, "/")
|
|
return h
|
|
}
|
|
|
|
// serveLFS answers a request to the Git LFS API of a repository: batch
|
|
// downloads, and the objects the batch responses point at. Objects are
|
|
// stored in the artifact cache, checked against their sha256 object ID.
|
|
// Uploads and locking are refused.
|
|
func (h *GitHandler) serveLFS(w http.ResponseWriter, r *http.Request, repoPath, endpoint string) {
|
|
repo, status, msg := h.repository(repoPath)
|
|
if status != 0 {
|
|
lfsError(w, status, msg)
|
|
return
|
|
}
|
|
if endpoint == "objects/batch" {
|
|
if r.Method != http.MethodPost {
|
|
lfsError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
h.serveLFSBatch(w, r, repo)
|
|
return
|
|
}
|
|
oid, sizeText, ok := strings.Cut(strings.TrimPrefix(endpoint, "objects/"), "/")
|
|
size, err := strconv.ParseInt(sizeText, 10, 64)
|
|
if !strings.HasPrefix(endpoint, "objects/") || !ok || err != nil || size < 0 || !validLFSOID(oid) {
|
|
lfsError(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
lfsError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
h.serveLFSObject(w, r, repo, lfsObject{OID: oid, Size: size})
|
|
}
|
|
|
|
// serveLFSBatch forwards a download batch upstream and points every object
|
|
// the upstream offers at this proxy. The upstream answers for the
|
|
// repository, so a client only gets objects it could get from there.
|
|
func (h *GitHandler) serveLFSBatch(w http.ResponseWriter, r *http.Request, repo gitRepository) {
|
|
var req lfsBatchRequest
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, gitLFSMaxBatchRequest)).Decode(&req); err != nil {
|
|
var tooLarge *http.MaxBytesError
|
|
if errors.As(err, &tooLarge) {
|
|
lfsError(w, http.StatusRequestEntityTooLarge, "batch request too large")
|
|
return
|
|
}
|
|
lfsError(w, http.StatusBadRequest, "invalid batch request")
|
|
return
|
|
}
|
|
switch {
|
|
case req.Operation != "download":
|
|
lfsError(w, http.StatusForbidden, "pushing is not supported")
|
|
return
|
|
case req.HashAlgo != "" && req.HashAlgo != gitLFSHashAlgo:
|
|
lfsError(w, http.StatusConflict, "only sha256 objects are supported")
|
|
return
|
|
case len(req.Transfers) > 0 && !slices.Contains(req.Transfers, gitLFSTransfer):
|
|
lfsError(w, http.StatusUnprocessableEntity, "only the basic transfer is supported")
|
|
return
|
|
}
|
|
requested := make(map[string]int64, len(req.Objects))
|
|
for _, object := range req.Objects {
|
|
if !validLFSOID(object.OID) || object.Size < 0 {
|
|
lfsError(w, http.StatusUnprocessableEntity, "invalid object")
|
|
return
|
|
}
|
|
requested[object.OID] = object.Size
|
|
}
|
|
|
|
upstream, err := h.lfsBatch(r.Context(), repo, req.Ref, req.Objects)
|
|
if err != nil {
|
|
if resp, ok := h.lfsBatchFromCache(r, repo, req.Objects, err); ok {
|
|
writeLFSBatch(w, resp)
|
|
return
|
|
}
|
|
h.serveLFSUpstreamError(w, r, repo, err)
|
|
return
|
|
}
|
|
resp := newLFSBatchResponse()
|
|
for _, object := range upstream.Objects {
|
|
// Only objects the client asked for, at the size it gave, so a
|
|
// response can't point the client at anything else.
|
|
size, ok := requested[object.OID]
|
|
if !ok || size != object.Size {
|
|
continue
|
|
}
|
|
out := lfsObjectResponse{OID: object.OID, Size: object.Size, Error: object.Error}
|
|
if download, ok := object.Actions["download"]; ok && object.Error == nil && download.Href != "" {
|
|
out.Authenticated = true
|
|
out.Actions = map[string]lfsAction{"download": {Href: h.lfsObjectURL(repo, object.OID, object.Size)}}
|
|
// Kept so the client's GET for the object needs no second
|
|
// batch request.
|
|
h.lfsDownloads.put(repo.key, lfsObject{OID: object.OID, Size: object.Size}, download, time.Now())
|
|
}
|
|
resp.Objects = append(resp.Objects, out)
|
|
}
|
|
writeLFSBatch(w, resp)
|
|
}
|
|
|
|
// lfsBatchFromCache answers a batch the upstream couldn't, because it was
|
|
// unreachable or failing, from the objects cached for repo. This is what a
|
|
// GET of each object would serve anyway. A refusal by the upstream is not
|
|
// overridden, and a batch none of whose objects are cached is left to fail.
|
|
func (h *GitHandler) lfsBatchFromCache(r *http.Request, repo gitRepository, objects []lfsObject, err error) (lfsBatchResponse, bool) {
|
|
var refused *lfsUpstreamError
|
|
if r.Context().Err() != nil || (errors.As(err, &refused) && refused.status < http.StatusInternalServerError) {
|
|
return lfsBatchResponse{}, false
|
|
}
|
|
resp := newLFSBatchResponse()
|
|
seen := make(map[string]bool, len(objects))
|
|
cached := 0
|
|
for _, object := range objects {
|
|
if seen[object.OID] {
|
|
continue
|
|
}
|
|
seen[object.OID] = true
|
|
out := lfsObjectResponse{OID: object.OID, Size: object.Size}
|
|
if ok, _ := h.proxy.HasCachedArtifact(r.Context(), gitEcosystem, repo.lfsName(), object.OID, object.OID); ok {
|
|
out.Authenticated = true
|
|
out.Actions = map[string]lfsAction{"download": {Href: h.lfsObjectURL(repo, object.OID, object.Size)}}
|
|
cached++
|
|
} else {
|
|
out.Error = &lfsObjectError{Code: http.StatusBadGateway, Message: "upstream unavailable and object not cached"}
|
|
}
|
|
resp.Objects = append(resp.Objects, out)
|
|
}
|
|
if cached == 0 {
|
|
return lfsBatchResponse{}, false
|
|
}
|
|
h.proxy.Logger.Warn("git: LFS batch request failed, answering from the cache",
|
|
"repository", repo.key, "cached", cached, "objects", len(resp.Objects), "error", err)
|
|
return resp, true
|
|
}
|
|
|
|
func newLFSBatchResponse() lfsBatchResponse {
|
|
return lfsBatchResponse{Transfer: gitLFSTransfer, Objects: []lfsObjectResponse{}, HashAlgo: gitLFSHashAlgo}
|
|
}
|
|
|
|
func writeLFSBatch(w http.ResponseWriter, resp lfsBatchResponse) {
|
|
w.Header().Set("Content-Type", gitLFSMediaType)
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// serveLFSObject serves an object from the artifact cache. On a miss it
|
|
// takes the download URL from the client's batch, or asks the upstream batch
|
|
// API for a fresh one, and caches the object from there, or from a copy
|
|
// already stored under another repository, once its content matches its
|
|
// object ID.
|
|
func (h *GitHandler) serveLFSObject(w http.ResponseWriter, r *http.Request, repo gitRepository, object lfsObject) {
|
|
cached, err := h.proxy.GetCachedArtifact(r.Context(), gitEcosystem, repo.lfsName(), object.OID, object.OID)
|
|
if err != nil {
|
|
h.proxy.serveArtifactError(w, err, "failed to read cached LFS object")
|
|
return
|
|
}
|
|
if cached == nil {
|
|
var ok bool
|
|
if cached, ok = h.fetchLFSObject(w, r, repo, object); !ok {
|
|
return
|
|
}
|
|
}
|
|
serveGenericArtifact(w, r, cached)
|
|
}
|
|
|
|
// fetchLFSObject caches an object the cache doesn't have. When it can't, or
|
|
// the request is a HEAD, it answers the request and returns false.
|
|
func (h *GitHandler) fetchLFSObject(w http.ResponseWriter, r *http.Request, repo gitRepository, object lfsObject) (*CacheResult, bool) {
|
|
download, kept := h.lfsDownloads.get(repo.key, object, time.Now())
|
|
if !kept {
|
|
var ok bool
|
|
if download, ok = h.lfsFreshDownload(w, r, repo, object); !ok {
|
|
return nil, false
|
|
}
|
|
}
|
|
if r.Method == http.MethodHead {
|
|
// The upstream has the object, and a HEAD needs no more than that:
|
|
// fetching it is left to a GET.
|
|
w.Header().Set("Content-Type", gitLFSObjectMediaType)
|
|
w.Header().Set("Content-Length", strconv.FormatInt(object.Size, 10))
|
|
return nil, false
|
|
}
|
|
h.lfsDownloads.drop(repo.key, object.OID)
|
|
cached, err := h.cacheLFSObject(r.Context(), repo, object, download)
|
|
if err != nil && kept && retryLFSDownload(r.Context(), err) {
|
|
// The kept URL may have expired early or been revoked.
|
|
var ok bool
|
|
if download, ok = h.lfsFreshDownload(w, r, repo, object); !ok {
|
|
return nil, false
|
|
}
|
|
cached, err = h.cacheLFSObject(r.Context(), repo, object, download)
|
|
}
|
|
if err != nil {
|
|
h.proxy.serveArtifactError(w, err, "failed to fetch LFS object")
|
|
return nil, false
|
|
}
|
|
return cached, true
|
|
}
|
|
|
|
// lfsFreshDownload asks the upstream batch API for object's download. When
|
|
// there is none it answers the request and returns false.
|
|
func (h *GitHandler) lfsFreshDownload(w http.ResponseWriter, r *http.Request, repo gitRepository, object lfsObject) (lfsAction, bool) {
|
|
upstream, err := h.lfsBatch(r.Context(), repo, nil, []lfsObject{object})
|
|
if err != nil {
|
|
h.serveLFSUpstreamError(w, r, repo, err)
|
|
return lfsAction{}, false
|
|
}
|
|
download, status, msg := lfsDownload(upstream, object)
|
|
if status != 0 {
|
|
lfsError(w, status, msg)
|
|
return lfsAction{}, false
|
|
}
|
|
return download, true
|
|
}
|
|
|
|
// cacheLFSObject caches object, which the upstream offers as download. A
|
|
// copy already stored under another name, such as by a fork of the
|
|
// repository, saves the download.
|
|
func (h *GitHandler) cacheLFSObject(ctx context.Context, repo gitRepository, object lfsObject, download lfsAction) (*CacheResult, error) {
|
|
header := make(http.Header, len(download.Header))
|
|
for key, value := range download.Header {
|
|
header.Set(key, value)
|
|
}
|
|
// Download URLs are often signed, granting access to the object until
|
|
// they expire, so only the URL without its query is logged and
|
|
// recorded. The object must have the size the client asked for.
|
|
ctx, recorded := withSignedDownload(ctx, download.Href, object.Size)
|
|
if !h.proxy.StreamArtifacts {
|
|
stored, err := h.proxy.DB.FindStoredContent(object.OID)
|
|
if err != nil {
|
|
h.proxy.Logger.Warn("git: looking up a stored copy of an LFS object failed", "oid", object.OID, "error", err)
|
|
}
|
|
if stored != nil && stored.Size == object.Size {
|
|
cached, err := h.proxy.cacheStoredCopy(ctx, gitEcosystem, repo.lfsName(), object.OID, object.OID,
|
|
recorded, object.OID, stored)
|
|
if err == nil {
|
|
return cached, nil
|
|
}
|
|
h.proxy.Logger.Warn("git: copying a stored LFS object failed, downloading it", "oid", object.OID, "error", err)
|
|
}
|
|
}
|
|
return h.proxy.getOrFetchArtifactFromURL(ctx, gitEcosystem, repo.lfsName(), object.OID, object.OID,
|
|
recorded, header, object.OID, "")
|
|
}
|
|
|
|
// retryLFSDownload reports whether a download from a kept URL failed in a way
|
|
// a fresh URL might not.
|
|
func retryLFSDownload(ctx context.Context, err error) bool {
|
|
return ctx.Err() == nil &&
|
|
!errors.Is(err, ErrArtifactDigestMismatch) &&
|
|
!errors.Is(err, errDownloadSize) &&
|
|
!errors.Is(err, ErrVersionDenied) &&
|
|
!errors.Is(err, ErrArtifactBlocked)
|
|
}
|
|
|
|
// lfsDownload returns the download action the upstream gave for object, or
|
|
// the status and message to answer with when it gave none.
|
|
func lfsDownload(resp *lfsBatchResponse, object lfsObject) (lfsAction, int, string) {
|
|
for _, got := range resp.Objects {
|
|
if got.OID != object.OID {
|
|
continue
|
|
}
|
|
if got.Error != nil {
|
|
if got.Error.Code == http.StatusNotFound || got.Error.Code == http.StatusGone {
|
|
return lfsAction{}, http.StatusNotFound, "object not found"
|
|
}
|
|
return lfsAction{}, http.StatusBadGateway, "upstream refused the object"
|
|
}
|
|
download, ok := got.Actions["download"]
|
|
if !ok || !validLFSHref(download.Href) {
|
|
return lfsAction{}, http.StatusBadGateway, "upstream gave no download for the object"
|
|
}
|
|
return download, 0, ""
|
|
}
|
|
return lfsAction{}, http.StatusNotFound, "object not found"
|
|
}
|
|
|
|
// validLFSHref reports whether href is an absolute HTTP(S) URL.
|
|
func validLFSHref(href string) bool {
|
|
u, err := url.Parse(href)
|
|
return err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != ""
|
|
}
|
|
|
|
// lfsBatch asks the upstream's LFS batch API for download actions for
|
|
// objects. upstream.auth applies to it as to the repository.
|
|
func (h *GitHandler) lfsBatch(ctx context.Context, repo gitRepository, ref json.RawMessage, objects []lfsObject) (*lfsBatchResponse, error) {
|
|
body, err := json.Marshal(lfsBatchRequest{
|
|
Operation: "download",
|
|
Transfers: []string{gitLFSTransfer},
|
|
Ref: ref,
|
|
Objects: objects,
|
|
HashAlgo: gitLFSHashAlgo,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
batchURL := strings.TrimSuffix(repo.url, ".git") + ".git" + gitLFSPath + "objects/batch"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, batchURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", gitLFSMediaType)
|
|
req.Header.Set("Content-Type", gitLFSMediaType)
|
|
resp, err := h.proxy.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, &lfsUpstreamError{status: resp.StatusCode}
|
|
}
|
|
var batch lfsBatchResponse
|
|
if err := json.NewDecoder(io.LimitReader(resp.Body, gitLFSMaxBatchResponse)).Decode(&batch); err != nil {
|
|
return nil, fmt.Errorf("reading LFS batch response: %w", err)
|
|
}
|
|
if batch.Transfer != "" && batch.Transfer != gitLFSTransfer {
|
|
return nil, fmt.Errorf("LFS batch response uses the %q transfer", batch.Transfer)
|
|
}
|
|
return &batch, nil
|
|
}
|
|
|
|
// serveLFSUpstreamError answers a request whose upstream batch request
|
|
// failed.
|
|
func (h *GitHandler) serveLFSUpstreamError(w http.ResponseWriter, r *http.Request, repo gitRepository, err error) {
|
|
if r.Context().Err() != nil {
|
|
return
|
|
}
|
|
h.proxy.Logger.Warn("git: LFS batch request failed", "repository", repo.key, "error", err)
|
|
var upstream *lfsUpstreamError
|
|
switch {
|
|
case errors.As(err, &upstream) && upstream.status == http.StatusNotFound:
|
|
lfsError(w, http.StatusNotFound, "repository not found")
|
|
case errors.As(err, &upstream) && upstream.status < http.StatusInternalServerError:
|
|
lfsError(w, http.StatusBadGateway, "upstream refused the request")
|
|
default:
|
|
lfsError(w, http.StatusBadGateway, "upstream unavailable")
|
|
}
|
|
}
|
|
|
|
// lfsName names repo's LFS objects in the artifact cache. Objects are cached
|
|
// per repository: having one cached for another repository says nothing
|
|
// about whether this one has it.
|
|
func (repo gitRepository) lfsName() string {
|
|
return repo.name + "/" + repo.repo
|
|
}
|
|
|
|
// lfsObjectURL is where this proxy serves an object of repo.
|
|
func (h *GitHandler) lfsObjectURL(repo gitRepository, oid string, size int64) string {
|
|
return fmt.Sprintf("%s/git/%s/%s.git%sobjects/%s/%d",
|
|
h.baseURL, url.PathEscape(repo.name), repo.escapedRepo, gitLFSPath, oid, size)
|
|
}
|
|
|
|
// validLFSOID reports whether oid is a sha256 object ID in lowercase hex.
|
|
func validLFSOID(oid string) bool {
|
|
if len(oid) != gitLFSOIDLength {
|
|
return false
|
|
}
|
|
for _, c := range oid {
|
|
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// lfsError answers with a Git LFS error document, which git-lfs shows.
|
|
func lfsError(w http.ResponseWriter, status int, msg string) {
|
|
w.Header().Set("Content-Type", gitLFSMediaType)
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"message": msg})
|
|
}
|
|
|
|
const (
|
|
// gitLFSKeptDownloads bounds how many download URLs from batch responses
|
|
// are kept for the GETs that follow them.
|
|
gitLFSKeptDownloads = 4096
|
|
// gitLFSKeptTTL is how long a download URL is kept when the upstream
|
|
// gives no expiry, and gitLFSKeptMaxTTL how long at most.
|
|
gitLFSKeptTTL = 5 * time.Minute
|
|
gitLFSKeptMaxTTL = time.Hour
|
|
// gitLFSExpiryMargin is how long before its expiry a URL is no longer
|
|
// used, so a download started with it can finish.
|
|
gitLFSExpiryMargin = 30 * time.Second
|
|
)
|
|
|
|
// lfsDownloads keeps the download actions of recent batch responses, so the
|
|
// GET for an object a client just got in a batch needs no second batch
|
|
// request. They stay in memory, never reach a client and expire with the
|
|
// URLs.
|
|
type lfsDownloads struct {
|
|
mu sync.Mutex
|
|
entries map[lfsDownloadKey]lfsKeptDownload
|
|
}
|
|
|
|
type lfsDownloadKey struct {
|
|
repo, oid string
|
|
}
|
|
|
|
type lfsKeptDownload struct {
|
|
size int64
|
|
action lfsAction
|
|
expires time.Time
|
|
}
|
|
|
|
// put keeps action for object of repo until shortly before it expires.
|
|
func (d *lfsDownloads) put(repo string, object lfsObject, action lfsAction, now time.Time) {
|
|
if !validLFSHref(action.Href) {
|
|
return
|
|
}
|
|
expires := now.Add(gitLFSKeptTTL)
|
|
if action.ExpiresIn > 0 || action.ExpiresAt != "" {
|
|
expires = now.Add(gitLFSKeptMaxTTL)
|
|
}
|
|
if action.ExpiresIn > 0 {
|
|
expires = minTime(expires, now.Add(time.Duration(action.ExpiresIn)*time.Second))
|
|
}
|
|
if at, err := time.Parse(time.RFC3339, action.ExpiresAt); err == nil {
|
|
expires = minTime(expires, at)
|
|
}
|
|
expires = expires.Add(-gitLFSExpiryMargin)
|
|
if !expires.After(now) {
|
|
return
|
|
}
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if d.entries == nil {
|
|
d.entries = make(map[lfsDownloadKey]lfsKeptDownload)
|
|
}
|
|
if len(d.entries) >= gitLFSKeptDownloads {
|
|
for key, kept := range d.entries {
|
|
if !kept.expires.After(now) {
|
|
delete(d.entries, key)
|
|
}
|
|
}
|
|
}
|
|
if len(d.entries) >= gitLFSKeptDownloads {
|
|
// Still full: make room by dropping any one.
|
|
for key := range d.entries {
|
|
delete(d.entries, key)
|
|
break
|
|
}
|
|
}
|
|
d.entries[lfsDownloadKey{repo, object.OID}] = lfsKeptDownload{size: object.Size, action: action, expires: expires}
|
|
}
|
|
|
|
// get returns the action kept for object of repo, if it hasn't expired.
|
|
func (d *lfsDownloads) get(repo string, object lfsObject, now time.Time) (lfsAction, bool) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
kept, ok := d.entries[lfsDownloadKey{repo, object.OID}]
|
|
if !ok || kept.size != object.Size || !kept.expires.After(now) {
|
|
return lfsAction{}, false
|
|
}
|
|
return kept.action, true
|
|
}
|
|
|
|
// drop forgets the action kept for oid of repo, once a GET has used it.
|
|
func (d *lfsDownloads) drop(repo, oid string) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
delete(d.entries, lfsDownloadKey{repo, oid})
|
|
}
|
|
|
|
func minTime(a, b time.Time) time.Time {
|
|
if b.Before(a) {
|
|
return b
|
|
}
|
|
return a
|
|
}
|