Skip to content

Commit 8c4be29

Browse files
committed
first release 0.9.0
1 parent a85eab7 commit 8c4be29

File tree

7 files changed

+744
-1
lines changed

7 files changed

+744
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.DS_Store
2+
go.sum

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,26 @@
11
# git-lfs-transfer
2-
Server-side implementation of Git LFS over SSH
2+
3+
git-lfs-transfer is the server-side implementation of Git LFS using the SSH protocol.
4+
5+
```bash
6+
git-lfs-transfer <git-dir> <operation>
7+
```
8+
9+
Invoked by Git LFS client and updates the repository with the blob files from the remote end.
10+
11+
The command is usually not invoked directly by the end user. The UI for the [GIT LFS SSH protocol](https://github.com/git-lfs/git-lfs/blob/main/docs/proposals/ssh_adapter.md) is on the client side, and the program pair is meant to be used to push/pull updates to remote repository.
12+
13+
## Getting Started
14+
15+
### Installing
16+
17+
#### From source
18+
19+
- Ensure you have the latest version of Go, SSH server and Git installed.
20+
- Clone the repository
21+
- Run `go build -o git-lfs-transfer main.go`.
22+
- Place the `git-lfs-transfer` binary on your system’s executable `$PATH` or equivalent.
23+
24+
## License
25+
26+
MIT

go.mod

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module github.com/autovia/git-lfs-transfer
2+
3+
go 1.20
4+
5+
require (
6+
github.com/git-lfs/git-lfs/v3 v3.3.0
7+
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1
8+
)
9+
10+
require (
11+
github.com/avast/retry-go v2.4.2+incompatible // indirect
12+
github.com/git-lfs/wildmatch/v2 v2.0.1 // indirect
13+
github.com/leonelquinteros/gotext v1.5.0 // indirect
14+
github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 // indirect
15+
github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 // indirect
16+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
17+
golang.org/x/text v0.3.7 // indirect
18+
)

internal/fs.go

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
package internal
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"encoding/gob"
7+
"encoding/hex"
8+
"fmt"
9+
"io/ioutil"
10+
"os"
11+
"os/user"
12+
"path/filepath"
13+
"strconv"
14+
"strings"
15+
"syscall"
16+
"time"
17+
18+
"github.com/git-lfs/git-lfs/v3/tools"
19+
)
20+
21+
type Filesystem struct {
22+
c *PktlineChannel
23+
}
24+
25+
func (fs *Filesystem) lockObject() ([]string, error) {
26+
var file string
27+
for _, arg := range fs.c.req.args {
28+
if strings.HasPrefix(arg, "path=") {
29+
file = arg[5:]
30+
}
31+
}
32+
33+
hash := sha256.New()
34+
hash.Write([]byte(file))
35+
id := hex.EncodeToString(hash.Sum(nil))
36+
37+
lockpath := filepath.Join(fs.c.path, "locks", id)
38+
if _, err := os.Stat(lockpath); err == nil {
39+
return nil, err
40+
} else {
41+
uid := syscall.Getuid()
42+
user, err := user.LookupId(strconv.Itoa(uid))
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
now := time.Now().UTC().Format(time.RFC3339)
48+
49+
lockfile, err := os.Create(lockpath)
50+
if err != nil {
51+
return nil, err
52+
}
53+
54+
m := map[string]string{"path": file, "locked-at": now, "ownername": user.Username}
55+
b := new(bytes.Buffer)
56+
e := gob.NewEncoder(b)
57+
err = e.Encode(m)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
_, err = lockfile.Write(b.Bytes())
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
lockfile.Close()
68+
69+
msgs := []string{
70+
fmt.Sprintf("id=%s", id),
71+
fmt.Sprintf("path=%s", file),
72+
fmt.Sprintf("locked-at=%s", now),
73+
fmt.Sprintf("ownername=%s", user.Username),
74+
}
75+
76+
return msgs, nil
77+
}
78+
}
79+
80+
func (fs *Filesystem) listLocks() ([]string, error) {
81+
lockpath := filepath.Join(fs.c.path, "locks")
82+
83+
files, err := ioutil.ReadDir(lockpath)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
msgs := []string{}
89+
90+
for _, file := range files {
91+
path := filepath.Join(lockpath, file.Name())
92+
decodedMap, err := readLockFile(path)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
msgs = append(msgs, fmt.Sprintf("lock %s", file.Name()))
98+
msgs = append(msgs, fmt.Sprintf("path %s %s", file.Name(), decodedMap["path"]))
99+
msgs = append(msgs, fmt.Sprintf("locked-at %s %s", file.Name(), decodedMap["locked-at"]))
100+
msgs = append(msgs, fmt.Sprintf("ownername %s %s", file.Name(), decodedMap["ownername"]))
101+
102+
uid := syscall.Getuid()
103+
currentUser, err := user.LookupId(strconv.Itoa(uid))
104+
if err != nil {
105+
return nil, err
106+
}
107+
stat, err := os.Stat(path)
108+
if err != nil {
109+
return nil, err
110+
}
111+
fileinfo, ok := stat.Sys().(*syscall.Stat_t)
112+
if !ok {
113+
return nil, fmt.Errorf("cannot get user for file %q", path)
114+
}
115+
fileUser, err := user.LookupId(strconv.Itoa(int(fileinfo.Uid)))
116+
if err != nil {
117+
return nil, err
118+
}
119+
if currentUser.Username == fileUser.Username {
120+
msgs = append(msgs, fmt.Sprintf("owner %s %s", file.Name(), "ours"))
121+
} else {
122+
msgs = append(msgs, fmt.Sprintf("owner %s %s", file.Name(), "theirs"))
123+
}
124+
}
125+
return msgs, nil
126+
}
127+
128+
func (fs *Filesystem) unlockObject() ([]string, error) {
129+
var id string
130+
131+
values := strings.Split(fs.c.req.args[0], " ")
132+
if len(values) > 0 {
133+
id = values[1]
134+
}
135+
136+
lockpath := filepath.Join(fs.c.path, "locks", id)
137+
if _, err := os.Stat(lockpath); err == nil {
138+
decodedMap, err := readLockFile(lockpath)
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
msgs := []string{
144+
fmt.Sprintf("id=%s", id),
145+
fmt.Sprintf("path=%s", lockpath),
146+
fmt.Sprintf("locked-at=%s", decodedMap["locked-at"]),
147+
fmt.Sprintf("ownername=%s", decodedMap["ownername"]),
148+
}
149+
150+
err = os.Remove(lockpath)
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
return msgs, nil
156+
157+
} else {
158+
return nil, fmt.Errorf("lock does not exists")
159+
}
160+
}
161+
162+
func (fs *Filesystem) getObject() error {
163+
var oid string
164+
var size int64
165+
var err error
166+
for _, arg := range fs.c.req.args {
167+
if strings.HasPrefix(arg, "get-object") {
168+
oid = strings.Split(arg, " ")[1]
169+
}
170+
if strings.HasPrefix(arg, "size=") {
171+
size, err = strconv.ParseInt(arg[5:], 10, 64)
172+
if err != nil {
173+
return err
174+
}
175+
}
176+
}
177+
f, err := os.OpenFile(filepath.Join(fs.c.path, "objects", oid[0:2], oid[2:4], oid), os.O_RDONLY, 0644)
178+
if err != nil {
179+
return err
180+
}
181+
182+
fs.c.pl.WritePacketText("status 200")
183+
fs.c.pl.WritePacketText(fmt.Sprintf("size=%v", size))
184+
fs.c.pl.WriteDelim()
185+
186+
defer f.Close()
187+
type ProgressCallback func(totalSize, readSoFar int64, readSinceLast int) error
188+
var cb ProgressCallback
189+
ccb := func(totalSize int64, readSoFar int64, readSinceLast int) error {
190+
if cb != nil {
191+
return cb(totalSize, readSoFar, readSinceLast)
192+
}
193+
return nil
194+
}
195+
cbr := tools.NewFileBodyWithCallback(f, size, ccb)
196+
buf := make([]byte, 32768)
197+
for {
198+
n, err := cbr.Read(buf)
199+
if n > 0 {
200+
err := fs.c.pl.WritePacket(buf[0:n])
201+
if err != nil {
202+
return err
203+
}
204+
}
205+
if err != nil {
206+
break
207+
}
208+
}
209+
fs.c.pl.WriteFlush()
210+
return nil
211+
}
212+
213+
func (fs *Filesystem) storeObject() error {
214+
var oid string
215+
var size int64
216+
var err error
217+
for _, arg := range fs.c.req.args {
218+
if strings.HasPrefix(arg, "put-object") {
219+
oid = strings.Split(arg, " ")[1]
220+
}
221+
if strings.HasPrefix(arg, "size=") {
222+
size, err = strconv.ParseInt(arg[5:], 10, 64)
223+
if err != nil {
224+
return err
225+
}
226+
}
227+
}
228+
var fa os.FileInfo
229+
fa, err = os.Stat(filepath.Join(fs.c.path, "objects", oid[0:2], oid[2:4], oid))
230+
if err == nil {
231+
if size == fa.Size() {
232+
// file already exists, nothing to do
233+
return nil
234+
}
235+
}
236+
237+
dst, err := ioutil.TempFile(filepath.Join(fs.c.path, "tmp"), "dst")
238+
if err != nil {
239+
return err
240+
}
241+
type ProgressCallback func(name string, totalSize, readSoFar int64, readSinceLast int) error
242+
var cb ProgressCallback
243+
ccb := func(totalSize int64, readSoFar int64, readSinceLast int) error {
244+
if cb != nil {
245+
return cb(oid, totalSize, readSoFar, readSinceLast)
246+
}
247+
return nil
248+
}
249+
hasher := tools.NewHashingReader(fs.c.req.data)
250+
written, err := tools.CopyWithCallback(dst, hasher, size, ccb)
251+
if err != nil {
252+
return err
253+
}
254+
if actual := hasher.Hash(); actual != oid {
255+
return fmt.Errorf("expected OID %s, got %s after %d bytes written", oid, actual, written)
256+
}
257+
if err := dst.Close(); err != nil {
258+
return err
259+
}
260+
fi, err := os.Stat(dst.Name())
261+
if err != nil {
262+
return err
263+
}
264+
if size != fi.Size() {
265+
return fmt.Errorf("can not verify file size after upload")
266+
}
267+
err = os.MkdirAll(filepath.Join(fs.c.path, "objects", oid[0:2], oid[2:4]), os.ModePerm)
268+
if err != nil {
269+
return err
270+
}
271+
err = os.Rename(dst.Name(), filepath.Join(fs.c.path, "objects", oid[0:2], oid[2:4], oid))
272+
if err != nil {
273+
return err
274+
}
275+
err = os.Chmod(filepath.Join(fs.c.path, "objects", oid[0:2], oid[2:4], oid), 0775)
276+
if err != nil {
277+
return err
278+
}
279+
return nil
280+
}
281+
282+
func (fs *Filesystem) verifyObject() error {
283+
var oid string
284+
var size int64
285+
var err error
286+
for _, arg := range fs.c.req.args {
287+
if strings.HasPrefix(arg, "verify-object") {
288+
oid = strings.Split(arg, " ")[1]
289+
}
290+
if strings.HasPrefix(arg, "size=") {
291+
size, err = strconv.ParseInt(arg[5:], 10, 64)
292+
if err != nil {
293+
return err
294+
}
295+
}
296+
}
297+
fi, err := os.Stat(filepath.Join(fs.c.path, "objects", oid[0:2], oid[2:4], oid))
298+
if err != nil {
299+
return err
300+
}
301+
if size != fi.Size() {
302+
return fmt.Errorf("can not verify file size after upload")
303+
}
304+
return nil
305+
}
306+
307+
func readLockFile(path string) (map[string]string, error) {
308+
f, err := os.Open(path)
309+
if err != nil {
310+
return nil, err
311+
}
312+
var decodedMap map[string]string
313+
d := gob.NewDecoder(f)
314+
err = d.Decode(&decodedMap)
315+
if err != nil {
316+
return nil, err
317+
}
318+
defer f.Close()
319+
return decodedMap, nil
320+
}

0 commit comments

Comments
 (0)