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