|  | 
|  | 1 | +package backend | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"context" | 
|  | 5 | +	"crypto/sha256" | 
|  | 6 | +	"encoding/hex" | 
|  | 7 | +	"fmt" | 
|  | 8 | +	"io" | 
|  | 9 | + | 
|  | 10 | +	git_model "code.gitea.io/gitea/models/git" | 
|  | 11 | +	repo_model "code.gitea.io/gitea/models/repo" | 
|  | 12 | +	"code.gitea.io/gitea/modules/lfs" | 
|  | 13 | +	"code.gitea.io/gitea/modules/lfs_ssh/transfer" | 
|  | 14 | +) | 
|  | 15 | + | 
|  | 16 | +// Version is the git-lfs-transfer protocol version number. | 
|  | 17 | +const Version = "1" | 
|  | 18 | + | 
|  | 19 | +// Capabilities is a list of Git LFS capabilities supported by this package. | 
|  | 20 | +var Capabilities = []string{ | 
|  | 21 | +	"version=" + Version, | 
|  | 22 | +	// "locking", // no support yet in gitea backend | 
|  | 23 | +} | 
|  | 24 | + | 
|  | 25 | +// GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API | 
|  | 26 | +type GiteaBackend struct { | 
|  | 27 | +	ctx   context.Context | 
|  | 28 | +	repo  *repo_model.Repository | 
|  | 29 | +	store *lfs.ContentStore | 
|  | 30 | +} | 
|  | 31 | + | 
|  | 32 | +var _ transfer.Backend = &GiteaBackend{} | 
|  | 33 | + | 
|  | 34 | +// Batch implements transfer.Backend | 
|  | 35 | +func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, _ transfer.Args) ([]transfer.BatchItem, error) { | 
|  | 36 | +	for i := range pointers { | 
|  | 37 | +		pointers[i].Present = false | 
|  | 38 | +		pointer := lfs.Pointer{Oid: pointers[i].Oid, Size: pointers[i].Size} | 
|  | 39 | +		exists, err := g.store.Verify(pointer) | 
|  | 40 | +		if err != nil || !exists { | 
|  | 41 | +			continue | 
|  | 42 | +		} | 
|  | 43 | +		accessible, err := g.repoHasAccess(pointers[i].Oid) | 
|  | 44 | +		if err != nil || !accessible { | 
|  | 45 | +			continue | 
|  | 46 | +		} | 
|  | 47 | +		pointers[i].Present = true | 
|  | 48 | +	} | 
|  | 49 | +	return pointers, nil | 
|  | 50 | +} | 
|  | 51 | + | 
|  | 52 | +// Download implements transfer.Backend. The returned reader must be closed by the | 
|  | 53 | +// caller. | 
|  | 54 | +func (g *GiteaBackend) Download(oid string, _ transfer.Args) (io.ReadCloser, int64, error) { | 
|  | 55 | +	pointer := lfs.Pointer{Oid: oid} | 
|  | 56 | +	pointer, err := g.store.GetMeta(pointer) | 
|  | 57 | +	if err != nil { | 
|  | 58 | +		return nil, 0, err | 
|  | 59 | +	} | 
|  | 60 | +	obj, err := g.store.Get(pointer) | 
|  | 61 | +	if err != nil { | 
|  | 62 | +		return nil, 0, err | 
|  | 63 | +	} | 
|  | 64 | +	accessible, err := g.repoHasAccess(oid) | 
|  | 65 | +	if err != nil { | 
|  | 66 | +		return nil, 0, err | 
|  | 67 | +	} | 
|  | 68 | +	if !accessible { | 
|  | 69 | +		return nil, 0, fmt.Errorf("LFS Meta Object [%v] not accessible from repo: %v", oid, g.repo.RepoPath()) | 
|  | 70 | +	} | 
|  | 71 | +	return obj, pointer.Size, nil | 
|  | 72 | +} | 
|  | 73 | + | 
|  | 74 | +// StartUpload implements transfer.Backend. | 
|  | 75 | +func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, _ transfer.Args) error { | 
|  | 76 | +	if r == nil { | 
|  | 77 | +		return fmt.Errorf("%w: received null data", transfer.ErrMissingData) | 
|  | 78 | +	} | 
|  | 79 | +	pointer := lfs.Pointer{Oid: oid, Size: size} | 
|  | 80 | +	exists, err := g.store.Verify(pointer) | 
|  | 81 | +	if err != nil { | 
|  | 82 | +		return err | 
|  | 83 | +	} | 
|  | 84 | +	if exists { | 
|  | 85 | +		accessible, err := g.repoHasAccess(oid) | 
|  | 86 | +		if err != nil { | 
|  | 87 | +			return err | 
|  | 88 | +		} | 
|  | 89 | +		if accessible { | 
|  | 90 | +			// we already have this object in the store and metadata | 
|  | 91 | +			return nil | 
|  | 92 | +		} | 
|  | 93 | +		// we have this object in the store but not accessible | 
|  | 94 | +		// so verify hash and size, and add it to metadata | 
|  | 95 | +		hash := sha256.New() | 
|  | 96 | +		written, err := io.Copy(hash, r) | 
|  | 97 | +		if err != nil { | 
|  | 98 | +			return fmt.Errorf("error creating hash: %v", err) | 
|  | 99 | +		} | 
|  | 100 | +		if written != size { | 
|  | 101 | +			return fmt.Errorf("uploaded object [%v] has unexpected size: %v expected != %v received", oid, size, written) | 
|  | 102 | +		} | 
|  | 103 | +		recvOid := hex.EncodeToString(hash.Sum(nil)) != oid | 
|  | 104 | +		if recvOid { | 
|  | 105 | +			return fmt.Errorf("uploaded object [%v] has hash mismatch: %v received", oid, recvOid) | 
|  | 106 | +		} | 
|  | 107 | +	} else { | 
|  | 108 | +		err = g.store.Put(pointer, r) | 
|  | 109 | +		if err != nil { | 
|  | 110 | +			return err | 
|  | 111 | +		} | 
|  | 112 | +	} | 
|  | 113 | +	_, err = git_model.NewLFSMetaObject(g.ctx, g.repo.ID, pointer) | 
|  | 114 | +	if err != nil { | 
|  | 115 | +		return fmt.Errorf("could not create LFS Meta Object: %v", err) | 
|  | 116 | +	} | 
|  | 117 | +	return nil | 
|  | 118 | +} | 
|  | 119 | + | 
|  | 120 | +// Verify implements transfer.Backend. | 
|  | 121 | +func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (transfer.Status, error) { | 
|  | 122 | +	pointer := lfs.Pointer{Oid: oid, Size: size} | 
|  | 123 | +	exists, err := g.store.Verify(pointer) | 
|  | 124 | +	if err != nil { | 
|  | 125 | +		return transfer.NewStatus(transfer.StatusNotFound, err.Error()), err | 
|  | 126 | +	} | 
|  | 127 | +	if !exists { | 
|  | 128 | +		return transfer.NewStatus(transfer.StatusNotFound, "not found"), fmt.Errorf("LFS Meta Object [%v] does not exist", oid) | 
|  | 129 | +	} | 
|  | 130 | +	accessible, err := g.repoHasAccess(oid) | 
|  | 131 | +	if err != nil { | 
|  | 132 | +		return transfer.NewStatus(transfer.StatusNotFound, "not found"), err | 
|  | 133 | +	} | 
|  | 134 | +	if !accessible { | 
|  | 135 | +		return transfer.NewStatus(transfer.StatusNotFound, "not found"), fmt.Errorf("LFS Meta Object [%v] not accessible from repo: %v", oid, g.repo.RepoPath()) | 
|  | 136 | +	} | 
|  | 137 | +	return transfer.SuccessStatus(), nil | 
|  | 138 | +} | 
|  | 139 | + | 
|  | 140 | +// LockBackend implements transfer.Backend. | 
|  | 141 | +func (g *GiteaBackend) LockBackend(_ transfer.Args) transfer.LockBackend { | 
|  | 142 | +	// Gitea doesn't support the locking API | 
|  | 143 | +	// this should never be called as we don't advertise the capability | 
|  | 144 | +	return (transfer.LockBackend)(nil) | 
|  | 145 | +} | 
|  | 146 | + | 
|  | 147 | +// repoHasAccess checks if the repo already has the object with OID stored | 
|  | 148 | +func (g *GiteaBackend) repoHasAccess(oid string) (bool, error) { | 
|  | 149 | +	// check if OID is in global LFS store | 
|  | 150 | +	exists, err := g.store.Exists(lfs.Pointer{Oid: oid}) | 
|  | 151 | +	if err != nil || !exists { | 
|  | 152 | +		return false, err | 
|  | 153 | +	} | 
|  | 154 | +	// check if OID is in repo LFS store | 
|  | 155 | +	metaObj, err := git_model.GetLFSMetaObjectByOid(g.ctx, g.repo.ID, oid) | 
|  | 156 | +	if err != nil || metaObj == nil { | 
|  | 157 | +		return false, err | 
|  | 158 | +	} | 
|  | 159 | +	return true, nil | 
|  | 160 | +} | 
|  | 161 | + | 
|  | 162 | +func New(ctx context.Context, r *repo_model.Repository, s *lfs.ContentStore) transfer.Backend { | 
|  | 163 | +	return &GiteaBackend{ctx: ctx, repo: r, store: s} | 
|  | 164 | +} | 
0 commit comments