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