lfs.go

  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}