Skip to content

Commit 77134d5

Browse files
modules/lfstransfer: add locking support
1 parent b0bde5f commit 77134d5

File tree

3 files changed

+304
-6
lines changed

3 files changed

+304
-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
"net/url"
@@ -25,7 +24,7 @@ const Version = "1"
2524
// Capabilities is a list of Git LFS capabilities supported by this package.
2625
var Capabilities = []string{
2726
"version=" + Version,
28-
// "locking", // no support yet in gitea backend
27+
"locking",
2928
}
3029

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

292291
// LockBackend implements transfer.Backend.
293292
func (g *GiteaBackend) LockBackend(_ transfer.Args) transfer.LockBackend {
294-
// Gitea doesn't support the locking API
295-
// this should never be called as we don't advertise the capability
296-
panic(fmt.Errorf("backend doesn't implement locking"))
297-
return (transfer.LockBackend)(nil)
293+
return newGiteaLockBackend(g)
298294
}

modules/lfstransfer/backend/lock.go

+293
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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 *url.URL
26+
token string
27+
logger transfer.Logger
28+
}
29+
30+
func newGiteaLockBackend(g *GiteaBackend) transfer.LockBackend {
31+
server := g.server.JoinPath("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.String()
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.JoinPath(lock.ID(), "unlock").String()
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+
urlStruct := *g.server // make a copy
177+
urlStruct.RawQuery = v.Encode()
178+
url := urlStruct.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 string, 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+
// TODO fish out from request
260+
return userSelf, nil
261+
}
262+
263+
// AsLockSpec implements transfer.Lock
264+
func (g *giteaLock) AsLockSpec(ownerID bool) ([]string, error) {
265+
msgs := []string{
266+
fmt.Sprintf("lock %s", g.ID()),
267+
fmt.Sprintf("path %s %s", g.ID(), g.Path()),
268+
fmt.Sprintf("locked-at %s %s", g.ID(), g.FormattedTimestamp()),
269+
fmt.Sprintf("ownername %s %s", g.ID(), g.OwnerName()),
270+
}
271+
if ownerID {
272+
user, err := g.CurrentUser()
273+
if err != nil {
274+
return nil, fmt.Errorf("error getting current user: %w", err)
275+
}
276+
who := "theirs"
277+
if user == g.OwnerName() {
278+
who = "ours"
279+
}
280+
msgs = append(msgs, fmt.Sprintf("owner %s %s", g.ID(), who))
281+
}
282+
return msgs, nil
283+
}
284+
285+
// AsArguments implements transfer.Lock
286+
func (g *giteaLock) AsArguments() []string {
287+
return []string{
288+
fmt.Sprintf("id=%s", g.ID()),
289+
fmt.Sprintf("path=%s", g.Path()),
290+
fmt.Sprintf("locked-at=%s", g.FormattedTimestamp()),
291+
fmt.Sprintf("ownername=%s", g.OwnerName()),
292+
}
293+
}

modules/lfstransfer/backend/util.go

+9
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,22 @@ const (
4040

4141
// SSH protocol argument keys
4242
const (
43+
argCursor = "cursor"
4344
argExpiresAt = "expires-at"
4445
argID = "id"
46+
argLimit = "limit"
47+
argPath = "path"
4548
argRefname = "refname"
4649
argToken = "token"
4750
argTransfer = "transfer"
4851
)
4952

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

0 commit comments

Comments
 (0)