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, 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, 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) (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); err != nil {
263 return 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 err
270 }
271
272 lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
273 return err
274 }); err != nil {
275 return nil, err
276 }
277
278 lock.backend = l
279
280 return &lock, nil
281}
282
283// FromID implements transfer.LockBackend.
284func (l *lfsLockBackend) FromID(id string) (transfer.Lock, error) {
285 var lock LFSLock
286 user, ok := l.ctx.Value(proto.ContextKeyUser).(proto.User)
287 if !ok || user == nil {
288 return nil, errors.New("no user in context")
289 }
290
291 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
292 var err error
293 lock.lock, err = l.store.GetLFSLockForUserByID(l.ctx, tx, user.ID(), id)
294 if err != nil {
295 return err
296 }
297
298 lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
299 return err
300 }); err != nil {
301 return nil, err
302 }
303
304 lock.backend = l
305
306 return &lock, nil
307}
308
309// FromPath implements transfer.LockBackend.
310func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) {
311 var lock LFSLock
312
313 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
314 var err error
315 lock.lock, err = l.store.GetLFSLockForUserPath(l.ctx, tx, l.repo.ID(), l.user.ID(), path)
316 if err != nil {
317 return err
318 }
319
320 lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
321 return err
322 }); err != nil {
323 return nil, err
324 }
325
326 lock.backend = l
327
328 return &lock, nil
329}
330
331// Range implements transfer.LockBackend.
332func (l *lfsLockBackend) Range(fn func(transfer.Lock) error) error {
333 var locks []*LFSLock
334
335 if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
336 mlocks, err := l.store.GetLFSLocks(l.ctx, tx, l.repo.ID())
337 if err != nil {
338 return err
339 }
340
341 users := make(map[int64]models.User, 0)
342 for _, mlock := range mlocks {
343 owner, ok := users[mlock.UserID]
344 if !ok {
345 owner, err = l.store.GetUserByID(l.ctx, tx, mlock.UserID)
346 if err != nil {
347 return err
348 }
349
350 users[mlock.UserID] = owner
351 }
352
353 locks = append(locks, &LFSLock{lock: mlock, owner: owner, backend: l})
354 }
355
356 return nil
357 }); err != nil {
358 return err
359 }
360
361 for _, lock := range locks {
362 if err := fn(lock); err != nil {
363 return err
364 }
365 }
366
367 return nil
368}
369
370// Unlock implements transfer.LockBackend.
371func (l *lfsLockBackend) Unlock(lock transfer.Lock) error {
372 return l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
373 return l.store.DeleteLFSLockForUserByID(l.ctx, tx, l.user.ID(), lock.ID())
374 })
375}
376
377// LFSLock is a Git LFS lock object.
378// It implements transfer.Lock.
379type LFSLock struct {
380 lock models.LFSLock
381 owner models.User
382 backend *lfsLockBackend
383}
384
385var _ transfer.Lock = (*LFSLock)(nil)
386
387// AsArguments implements transfer.Lock.
388func (l *LFSLock) AsArguments() []string {
389 return []string{
390 fmt.Sprintf("id=%s", l.ID()),
391 fmt.Sprintf("path=%s", l.Path()),
392 fmt.Sprintf("locked-at=%s", l.FormattedTimestamp()),
393 fmt.Sprintf("ownername=%s", l.OwnerName()),
394 }
395}
396
397// AsLockSpec implements transfer.Lock.
398func (l *LFSLock) AsLockSpec(ownerID bool) ([]string, error) {
399 id := l.ID()
400 spec := []string{
401 fmt.Sprintf("lock %s", id),
402 fmt.Sprintf("path %s %s", id, l.Path()),
403 fmt.Sprintf("locked-at %s %s", id, l.FormattedTimestamp()),
404 fmt.Sprintf("ownername %s %s", id, l.OwnerName()),
405 }
406
407 if ownerID {
408 who := "theirs"
409 if l.lock.UserID == l.owner.ID {
410 who = "ours"
411 }
412
413 spec = append(spec, fmt.Sprintf("owner %s %s", id, who))
414 }
415
416 return spec, nil
417}
418
419// FormattedTimestamp implements transfer.Lock.
420func (l *LFSLock) FormattedTimestamp() string {
421 return l.lock.CreatedAt.Format(time.RFC3339)
422}
423
424// ID implements transfer.Lock.
425func (l *LFSLock) ID() string {
426 return strconv.FormatInt(l.lock.ID, 10)
427}
428
429// OwnerName implements transfer.Lock.
430func (l *LFSLock) OwnerName() string {
431 return l.owner.Username
432}
433
434// Path implements transfer.Lock.
435func (l *LFSLock) Path() string {
436 return l.lock.Path
437}
438
439// Unlock implements transfer.Lock.
440func (l *LFSLock) Unlock() error {
441 return l.backend.Unlock(l)
442}