1package git23import (4 "context"5 "crypto/rand"6 "errors"7 "fmt"8 "io"9 "path"10 "path/filepath"11 "strconv"12 "time"1314 "github.com/charmbracelet/git-lfs-transfer/transfer"15 "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)2425// lfsTransfer implements transfer.Backend.26type lfsTransfer struct {27 ctx context.Context28 cfg *config.Config29 dbx *db.DB30 store store.Store31 logger *log.Logger32 storage storage.Storage33 repo proto.Repository34}3536var _ transfer.Backend = &lfsTransfer{}3738// 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 }4748 op := cmd.Args[1]49 if op != lfs.OperationDownload && op != lfs.OperationUpload {50 return errors.New("invalid operation")51 }5253 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.ErrRepoNotFound59 }6061 // 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 err66 }67 }6869 if err := handler.WriteFlush(); err != nil {70 logger.Error("error sending flush", "err", err)71 return err72 }7374 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})8586 return processor.ProcessCommands(op)87}8889// 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 }9697 pointers[i].Present, err = t.storage.Exists(path.Join("objects", pointers[i].RelativePath()))98 if err != nil {99 return pointers, err100 }101102 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 }108109 return pointers, nil110}111112// 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, err121 }122 stat, err := obj.Stat()123 if err != nil {124 return nil, 0, err125 }126 return obj, stat.Size(), nil127}128129// 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 }134135 tempDir := "incomplete"136 randBytes := make([]byte, 12)137 if _, err := rand.Read(randBytes); err != nil {138 return err139 }140141 tempName := fmt.Sprintf("%s%x", oid, randBytes)142 tempName = path.Join(tempDir, tempName)143144 written, err := t.storage.Put(tempName, r)145 if err != nil {146 t.logger.Errorf("error putting object: %v", err)147 return err148 }149150 obj, err := t.storage.Open(tempName)151 if err != nil {152 t.logger.Errorf("error opening object: %v", err)153 return err154 }155156 pointer := transfer.Pointer{157 Oid: oid,158 }159 if size > 0 {160 pointer.Size = size161 } else {162 pointer.Size = written163 }164165 if err := t.store.CreateLFSObject(t.ctx, t.dbx, t.repo.ID(), pointer.Oid, pointer.Size); err != nil {166 return db.WrapError(err)167 }168169 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 err174 }175176 return nil177}178179// 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"), nil185 }186 t.logger.Errorf("error getting object: %v", err)187 return nil, err188 }189190 if obj.Size != size {191 t.logger.Errorf("size mismatch: %d != %d", obj.Size, size)192 return transfer.NewStatus(transfer.StatusConflict, "size mismatch"), nil193 }194195 return transfer.SuccessStatus(), nil196}197198type lfsLockBackend struct {199 *lfsTransfer200 args map[string]string201 user proto.User202}203204var _ transfer.LockBackend = (*lfsLockBackend)(nil)205206// 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 nil212 }213214 return &lfsLockBackend{t, args, user}215}216217// Create implements transfer.LockBackend.218func (l *lfsLockBackend) Create(path string, refname string) (transfer.Lock, error) {219 var lock LFSLock220 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 }224225 var err error226 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 }230231 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.ErrConflict237 }238 l.logger.Errorf("error creating lock: %v", err)239 return nil, err240 }241242 lock.backend = l243244 return &lock, nil245}246247// FromID implements transfer.LockBackend.248func (l *lfsLockBackend) FromID(id string) (transfer.Lock, error) {249 var lock LFSLock250 iid, err := strconv.ParseInt(id, 10, 64)251 if err != nil {252 return nil, err253 }254255 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {256 var err error257 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 }261262 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.ErrNotFound267 }268 l.logger.Errorf("error getting lock: %v", err)269 return nil, err270 }271272 lock.backend = l273274 return &lock, nil275}276277// FromPath implements transfer.LockBackend.278func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) {279 var lock LFSLock280281 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {282 var err error283 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 }287288 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.ErrNotFound293 }294 l.logger.Errorf("error getting lock: %v", err)295 return nil, err296 }297298 lock.backend = l299300 return &lock, nil301}302303// Range implements transfer.LockBackend.304func (l *lfsLockBackend) Range(cursor string, limit int, fn func(transfer.Lock) error) (string, error) {305 var nextCursor string306 var locks []*LFSLock307308 page, _ := strconv.Atoi(cursor)309 if page <= 0 {310 page = 1311 }312313 if limit <= 0 {314 limit = lfs.DefaultLocksLimit315 } else if limit > 100 {316 limit = 100317 }318319 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 }325326 if len(mlocks) == limit {327 nextCursor = strconv.Itoa(page + 1)328 }329330 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 }338339 users[mlock.UserID] = owner340 }341342 locks = append(locks, &LFSLock{lock: mlock, owner: owner, backend: l})343 }344345 return nil346 }); err != nil {347 return "", err348 }349350 for _, lock := range locks {351 if err := fn(lock); err != nil {352 return "", err353 }354 }355356 return nextCursor, nil357}358359// 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 err364 }365366 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.ErrNotFound374 }375 l.logger.Error("error unlocking lock", "err", err)376 return err377 }378379 return nil380}381382// LFSLock is a Git LFS lock object.383// It implements transfer.Lock.384type LFSLock struct {385 lock models.LFSLock386 owner models.User387 backend *lfsLockBackend388}389390var _ transfer.Lock = (*LFSLock)(nil)391392// 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}401402// 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 }411412 if ownerID {413 who := "theirs"414 if l.lock.UserID == l.owner.ID {415 who = "ours"416 }417418 spec = append(spec, fmt.Sprintf("owner %s %s", id, who))419 }420421 return spec, nil422}423424// FormattedTimestamp implements transfer.Lock.425func (l *LFSLock) FormattedTimestamp() string {426 return l.lock.CreatedAt.Format(time.RFC3339)427}428429// ID implements transfer.Lock.430func (l *LFSLock) ID() string {431 return strconv.FormatInt(l.lock.ID, 10)432}433434// OwnerName implements transfer.Lock.435func (l *LFSLock) OwnerName() string {436 return l.owner.Username437}438439// Path implements transfer.Lock.440func (l *LFSLock) Path() string {441 return l.lock.Path442}443444// Unlock implements transfer.Lock.445func (l *LFSLock) Unlock() error {446 return l.backend.Unlock(l)447}