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