1package git
2
3import (
4 "context"
5 "crypto/rand"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "path"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "time"
15
16 "github.com/charmbracelet/git-lfs-transfer/transfer"
17 "github.com/charmbracelet/log"
18 "github.com/charmbracelet/soft-serve/pkg/config"
19 "github.com/charmbracelet/soft-serve/pkg/db"
20 "github.com/charmbracelet/soft-serve/pkg/db/models"
21 "github.com/charmbracelet/soft-serve/pkg/lfs"
22 "github.com/charmbracelet/soft-serve/pkg/proto"
23 "github.com/charmbracelet/soft-serve/pkg/storage"
24 "github.com/charmbracelet/soft-serve/pkg/store"
25 "github.com/rubyist/tracerx"
26)
27
28func init() {
29 // git-lfs-transfer uses tracerx for logging.
30 // use a custom key to avoid conflicts
31 // SOFT_SERVE_TRACE=1 to enable tracing git-lfs-transfer in soft-serve
32 tracerx.DefaultKey = "SOFT_SERVE"
33 tracerx.Prefix = "trace soft-serve-lfs-transfer: "
34}
35
36// lfsTransfer implements transfer.Backend.
37type lfsTransfer struct {
38 ctx context.Context
39 cfg *config.Config
40 dbx *db.DB
41 store store.Store
42 logger *log.Logger
43 storage storage.Storage
44 repo proto.Repository
45}
46
47var _ transfer.Backend = &lfsTransfer{}
48
49// LFSTransfer is a Git LFS transfer service handler.
50// ctx is expected to have proto.User, *backend.Backend, *log.Logger,
51// *config.Config, *db.DB, and store.Store.
52// The first arg in cmd.Args should be the repo path.
53// The second arg in cmd.Args should be the LFS operation (download or upload).
54func LFSTransfer(ctx context.Context, cmd ServiceCommand) error {
55 if len(cmd.Args) < 2 {
56 return errors.New("missing args")
57 }
58
59 op := cmd.Args[1]
60 if op != lfs.OperationDownload && op != lfs.OperationUpload {
61 return errors.New("invalid operation")
62 }
63
64 logger := log.FromContext(ctx).WithPrefix("lfs-transfer")
65 handler := transfer.NewPktline(cmd.Stdin, cmd.Stdout)
66 repo := proto.RepositoryFromContext(ctx)
67 if repo == nil {
68 logger.Error("no repository in context")
69 return proto.ErrRepoNotFound
70 }
71
72 // Advertise capabilities.
73 for _, cap := range []string{
74 "version=1",
75 "locking",
76 } {
77 if err := handler.WritePacketText(cap); err != nil {
78 logger.Errorf("error sending capability: %s: %v", cap, err)
79 return err
80 }
81 }
82
83 if err := handler.WriteFlush(); err != nil {
84 logger.Error("error sending flush", "err", err)
85 return err
86 }
87
88 repoID := strconv.FormatInt(repo.ID(), 10)
89 cfg := config.FromContext(ctx)
90 processor := transfer.NewProcessor(handler, &lfsTransfer{
91 ctx: ctx,
92 cfg: cfg,
93 dbx: db.FromContext(ctx),
94 store: store.FromContext(ctx),
95 logger: logger,
96 storage: storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)),
97 repo: repo,
98 })
99
100 return processor.ProcessCommands(op)
101}
102
103// Batch implements transfer.Backend.
104func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer, _ map[string]string) ([]transfer.BatchItem, error) {
105 items := make([]transfer.BatchItem, 0)
106 for _, p := range pointers {
107 obj, err := t.store.GetLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), p.Oid)
108 if err != nil && !errors.Is(err, db.ErrRecordNotFound) {
109 return items, db.WrapError(err)
110 }
111
112 exist, err := t.storage.Exists(path.Join("objects", p.RelativePath()))
113 if err != nil {
114 return items, err
115 }
116
117 if exist && obj.ID == 0 {
118 if err := t.store.CreateLFSObject(t.ctx, t.dbx, t.repo.ID(), p.Oid, p.Size); err != nil {
119 return items, db.WrapError(err)
120 }
121 }
122
123 item := transfer.BatchItem{
124 Pointer: p,
125 Present: exist,
126 }
127 items = append(items, item)
128 }
129
130 return items, nil
131}
132
133// Download implements transfer.Backend.
134func (t *lfsTransfer) Download(oid string, _ map[string]string) (fs.File, error) {
135 cfg := config.FromContext(t.ctx)
136 repoID := strconv.FormatInt(t.repo.ID(), 10)
137 strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
138 pointer := transfer.Pointer{Oid: oid}
139 return strg.Open(path.Join("objects", pointer.RelativePath()))
140}
141
142type uploadObject struct {
143 oid string
144 size int64
145 object storage.Object
146}
147
148// StartUpload implements transfer.Backend.
149func (t *lfsTransfer) StartUpload(oid string, r io.Reader, _ map[string]string) (interface{}, error) {
150 if r == nil {
151 return nil, fmt.Errorf("no reader: %w", transfer.ErrMissingData)
152 }
153
154 tempDir := "incomplete"
155 randBytes := make([]byte, 12)
156 if _, err := rand.Read(randBytes); err != nil {
157 return nil, err
158 }
159
160 tempName := fmt.Sprintf("%s%x", oid, randBytes)
161 tempName = path.Join(tempDir, tempName)
162
163 written, err := t.storage.Put(tempName, r)
164 if err != nil {
165 t.logger.Errorf("error putting object: %v", err)
166 return nil, err
167 }
168
169 obj, err := t.storage.Open(tempName)
170 if err != nil {
171 t.logger.Errorf("error opening object: %v", err)
172 return nil, err
173 }
174
175 t.logger.Infof("Object name: %s", obj.Name())
176
177 return uploadObject{
178 oid: oid,
179 size: written,
180 object: obj,
181 }, nil
182}
183
184// FinishUpload implements transfer.Backend.
185func (t *lfsTransfer) FinishUpload(state interface{}, args map[string]string) error {
186 upl, ok := state.(uploadObject)
187 if !ok {
188 return errors.New("invalid state")
189 }
190
191 var size int64
192 for _, arg := range args {
193 if strings.HasPrefix(arg, "size=") {
194 size, _ = strconv.ParseInt(strings.TrimPrefix(arg, "size="), 10, 64)
195 break
196 }
197 }
198
199 pointer := transfer.Pointer{
200 Oid: upl.oid,
201 }
202 if size > 0 {
203 pointer.Size = size
204 } else {
205 pointer.Size = upl.size
206 }
207
208 if err := t.store.CreateLFSObject(t.ctx, t.dbx, t.repo.ID(), pointer.Oid, pointer.Size); err != nil {
209 return db.WrapError(err)
210 }
211
212 expectedPath := path.Join("objects", pointer.RelativePath())
213 if err := t.storage.Rename(upl.object.Name(), expectedPath); err != nil {
214 t.logger.Errorf("error renaming object: %v", err)
215 _ = t.store.DeleteLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), pointer.Oid)
216 return err
217 }
218
219 return nil
220}
221
222// Verify implements transfer.Backend.
223func (t *lfsTransfer) Verify(oid string, args map[string]string) (transfer.Status, error) {
224 var expectedSize int64
225 var err error
226 size, ok := args[transfer.SizeKey]
227 if !ok {
228 return transfer.NewFailureStatus(transfer.StatusBadRequest, "missing size"), nil
229 }
230
231 expectedSize, err = strconv.ParseInt(size, 10, 64)
232 if err != nil {
233 t.logger.Errorf("invalid size argument: %v", err)
234 return transfer.NewFailureStatus(transfer.StatusBadRequest, "invalid size argument"), nil
235 }
236
237 obj, err := t.store.GetLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), oid)
238 if err != nil {
239 if errors.Is(err, db.ErrRecordNotFound) {
240 return transfer.NewFailureStatus(transfer.StatusNotFound, "object not found"), nil
241 }
242 t.logger.Errorf("error getting object: %v", err)
243 return nil, err
244 }
245
246 if obj.Size != expectedSize {
247 t.logger.Errorf("size mismatch: %d != %d", obj.Size, expectedSize)
248 return transfer.NewFailureStatus(transfer.StatusConflict, "size mismatch"), nil
249 }
250
251 return transfer.SuccessStatus(), nil
252}
253
254type lfsLockBackend struct {
255 *lfsTransfer
256 args map[string]string
257 user proto.User
258}
259
260var _ transfer.LockBackend = (*lfsLockBackend)(nil)
261
262// LockBackend implements transfer.Backend.
263func (t *lfsTransfer) LockBackend(args map[string]string) transfer.LockBackend {
264 user := proto.UserFromContext(t.ctx)
265 if user == nil {
266 t.logger.Errorf("no user in context while creating lock backend, repo %s", t.repo.Name())
267 return nil
268 }
269
270 return &lfsLockBackend{t, args, user}
271}
272
273// Create implements transfer.LockBackend.
274func (l *lfsLockBackend) Create(path string, refname string) (transfer.Lock, error) {
275 var lock LFSLock
276 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
277 if err := l.store.CreateLFSLockForUser(l.ctx, tx, l.repo.ID(), l.user.ID(), path, refname); err != nil {
278 return db.WrapError(err)
279 }
280
281 var err error
282 lock.lock, err = l.store.GetLFSLockForUserPath(l.ctx, tx, l.repo.ID(), l.user.ID(), path)
283 if err != nil {
284 return db.WrapError(err)
285 }
286
287 lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
288 return db.WrapError(err)
289 }); err != nil {
290 // Return conflict (409) if the lock already exists.
291 if errors.Is(err, db.ErrDuplicateKey) {
292 return nil, transfer.ErrConflict
293 }
294 l.logger.Errorf("error creating lock: %v", err)
295 return nil, err
296 }
297
298 lock.backend = l
299
300 return &lock, nil
301}
302
303// FromID implements transfer.LockBackend.
304func (l *lfsLockBackend) FromID(id string) (transfer.Lock, error) {
305 var lock LFSLock
306 iid, err := strconv.ParseInt(id, 10, 64)
307 if err != nil {
308 return nil, err
309 }
310
311 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
312 var err error
313 lock.lock, err = l.store.GetLFSLockForUserByID(l.ctx, tx, l.repo.ID(), l.user.ID(), iid)
314 if err != nil {
315 return db.WrapError(err)
316 }
317
318 lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
319 return db.WrapError(err)
320 }); err != nil {
321 if errors.Is(err, db.ErrRecordNotFound) {
322 return nil, transfer.ErrNotFound
323 }
324 l.logger.Errorf("error getting lock: %v", err)
325 return nil, err
326 }
327
328 lock.backend = l
329
330 return &lock, nil
331}
332
333// FromPath implements transfer.LockBackend.
334func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) {
335 var lock LFSLock
336
337 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
338 var err error
339 lock.lock, err = l.store.GetLFSLockForUserPath(l.ctx, tx, l.repo.ID(), l.user.ID(), path)
340 if err != nil {
341 return db.WrapError(err)
342 }
343
344 lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
345 return db.WrapError(err)
346 }); err != nil {
347 if errors.Is(err, db.ErrRecordNotFound) {
348 return nil, transfer.ErrNotFound
349 }
350 l.logger.Errorf("error getting lock: %v", err)
351 return nil, err
352 }
353
354 lock.backend = l
355
356 return &lock, nil
357}
358
359// Range implements transfer.LockBackend.
360func (l *lfsLockBackend) Range(cursor string, limit int, fn func(transfer.Lock) error) (string, error) {
361 var nextCursor string
362 var locks []*LFSLock
363
364 page, _ := strconv.Atoi(cursor)
365 if page <= 0 {
366 page = 1
367 }
368
369 if limit <= 0 {
370 limit = lfs.DefaultLocksLimit
371 } else if limit > 100 {
372 limit = 100
373 }
374
375 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
376 l.logger.Debug("getting locks", "limit", limit, "page", page)
377 mlocks, err := l.store.GetLFSLocks(l.ctx, tx, l.repo.ID(), page, limit)
378 if err != nil {
379 return db.WrapError(err)
380 }
381
382 if len(mlocks) == limit {
383 nextCursor = strconv.Itoa(page + 1)
384 }
385
386 users := make(map[int64]models.User, 0)
387 for _, mlock := range mlocks {
388 owner, ok := users[mlock.UserID]
389 if !ok {
390 owner, err = l.store.GetUserByID(l.ctx, tx, mlock.UserID)
391 if err != nil {
392 return db.WrapError(err)
393 }
394
395 users[mlock.UserID] = owner
396 }
397
398 locks = append(locks, &LFSLock{lock: mlock, owner: owner, backend: l})
399 }
400
401 return nil
402 }); err != nil {
403 return "", err
404 }
405
406 for _, lock := range locks {
407 if err := fn(lock); err != nil {
408 return "", err
409 }
410 }
411
412 return nextCursor, nil
413}
414
415// Unlock implements transfer.LockBackend.
416func (l *lfsLockBackend) Unlock(lock transfer.Lock) error {
417 id, err := strconv.ParseInt(lock.ID(), 10, 64)
418 if err != nil {
419 return err
420 }
421
422 err = l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
423 return db.WrapError(
424 l.store.DeleteLFSLockForUserByID(l.ctx, tx, l.repo.ID(), l.user.ID(), id),
425 )
426 })
427 if err != nil {
428 if errors.Is(err, db.ErrRecordNotFound) {
429 return transfer.ErrNotFound
430 }
431 l.logger.Error("error unlocking lock", "err", err)
432 return err
433 }
434
435 return nil
436}
437
438// LFSLock is a Git LFS lock object.
439// It implements transfer.Lock.
440type LFSLock struct {
441 lock models.LFSLock
442 owner models.User
443 backend *lfsLockBackend
444}
445
446var _ transfer.Lock = (*LFSLock)(nil)
447
448// AsArguments implements transfer.Lock.
449func (l *LFSLock) AsArguments() []string {
450 return []string{
451 fmt.Sprintf("id=%s", l.ID()),
452 fmt.Sprintf("path=%s", l.Path()),
453 fmt.Sprintf("locked-at=%s", l.FormattedTimestamp()),
454 fmt.Sprintf("ownername=%s", l.OwnerName()),
455 }
456}
457
458// AsLockSpec implements transfer.Lock.
459func (l *LFSLock) AsLockSpec(ownerID bool) ([]string, error) {
460 id := l.ID()
461 spec := []string{
462 fmt.Sprintf("lock %s", id),
463 fmt.Sprintf("path %s %s", id, l.Path()),
464 fmt.Sprintf("locked-at %s %s", id, l.FormattedTimestamp()),
465 fmt.Sprintf("ownername %s %s", id, l.OwnerName()),
466 }
467
468 if ownerID {
469 who := "theirs"
470 if l.lock.UserID == l.owner.ID {
471 who = "ours"
472 }
473
474 spec = append(spec, fmt.Sprintf("owner %s %s", id, who))
475 }
476
477 return spec, nil
478}
479
480// FormattedTimestamp implements transfer.Lock.
481func (l *LFSLock) FormattedTimestamp() string {
482 return l.lock.CreatedAt.Format(time.RFC3339)
483}
484
485// ID implements transfer.Lock.
486func (l *LFSLock) ID() string {
487 return strconv.FormatInt(l.lock.ID, 10)
488}
489
490// OwnerName implements transfer.Lock.
491func (l *LFSLock) OwnerName() string {
492 return l.owner.Username
493}
494
495// Path implements transfer.Lock.
496func (l *LFSLock) Path() string {
497 return l.lock.Path
498}
499
500// Unlock implements transfer.Lock.
501func (l *LFSLock) Unlock() error {
502 return l.backend.Unlock(l)
503}