Something went wrong. Try again.
forked from charmbracelet/soft-serve
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475package git
import ( "context" "crypto/rand" "errors" "fmt" "io" "path" "path/filepath" "strconv" "time"
"charm.land/log/v2" "github.com/charmbracelet/git-lfs-transfer/transfer" "github.com/charmbracelet/soft-serve/pkg/config" "github.com/charmbracelet/soft-serve/pkg/db" "github.com/charmbracelet/soft-serve/pkg/db/models" "github.com/charmbracelet/soft-serve/pkg/lfs" "github.com/charmbracelet/soft-serve/pkg/proto" "github.com/charmbracelet/soft-serve/pkg/storage" "github.com/charmbracelet/soft-serve/pkg/store")
// lfsTransfer implements transfer.Backend.type lfsTransfer struct { ctx context.Context cfg *config.Config dbx *db.DB store store.Store logger *log.Logger storage storage.Storage repo proto.Repository}
var _ transfer.Backend = &lfsTransfer{}
// errInvalidOid is returned when a client sends a malformed object ID. Object// IDs are interpolated into storage paths, so anything outside the SHA-256// alphabet is a path traversal attempt.//// It wraps transfer.ErrParseError so the processor answers with a 400 instead// of falling through to a generic internal error, and lfs.ErrInvalidOIDFormat// so callers can still test for the cause.var errInvalidOid = fmt.Errorf("%w: %w", transfer.ErrParseError, lfs.ErrInvalidOIDFormat)
// LFSTransfer is a Git LFS transfer service handler.// ctx is expected to have proto.User, *backend.Backend, *log.Logger,// *config.Config, *db.DB, and store.Store.// The first arg in cmd.Args should be the repo path.// The second arg in cmd.Args should be the LFS operation (download or upload).func LFSTransfer(ctx context.Context, cmd ServiceCommand) error { if len(cmd.Args) < 2 { return errors.New("missing args") }
op := cmd.Args[1] if op != lfs.OperationDownload && op != lfs.OperationUpload { return errors.New("invalid operation") }
logger := log.FromContext(ctx).WithPrefix("lfs-transfer") handler := transfer.NewPktline(cmd.Stdin, cmd.Stdout, &lfsLogger{logger}) repo := proto.RepositoryFromContext(ctx) if repo == nil { logger.Error("no repository in context") return proto.ErrRepoNotFound }
// Advertise capabilities. for _, cap := range transfer.Capabilities { if err := handler.WritePacketText(cap); err != nil { logger.Errorf("error sending capability: %s: %v", cap, err) return err } }
if err := handler.WriteFlush(); err != nil { logger.Error("error sending flush", "err", err) return err }
repoID := strconv.FormatInt(repo.ID(), 10) cfg := config.FromContext(ctx) processor := transfer.NewProcessor(handler, &lfsTransfer{ ctx: ctx, cfg: cfg, dbx: db.FromContext(ctx), store: store.FromContext(ctx), logger: logger, storage: storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)), repo: repo, }, &lfsLogger{logger})
return processor.ProcessCommands(op)}
// Batch implements transfer.Backend.func (t *lfsTransfer) Batch(_ string, pointers []transfer.BatchItem, _ transfer.Args) ([]transfer.BatchItem, error) { for i := range pointers { p := transfer.Pointer{Oid: pointers[i].Oid, Size: pointers[i].Size} if !p.IsValid() { return pointers, errInvalidOid } }
for i := range pointers { obj, err := t.store.GetLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), pointers[i].Oid) if err != nil && !errors.Is(err, db.ErrRecordNotFound) { return pointers, db.WrapError(err) }
pointers[i].Present, err = t.storage.Exists(path.Join("objects", pointers[i].RelativePath())) if err != nil { return pointers, err }
if pointers[i].Present && obj.ID == 0 { if err := t.store.CreateLFSObject(t.ctx, t.dbx, t.repo.ID(), pointers[i].Oid, pointers[i].Size); err != nil { return pointers, db.WrapError(err) } } }
return pointers, nil}
// Download implements transfer.Backend.func (t *lfsTransfer) Download(oid string, _ transfer.Args) (io.ReadCloser, int64, error) { p := transfer.Pointer{Oid: oid} if !p.IsValid() { return nil, 0, errInvalidOid }
cfg := config.FromContext(t.ctx) repoID := strconv.FormatInt(t.repo.ID(), 10) strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)) pointer := transfer.Pointer{Oid: oid} obj, err := strg.Open(path.Join("objects", pointer.RelativePath())) if err != nil { return nil, 0, err } stat, err := obj.Stat() if err != nil { return nil, 0, err } return obj, stat.Size(), nil}
// Upload implements transfer.Backend.func (t *lfsTransfer) Upload(oid string, size int64, r io.Reader, _ transfer.Args) error { p := transfer.Pointer{Oid: oid} if !p.IsValid() { return errInvalidOid }
if r == nil { return fmt.Errorf("no reader: %w", transfer.ErrMissingData) }
tempDir := "incomplete" randBytes := make([]byte, 12) if _, err := rand.Read(randBytes); err != nil { return err }
tempName := fmt.Sprintf("%s%x", oid, randBytes) tempName = path.Join(tempDir, tempName)
written, err := t.storage.Put(tempName, r) if err != nil { t.logger.Errorf("error putting object: %v", err) return err }
pointer := transfer.Pointer{ Oid: oid, } if size > 0 { pointer.Size = size } else { pointer.Size = written }
if err := t.store.CreateLFSObject(t.ctx, t.dbx, t.repo.ID(), pointer.Oid, pointer.Size); err != nil { return db.WrapError(err) }
// Rename takes names relative to the storage root, not the absolute path // the temp file happens to live at. expectedPath := path.Join("objects", pointer.RelativePath()) if err := t.storage.Rename(tempName, expectedPath); err != nil { t.logger.Errorf("error renaming object: %v", err) _ = t.store.DeleteLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), pointer.Oid) return err }
return nil}
// Verify implements transfer.Backend.func (t *lfsTransfer) Verify(oid string, size int64, _ transfer.Args) (transfer.Status, error) { p := transfer.Pointer{Oid: oid} if !p.IsValid() { return transfer.NewStatus(transfer.StatusConflict, "invalid OID format"), nil }
obj, err := t.store.GetLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), oid) if err != nil { if errors.Is(err, db.ErrRecordNotFound) { return transfer.NewStatus(transfer.StatusNotFound, "object not found"), nil } t.logger.Errorf("error getting object: %v", err) return nil, err }
if obj.Size != size { t.logger.Errorf("size mismatch: %d != %d", obj.Size, size) return transfer.NewStatus(transfer.StatusConflict, "size mismatch"), nil }
return transfer.SuccessStatus(), nil}
type lfsLockBackend struct { *lfsTransfer args map[string]string user proto.User}
var _ transfer.LockBackend = (*lfsLockBackend)(nil)
// LockBackend implements transfer.Backend.func (t *lfsTransfer) LockBackend(args transfer.Args) transfer.LockBackend { user := proto.UserFromContext(t.ctx) if user == nil { t.logger.Errorf("no user in context while creating lock backend, repo %s", t.repo.Name()) return nil }
return &lfsLockBackend{t, args, user}}
// Create implements transfer.LockBackend.func (l *lfsLockBackend) Create(path string, refname string) (transfer.Lock, error) { var lock LFSLock if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error { if err := l.store.CreateLFSLockForUser(l.ctx, tx, l.repo.ID(), l.user.ID(), path, refname); err != nil { return db.WrapError(err) }
var err error lock.lock, err = l.store.GetLFSLockForUserPath(l.ctx, tx, l.repo.ID(), l.user.ID(), path) if err != nil { return db.WrapError(err) }
lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID) return db.WrapError(err) }); err != nil { // Return conflict (409) if the lock already exists. if errors.Is(err, db.ErrDuplicateKey) { return nil, transfer.ErrConflict } l.logger.Errorf("error creating lock: %v", err) return nil, err }
lock.backend = l
return &lock, nil}
// FromID implements transfer.LockBackend.func (l *lfsLockBackend) FromID(id string) (transfer.Lock, error) { var lock LFSLock iid, err := strconv.ParseInt(id, 10, 64) if err != nil { return nil, err }
if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error { var err error lock.lock, err = l.store.GetLFSLockForUserByID(l.ctx, tx, l.repo.ID(), l.user.ID(), iid) if err != nil { return db.WrapError(err) }
lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID) return db.WrapError(err) }); err != nil { if errors.Is(err, db.ErrRecordNotFound) { return nil, transfer.ErrNotFound } l.logger.Errorf("error getting lock: %v", err) return nil, err }
lock.backend = l
return &lock, nil}
// FromPath implements transfer.LockBackend.func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) { var lock LFSLock
if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error { var err error lock.lock, err = l.store.GetLFSLockForUserPath(l.ctx, tx, l.repo.ID(), l.user.ID(), path) if err != nil { return db.WrapError(err) }
lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID) return db.WrapError(err) }); err != nil { if errors.Is(err, db.ErrRecordNotFound) { return nil, transfer.ErrNotFound } l.logger.Errorf("error getting lock: %v", err) return nil, err }
lock.backend = l
return &lock, nil}
// Range implements transfer.LockBackend.func (l *lfsLockBackend) Range(cursor string, limit int, fn func(transfer.Lock) error) (string, error) { var nextCursor string var locks []*LFSLock
page, _ := strconv.Atoi(cursor) if page <= 0 { page = 1 }
if limit <= 0 { limit = lfs.DefaultLocksLimit } else if limit > 100 { limit = 100 }
if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error { l.logger.Debug("getting locks", "limit", limit, "page", page) mlocks, err := l.store.GetLFSLocks(l.ctx, tx, l.repo.ID(), page, limit) if err != nil { return db.WrapError(err) }
if len(mlocks) == limit { nextCursor = strconv.Itoa(page + 1) }
users := make(map[int64]models.User, 0) for _, mlock := range mlocks { owner, ok := users[mlock.UserID] if !ok { owner, err = l.store.GetUserByID(l.ctx, tx, mlock.UserID) if err != nil { return db.WrapError(err) }
users[mlock.UserID] = owner }
locks = append(locks, &LFSLock{lock: mlock, owner: owner, backend: l}) }
return nil }); err != nil { return "", err }
for _, lock := range locks { if err := fn(lock); err != nil { return "", err } }
return nextCursor, nil}
// Unlock implements transfer.LockBackend.func (l *lfsLockBackend) Unlock(lock transfer.Lock) error { id, err := strconv.ParseInt(lock.ID(), 10, 64) if err != nil { return err }
err = l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error { return db.WrapError( l.store.DeleteLFSLockForUserByID(l.ctx, tx, l.repo.ID(), l.user.ID(), id), ) }) if err != nil { if errors.Is(err, db.ErrRecordNotFound) { return transfer.ErrNotFound } l.logger.Error("error unlocking lock", "err", err) return err }
return nil}
// LFSLock is a Git LFS lock object.// It implements transfer.Lock.type LFSLock struct { lock models.LFSLock owner models.User backend *lfsLockBackend}
var _ transfer.Lock = (*LFSLock)(nil)
// AsArguments implements transfer.Lock.func (l *LFSLock) AsArguments() []string { return []string{ fmt.Sprintf("id=%s", l.ID()), fmt.Sprintf("path=%s", l.Path()), fmt.Sprintf("locked-at=%s", l.FormattedTimestamp()), fmt.Sprintf("ownername=%s", l.OwnerName()), }}
// AsLockSpec implements transfer.Lock.func (l *LFSLock) AsLockSpec(ownerID bool) ([]string, error) { id := l.ID() spec := []string{ fmt.Sprintf("lock %s", id), fmt.Sprintf("path %s %s", id, l.Path()), fmt.Sprintf("locked-at %s %s", id, l.FormattedTimestamp()), fmt.Sprintf("ownername %s %s", id, l.OwnerName()), }
if ownerID { who := "theirs" if l.lock.UserID == l.owner.ID { who = "ours" }
spec = append(spec, fmt.Sprintf("owner %s %s", id, who)) }
return spec, nil}
// FormattedTimestamp implements transfer.Lock.func (l *LFSLock) FormattedTimestamp() string { return l.lock.CreatedAt.Format(time.RFC3339)}
// ID implements transfer.Lock.func (l *LFSLock) ID() string { return strconv.FormatInt(l.lock.ID, 10)}
// OwnerName implements transfer.Lock.func (l *LFSLock) OwnerName() string { return l.owner.Username}
// Path implements transfer.Lock.func (l *LFSLock) Path() string { return l.lock.Path}
// Unlock implements transfer.Lock.func (l *LFSLock) Unlock() error { return l.backend.Unlock(l)}