Skip to content

Commit 3b7acc6

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 3b7acc6

File tree

4 files changed

+271
-1
lines changed

4 files changed

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

modules/lfstransfer/logger.go

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

modules/lfstransfer/main.go

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

0 commit comments

Comments
 (0)