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