Skip to content

Commit 81e8e90

Browse files
modules/lfstransfer: add a backend and runner
Also add handler in runServ() The protocol lib supports locking but the backend does not, as neither does Gitea. Support can be added later and the capability advertised.
1 parent 8492908 commit 81e8e90

File tree

4 files changed

+262
-1
lines changed

4 files changed

+262
-1
lines changed

cmd/serv.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"code.gitea.io/gitea/models/perm"
2323
"code.gitea.io/gitea/modules/git"
2424
"code.gitea.io/gitea/modules/json"
25+
"code.gitea.io/gitea/modules/lfstransfer"
2526
"code.gitea.io/gitea/modules/log"
2627
"code.gitea.io/gitea/modules/pprof"
2728
"code.gitea.io/gitea/modules/private"
@@ -40,6 +41,7 @@ const (
4041
verbUploadArchive = "git-upload-archive"
4142
verbReceivePack = "git-receive-pack"
4243
verbLfsAuthenticate = "git-lfs-authenticate"
44+
verbLfsTransfer = "git-lfs-transfer"
4345
)
4446

4547
// CmdServ represents the available serv sub-command.
@@ -83,9 +85,11 @@ var (
8385
verbUploadArchive: true,
8486
verbReceivePack: true,
8587
verbLfsAuthenticate: true,
88+
verbLfsTransfer: true,
8689
}
8790
allowedCommandsLfs = map[string]bool{
8891
verbLfsAuthenticate: true,
92+
verbLfsTransfer: true,
8993
}
9094
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
9195
)
@@ -138,7 +142,7 @@ func getAccessMode(verb string, lfsVerb string) perm.AccessMode {
138142
return perm.AccessModeRead
139143
case verbReceivePack:
140144
return perm.AccessModeWrite
141-
case verbLfsAuthenticate:
145+
case verbLfsAuthenticate, verbLfsTransfer:
142146
switch lfsVerb {
143147
case "upload":
144148
return perm.AccessModeWrite
@@ -276,6 +280,11 @@ func runServ(c *cli.Context) error {
276280
return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
277281
}
278282

283+
// LFS SSH protocol
284+
if verb == verbLfsTransfer {
285+
return lfstransfer.Main(ctx, repoPath, lfsVerb)
286+
}
287+
279288
// LFS token authentication
280289
if verb == verbLfsAuthenticate {
281290
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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/lfstransfer/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+
}

modules/lfstransfer/logger.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package lfstransfer
2+
3+
import (
4+
"code.gitea.io/gitea/modules/lfstransfer/transfer"
5+
)
6+
7+
// noop logger for passing into transfer
8+
type GiteaLogger struct{}
9+
10+
// Log implements transfer.Logger
11+
func (g *GiteaLogger) Log(msg string, itms ...interface{}) {
12+
}
13+
14+
var _ transfer.Logger = (*GiteaLogger)(nil)
15+
16+
func newLogger() transfer.Logger {
17+
return &GiteaLogger{}
18+
}

modules/lfstransfer/main.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package lfstransfer
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
db_model "code.gitea.io/gitea/models/db"
10+
repo_model "code.gitea.io/gitea/models/repo"
11+
"code.gitea.io/gitea/modules/lfs"
12+
"code.gitea.io/gitea/modules/lfstransfer/backend"
13+
"code.gitea.io/gitea/modules/lfstransfer/transfer"
14+
"code.gitea.io/gitea/modules/log"
15+
"code.gitea.io/gitea/modules/setting"
16+
"code.gitea.io/gitea/modules/storage"
17+
)
18+
19+
func initServices(ctx context.Context) error {
20+
setting.MustInstalled()
21+
setting.LoadDBSetting()
22+
setting.InitSQLLoggersForCli(log.INFO)
23+
if err := db_model.InitEngine(ctx); err != nil {
24+
return fmt.Errorf("unable to initialize the database using configuration [%q]: %w", setting.CustomConf, err)
25+
}
26+
if err := storage.Init(); err != nil {
27+
return fmt.Errorf("unable to initialise storage: %v", err)
28+
}
29+
return nil
30+
}
31+
32+
func getRepo(ctx context.Context, path string) (*repo_model.Repository, error) {
33+
// runServ ensures repoPath is [owner]/[name].git
34+
pathSeg := strings.Split(path, "/")
35+
pathSeg[1] = strings.TrimSuffix(pathSeg[1], ".git")
36+
return repo_model.GetRepositoryByOwnerAndName(ctx, pathSeg[0], pathSeg[1])
37+
}
38+
39+
func Main(ctx context.Context, repoPath string, verb string) error {
40+
if err := initServices(ctx); err != nil {
41+
return err
42+
}
43+
44+
logger := newLogger()
45+
pktline := transfer.NewPktline(os.Stdin, os.Stdout, logger)
46+
repo, err := getRepo(ctx, repoPath)
47+
if err != nil {
48+
return fmt.Errorf("unable to get repository: %s Error: %v", repoPath, err)
49+
}
50+
giteaBackend := backend.New(ctx, repo, lfs.NewContentStore())
51+
52+
for _, cap := range backend.Capabilities {
53+
if err := pktline.WritePacketText(cap); err != nil {
54+
log.Error("error sending capability [%v] due to error: %v", cap, err)
55+
}
56+
}
57+
if err := pktline.WriteFlush(); err != nil {
58+
log.Error("error flushing capabilities: %v", err)
59+
}
60+
p := transfer.NewProcessor(pktline, giteaBackend, logger)
61+
defer log.Info("done processing commands")
62+
switch verb {
63+
case "upload":
64+
return p.ProcessCommands(transfer.UploadOperation)
65+
case "download":
66+
return p.ProcessCommands(transfer.DownloadOperation)
67+
default:
68+
return fmt.Errorf("unknown operation %q", verb)
69+
}
70+
}

0 commit comments

Comments
 (0)