Skip to content

Commit d506260

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 719f85a commit d506260

File tree

4 files changed

+281
-1
lines changed

4 files changed

+281
-1
lines changed

cmd/serv.go

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

modules/lfstransfer/logger.go

Lines changed: 21 additions & 0 deletions
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

Lines changed: 73 additions & 0 deletions
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)