Skip to content

Commit f3c77c2

Browse files
modules/lfstransfer: add locking support
1 parent e31f62f commit f3c77c2

File tree

3 files changed

+303
-2
lines changed

3 files changed

+303
-2
lines changed

modules/lfstransfer/backend/backend.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const Version = "1"
2525
// Capabilities is a list of Git LFS capabilities supported by this package.
2626
var Capabilities = []string{
2727
"version=" + Version,
28-
// "locking",
28+
"locking",
2929
}
3030

3131
var _ transfer.Backend = &GiteaBackend{}
@@ -291,5 +291,5 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
291291

292292
// LockBackend implements transfer.Backend.
293293
func (g *GiteaBackend) LockBackend(_ transfer.Args) transfer.LockBackend {
294-
return (transfer.LockBackend)(nil)
294+
return newGiteaLockBackend(g)
295295
}

modules/lfstransfer/backend/lock.go

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package backend
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"net/url"
12+
"strconv"
13+
"time"
14+
15+
"code.gitea.io/gitea/modules/json"
16+
lfslock "code.gitea.io/gitea/modules/structs"
17+
18+
"github.com/charmbracelet/git-lfs-transfer/transfer"
19+
)
20+
21+
var _ transfer.LockBackend = &giteaLockBackend{}
22+
23+
type giteaLockBackend struct {
24+
ctx context.Context
25+
g *GiteaBackend
26+
server *url.URL
27+
token string
28+
logger transfer.Logger
29+
}
30+
31+
func newGiteaLockBackend(g *GiteaBackend) transfer.LockBackend {
32+
server := g.server.JoinPath("locks")
33+
return &giteaLockBackend{ctx: g.ctx, g: g, server: server, token: g.token, logger: g.logger}
34+
}
35+
36+
// Create implements transfer.LockBackend
37+
func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
38+
reqBody := lfslock.LFSLockRequest{Path: path}
39+
40+
bodyBytes, err := json.Marshal(reqBody)
41+
if err != nil {
42+
g.logger.Log("json marshal error", err)
43+
return nil, err
44+
}
45+
url := g.server.String()
46+
headers := map[string]string{
47+
headerAuthorisation: g.token,
48+
headerAccept: mimeGitLFS,
49+
headerContentType: mimeGitLFS,
50+
}
51+
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
52+
resp, err := req.Response()
53+
if err != nil {
54+
g.logger.Log("http request error", err)
55+
return nil, err
56+
}
57+
defer resp.Body.Close()
58+
respBytes, err := io.ReadAll(resp.Body)
59+
if err != nil {
60+
g.logger.Log("http read error", err)
61+
return nil, err
62+
}
63+
if resp.StatusCode != http.StatusCreated {
64+
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
65+
return nil, statusCodeToErr(resp.StatusCode)
66+
}
67+
var respBody lfslock.LFSLockResponse
68+
err = json.Unmarshal(respBytes, &respBody)
69+
if err != nil {
70+
g.logger.Log("json umarshal error", err)
71+
return nil, err
72+
}
73+
74+
if respBody.Lock == nil {
75+
g.logger.Log("api returned nil lock")
76+
return nil, fmt.Errorf("api returned nil lock")
77+
}
78+
respLock := respBody.Lock
79+
owner := userUnknown
80+
if respLock.Owner != nil {
81+
owner = respLock.Owner.Name
82+
}
83+
lock := newGiteaLock(g, respLock.ID, respLock.Path, respLock.LockedAt, owner)
84+
return lock, nil
85+
}
86+
87+
// Unlock implements transfer.LockBackend
88+
func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
89+
reqBody := lfslock.LFSLockDeleteRequest{}
90+
91+
bodyBytes, err := json.Marshal(reqBody)
92+
if err != nil {
93+
g.logger.Log("json marshal error", err)
94+
return err
95+
}
96+
url := g.server.JoinPath(lock.ID(), "unlock").String()
97+
headers := map[string]string{
98+
headerAuthorisation: g.token,
99+
headerAccept: mimeGitLFS,
100+
headerContentType: mimeGitLFS,
101+
}
102+
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
103+
resp, err := req.Response()
104+
if err != nil {
105+
g.logger.Log("http request error", err)
106+
return err
107+
}
108+
defer resp.Body.Close()
109+
if resp.StatusCode != http.StatusOK {
110+
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
111+
return statusCodeToErr(resp.StatusCode)
112+
}
113+
// no need to read response
114+
115+
return nil
116+
}
117+
118+
// FromPath implements transfer.LockBackend
119+
func (g *giteaLockBackend) FromPath(path string) (transfer.Lock, error) {
120+
v := url.Values{
121+
argPath: []string{path},
122+
}
123+
124+
respLocks, _, err := g.queryLocks(v)
125+
if err != nil {
126+
return nil, err
127+
}
128+
129+
if len(respLocks) == 0 {
130+
return nil, transfer.ErrNotFound
131+
}
132+
return respLocks[0], nil
133+
}
134+
135+
// FromID implements transfer.LockBackend
136+
func (g *giteaLockBackend) FromID(id string) (transfer.Lock, error) {
137+
v := url.Values{
138+
argID: []string{id},
139+
}
140+
141+
respLocks, _, err := g.queryLocks(v)
142+
if err != nil {
143+
return nil, err
144+
}
145+
146+
if len(respLocks) == 0 {
147+
return nil, transfer.ErrNotFound
148+
}
149+
return respLocks[0], nil
150+
}
151+
152+
// Range implements transfer.LockBackend
153+
func (g *giteaLockBackend) Range(cursor string, limit int, iter func(transfer.Lock) error) (string, error) {
154+
v := url.Values{
155+
argLimit: []string{strconv.FormatInt(int64(limit), 10)},
156+
}
157+
if cursor != "" {
158+
v[argCursor] = []string{cursor}
159+
}
160+
161+
respLocks, cursor, err := g.queryLocks(v)
162+
if err != nil {
163+
return "", err
164+
}
165+
166+
for _, lock := range respLocks {
167+
err := iter(lock)
168+
if err != nil {
169+
return "", err
170+
}
171+
}
172+
return cursor, nil
173+
}
174+
175+
func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) {
176+
urlq := g.server.JoinPath() // get a copy
177+
urlq.RawQuery = v.Encode()
178+
url := urlq.String()
179+
headers := map[string]string{
180+
headerAuthorisation: g.token,
181+
headerAccept: mimeGitLFS,
182+
headerContentType: mimeGitLFS,
183+
}
184+
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
185+
resp, err := req.Response()
186+
if err != nil {
187+
g.logger.Log("http request error", err)
188+
return nil, "", err
189+
}
190+
defer resp.Body.Close()
191+
respBytes, err := io.ReadAll(resp.Body)
192+
if err != nil {
193+
g.logger.Log("http read error", err)
194+
return nil, "", err
195+
}
196+
if resp.StatusCode != http.StatusOK {
197+
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
198+
return nil, "", statusCodeToErr(resp.StatusCode)
199+
}
200+
var respBody lfslock.LFSLockList
201+
err = json.Unmarshal(respBytes, &respBody)
202+
if err != nil {
203+
g.logger.Log("json umarshal error", err)
204+
return nil, "", err
205+
}
206+
207+
respLocks := make([]transfer.Lock, 0, len(respBody.Locks))
208+
for _, respLock := range respBody.Locks {
209+
owner := userUnknown
210+
if respLock.Owner != nil {
211+
owner = respLock.Owner.Name
212+
}
213+
lock := newGiteaLock(g, respLock.ID, respLock.Path, respLock.LockedAt, owner)
214+
respLocks = append(respLocks, lock)
215+
}
216+
return respLocks, respBody.Next, nil
217+
}
218+
219+
var _ transfer.Lock = &giteaLock{}
220+
221+
type giteaLock struct {
222+
g *giteaLockBackend
223+
id string
224+
path string
225+
lockedAt time.Time
226+
owner string
227+
}
228+
229+
func newGiteaLock(g *giteaLockBackend, id, path string, lockedAt time.Time, owner string) transfer.Lock {
230+
return &giteaLock{g: g, id: id, path: path, lockedAt: lockedAt, owner: owner}
231+
}
232+
233+
// Unlock implements transfer.Lock
234+
func (g *giteaLock) Unlock() error {
235+
return g.g.Unlock(g)
236+
}
237+
238+
// ID implements transfer.Lock
239+
func (g *giteaLock) ID() string {
240+
return g.id
241+
}
242+
243+
// Path implements transfer.Lock
244+
func (g *giteaLock) Path() string {
245+
return g.path
246+
}
247+
248+
// FormattedTimestamp implements transfer.Lock
249+
func (g *giteaLock) FormattedTimestamp() string {
250+
return g.lockedAt.UTC().Format(time.RFC3339)
251+
}
252+
253+
// OwnerName implements transfer.Lock
254+
func (g *giteaLock) OwnerName() string {
255+
return g.owner
256+
}
257+
258+
func (g *giteaLock) CurrentUser() (string, error) {
259+
return userSelf, nil
260+
}
261+
262+
// AsLockSpec implements transfer.Lock
263+
func (g *giteaLock) AsLockSpec(ownerID bool) ([]string, error) {
264+
msgs := []string{
265+
fmt.Sprintf("lock %s", g.ID()),
266+
fmt.Sprintf("path %s %s", g.ID(), g.Path()),
267+
fmt.Sprintf("locked-at %s %s", g.ID(), g.FormattedTimestamp()),
268+
fmt.Sprintf("ownername %s %s", g.ID(), g.OwnerName()),
269+
}
270+
if ownerID {
271+
user, err := g.CurrentUser()
272+
if err != nil {
273+
return nil, fmt.Errorf("error getting current user: %w", err)
274+
}
275+
who := "theirs"
276+
if user == g.OwnerName() {
277+
who = "ours"
278+
}
279+
msgs = append(msgs, fmt.Sprintf("owner %s %s", g.ID(), who))
280+
}
281+
return msgs, nil
282+
}
283+
284+
// AsArguments implements transfer.Lock
285+
func (g *giteaLock) AsArguments() []string {
286+
return []string{
287+
fmt.Sprintf("id=%s", g.ID()),
288+
fmt.Sprintf("path=%s", g.Path()),
289+
fmt.Sprintf("locked-at=%s", g.FormattedTimestamp()),
290+
fmt.Sprintf("ownername=%s", g.OwnerName()),
291+
}
292+
}

modules/lfstransfer/backend/util.go

+9
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,22 @@ const (
4141

4242
// SSH protocol argument keys
4343
const (
44+
argCursor = "cursor"
4445
argExpiresAt = "expires-at"
4546
argID = "id"
47+
argLimit = "limit"
48+
argPath = "path"
4649
argRefname = "refname"
4750
argToken = "token"
4851
argTransfer = "transfer"
4952
)
5053

54+
// Default username constants
55+
const (
56+
userSelf = "(self)"
57+
userUnknown = "(unknown)"
58+
)
59+
5160
// Operations enum
5261
const (
5362
opNone = iota

0 commit comments

Comments
 (0)