Skip to content

Commit 5fadb5d

Browse files
modules/lfstransfer: add locking support
1 parent 49c7aca commit 5fadb5d

File tree

3 files changed

+305
-6
lines changed

3 files changed

+305
-6
lines changed

modules/lfstransfer/backend/backend.go

+2-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"context"
99
"encoding/base64"
1010
"encoding/json"
11-
"fmt"
1211
"io"
1312
"net/http"
1413
"strconv"
@@ -24,7 +23,7 @@ const Version = "1"
2423
// Capabilities is a list of Git LFS capabilities supported by this package.
2524
var Capabilities = []string{
2625
"version=" + Version,
27-
// "locking", // no support yet in gitea backend
26+
"locking",
2827
}
2928

3029
var _ transfer.Backend = &GiteaBackend{}
@@ -286,8 +285,5 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
286285

287286
// LockBackend implements transfer.Backend.
288287
func (g *GiteaBackend) LockBackend(_ transfer.Args) transfer.LockBackend {
289-
// Gitea doesn't support the locking API
290-
// this should never be called as we don't advertise the capability
291-
panic(fmt.Errorf("backend doesn't implement locking"))
292-
return (transfer.LockBackend)(nil)
288+
return newGiteaLockBackend(g)
293289
}

modules/lfstransfer/backend/lock.go

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

modules/lfstransfer/backend/util.go

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
14
package backend
25

36
import (
@@ -37,13 +40,22 @@ const (
3740

3841
// SSH protocol argument keys
3942
const (
43+
argCursor = "cursor"
4044
argExpiresAt = "expires-at"
4145
argID = "id"
46+
argLimit = "limit"
47+
argPath = "path"
4248
argRefname = "refname"
4349
argToken = "token"
4450
argTransfer = "transfer"
4551
)
4652

53+
// unknown username constants
54+
const (
55+
userUnknown = "unknown"
56+
userSelf = "unknown-self"
57+
)
58+
4759
// Operations enum
4860
const (
4961
opNone = iota

0 commit comments

Comments
 (0)