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