1package git
2
3import (
4 "context"
5 "crypto/rand"
6 "errors"
7 "fmt"
8 "io"
9 "path"
10 "path/filepath"
11 "strconv"
12 "time"
13
14 "github.com/charmbracelet/git-lfs-transfer/transfer"
15 log "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)
24
25// lfsTransfer implements transfer.Backend.
26type lfsTransfer struct {
27 ctx context.Context
28 cfg *config.Config
29 dbx *db.DB
30 store store.Store
31 logger *log.Logger
32 storage storage.Storage
33 repo proto.Repository
34}
35
36var _ transfer.Backend = &lfsTransfer{}
37
38// 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 }
47
48 op := cmd.Args[1]
49 if op != lfs.OperationDownload && op != lfs.OperationUpload {
50 return errors.New("invalid operation")
51 }
52
53 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.ErrRepoNotFound
59 }
60
61 // 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 err
66 }
67 }
68
69 if err := handler.WriteFlush(); err != nil {
70 logger.Error("error sending flush", "err", err)
71 return err
72 }
73
74 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})
85
86 return processor.ProcessCommands(op)
87}
88
89// 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 }
96
97 pointers[i].Present, err = t.storage.Exists(path.Join("objects", pointers[i].RelativePath()))
98 if err != nil {
99 return pointers, err
100 }
101
102 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 }
108
109 return pointers, nil
110}
111
112// 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, err
121 }
122 stat, err := obj.Stat()
123 if err != nil {
124 return nil, 0, err
125 }
126 return obj, stat.Size(), nil
127}
128
129// 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 }
134
135 tempDir := "incomplete"
136 randBytes := make([]byte, 12)
137 if _, err := rand.Read(randBytes); err != nil {
138 return err
139 }
140
141 tempName := fmt.Sprintf("%s%x", oid, randBytes)
142 tempName = path.Join(tempDir, tempName)
143
144 written, err := t.storage.Put(tempName, r)
145 if err != nil {
146 t.logger.Errorf("error putting object: %v", err)
147 return err
148 }
149
150 obj, err := t.storage.Open(tempName)
151 if err != nil {
152 t.logger.Errorf("error opening object: %v", err)
153 return err
154 }
155
156 pointer := transfer.Pointer{
157 Oid: oid,
158 }
159 if size > 0 {
160 pointer.Size = size
161 } else {
162 pointer.Size = written
163 }
164
165 if err := t.store.CreateLFSObject(t.ctx, t.dbx, t.repo.ID(), pointer.Oid, pointer.Size); err != nil {
166 return db.WrapError(err)
167 }
168
169 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 err
174 }
175
176 return nil
177}
178
179// 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"), nil
185 }
186 t.logger.Errorf("error getting object: %v", err)
187 return nil, err
188 }
189
190 if obj.Size != size {
191 t.logger.Errorf("size mismatch: %d != %d", obj.Size, size)
192 return transfer.NewStatus(transfer.StatusConflict, "size mismatch"), nil
193 }
194
195 return transfer.SuccessStatus(), nil
196}
197
198type lfsLockBackend struct {
199 *lfsTransfer
200 args map[string]string
201 user proto.User
202}
203
204var _ transfer.LockBackend = (*lfsLockBackend)(nil)
205
206// 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 nil
212 }
213
214 return &lfsLockBackend{t, args, user}
215}
216
217// Create implements transfer.LockBackend.
218func (l *lfsLockBackend) Create(path string, refname string) (transfer.Lock, error) {
219 var lock LFSLock
220 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 }
224
225 var err error
226 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 }
230
231 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.ErrConflict
237 }
238 l.logger.Errorf("error creating lock: %v", err)
239 return nil, err
240 }
241
242 lock.backend = l
243
244 return &lock, nil
245}
246
247// FromID implements transfer.LockBackend.
248func (l *lfsLockBackend) FromID(id string) (transfer.Lock, error) {
249 var lock LFSLock
250 iid, err := strconv.ParseInt(id, 10, 64)
251 if err != nil {
252 return nil, err
253 }
254
255 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
256 var err error
257 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 }
261
262 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.ErrNotFound
267 }
268 l.logger.Errorf("error getting lock: %v", err)
269 return nil, err
270 }
271
272 lock.backend = l
273
274 return &lock, nil
275}
276
277// FromPath implements transfer.LockBackend.
278func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) {
279 var lock LFSLock
280
281 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
282 var err error
283 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 }
287
288 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.ErrNotFound
293 }
294 l.logger.Errorf("error getting lock: %v", err)
295 return nil, err
296 }
297
298 lock.backend = l
299
300 return &lock, nil
301}
302
303// Range implements transfer.LockBackend.
304func (l *lfsLockBackend) Range(cursor string, limit int, fn func(transfer.Lock) error) (string, error) {
305 var nextCursor string
306 var locks []*LFSLock
307
308 page, _ := strconv.Atoi(cursor)
309 if page <= 0 {
310 page = 1
311 }
312
313 if limit <= 0 {
314 limit = lfs.DefaultLocksLimit
315 } else if limit > 100 {
316 limit = 100
317 }
318
319 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 }
325
326 if len(mlocks) == limit {
327 nextCursor = strconv.Itoa(page + 1)
328 }
329
330 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 }
338
339 users[mlock.UserID] = owner
340 }
341
342 locks = append(locks, &LFSLock{lock: mlock, owner: owner, backend: l})
343 }
344
345 return nil
346 }); err != nil {
347 return "", err
348 }
349
350 for _, lock := range locks {
351 if err := fn(lock); err != nil {
352 return "", err
353 }
354 }
355
356 return nextCursor, nil
357}
358
359// 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 err
364 }
365
366 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.ErrNotFound
374 }
375 l.logger.Error("error unlocking lock", "err", err)
376 return err
377 }
378
379 return nil
380}
381
382// LFSLock is a Git LFS lock object.
383// It implements transfer.Lock.
384type LFSLock struct {
385 lock models.LFSLock
386 owner models.User
387 backend *lfsLockBackend
388}
389
390var _ transfer.Lock = (*LFSLock)(nil)
391
392// 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}
401
402// 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 }
411
412 if ownerID {
413 who := "theirs"
414 if l.lock.UserID == l.owner.ID {
415 who = "ours"
416 }
417
418 spec = append(spec, fmt.Sprintf("owner %s %s", id, who))
419 }
420
421 return spec, nil
422}
423
424// FormattedTimestamp implements transfer.Lock.
425func (l *LFSLock) FormattedTimestamp() string {
426 return l.lock.CreatedAt.Format(time.RFC3339)
427}
428
429// ID implements transfer.Lock.
430func (l *LFSLock) ID() string {
431 return strconv.FormatInt(l.lock.ID, 10)
432}
433
434// OwnerName implements transfer.Lock.
435func (l *LFSLock) OwnerName() string {
436 return l.owner.Username
437}
438
439// Path implements transfer.Lock.
440func (l *LFSLock) Path() string {
441 return l.lock.Path
442}
443
444// Unlock implements transfer.Lock.
445func (l *LFSLock) Unlock() error {
446 return l.backend.Unlock(l)
447}