lfs.go

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