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