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/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, 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, 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) (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); err != nil {
263			return 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 err
270		}
271
272		lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
273		return err
274	}); err != nil {
275		return nil, err
276	}
277
278	lock.backend = l
279
280	return &lock, nil
281}
282
283// FromID implements transfer.LockBackend.
284func (l *lfsLockBackend) FromID(id string) (transfer.Lock, error) {
285	var lock LFSLock
286	user, ok := l.ctx.Value(proto.ContextKeyUser).(proto.User)
287	if !ok || user == nil {
288		return nil, errors.New("no user in context")
289	}
290
291	if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
292		var err error
293		lock.lock, err = l.store.GetLFSLockForUserByID(l.ctx, tx, user.ID(), id)
294		if err != nil {
295			return err
296		}
297
298		lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
299		return err
300	}); err != nil {
301		return nil, err
302	}
303
304	lock.backend = l
305
306	return &lock, nil
307}
308
309// FromPath implements transfer.LockBackend.
310func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) {
311	var lock LFSLock
312
313	if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
314		var err error
315		lock.lock, err = l.store.GetLFSLockForUserPath(l.ctx, tx, l.repo.ID(), l.user.ID(), path)
316		if err != nil {
317			return err
318		}
319
320		lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
321		return err
322	}); err != nil {
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(fn func(transfer.Lock) error) error {
333	var locks []*LFSLock
334
335	if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
336		mlocks, err := l.store.GetLFSLocks(l.ctx, tx, l.repo.ID())
337		if err != nil {
338			return err
339		}
340
341		users := make(map[int64]models.User, 0)
342		for _, mlock := range mlocks {
343			owner, ok := users[mlock.UserID]
344			if !ok {
345				owner, err = l.store.GetUserByID(l.ctx, tx, mlock.UserID)
346				if err != nil {
347					return err
348				}
349
350				users[mlock.UserID] = owner
351			}
352
353			locks = append(locks, &LFSLock{lock: mlock, owner: owner, backend: l})
354		}
355
356		return nil
357	}); err != nil {
358		return err
359	}
360
361	for _, lock := range locks {
362		if err := fn(lock); err != nil {
363			return err
364		}
365	}
366
367	return nil
368}
369
370// Unlock implements transfer.LockBackend.
371func (l *lfsLockBackend) Unlock(lock transfer.Lock) error {
372	return l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
373		return l.store.DeleteLFSLockForUserByID(l.ctx, tx, l.user.ID(), lock.ID())
374	})
375}
376
377// LFSLock is a Git LFS lock object.
378// It implements transfer.Lock.
379type LFSLock struct {
380	lock    models.LFSLock
381	owner   models.User
382	backend *lfsLockBackend
383}
384
385var _ transfer.Lock = (*LFSLock)(nil)
386
387// AsArguments implements transfer.Lock.
388func (l *LFSLock) AsArguments() []string {
389	return []string{
390		fmt.Sprintf("id=%s", l.ID()),
391		fmt.Sprintf("path=%s", l.Path()),
392		fmt.Sprintf("locked-at=%s", l.FormattedTimestamp()),
393		fmt.Sprintf("ownername=%s", l.OwnerName()),
394	}
395}
396
397// AsLockSpec implements transfer.Lock.
398func (l *LFSLock) AsLockSpec(ownerID bool) ([]string, error) {
399	id := l.ID()
400	spec := []string{
401		fmt.Sprintf("lock %s", id),
402		fmt.Sprintf("path %s %s", id, l.Path()),
403		fmt.Sprintf("locked-at %s %s", id, l.FormattedTimestamp()),
404		fmt.Sprintf("ownername %s %s", id, l.OwnerName()),
405	}
406
407	if ownerID {
408		who := "theirs"
409		if l.lock.UserID == l.owner.ID {
410			who = "ours"
411		}
412
413		spec = append(spec, fmt.Sprintf("owner %s %s", id, who))
414	}
415
416	return spec, nil
417}
418
419// FormattedTimestamp implements transfer.Lock.
420func (l *LFSLock) FormattedTimestamp() string {
421	return l.lock.CreatedAt.Format(time.RFC3339)
422}
423
424// ID implements transfer.Lock.
425func (l *LFSLock) ID() string {
426	return strconv.FormatInt(l.lock.ID, 10)
427}
428
429// OwnerName implements transfer.Lock.
430func (l *LFSLock) OwnerName() string {
431	return l.owner.Username
432}
433
434// Path implements transfer.Lock.
435func (l *LFSLock) Path() string {
436	return l.lock.Path
437}
438
439// Unlock implements transfer.Lock.
440func (l *LFSLock) Unlock() error {
441	return l.backend.Unlock(l)
442}