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