Skip to content

Commit 2a03e96

Browse files
authored
Allow markdown files to read from the LFS (#5787)
This PR makes it possible for the markdown renderer to render images and media straight from the LFS. Fix #5746 Signed-off-by: Andrew Thornton [[email protected]](mailto:[email protected])
1 parent 296814e commit 2a03e96

File tree

9 files changed

+339
-45
lines changed

9 files changed

+339
-45
lines changed

integrations/download_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,15 @@ func TestDownloadByID(t *testing.T) {
2222

2323
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
2424
}
25+
26+
func TestDownloadByIDMedia(t *testing.T) {
27+
prepareTestEnv(t)
28+
29+
session := loginUser(t, "user2")
30+
31+
// Request raw blob
32+
req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
33+
resp := session.MakeRequest(t, req, http.StatusOK)
34+
35+
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
36+
}

integrations/git_test.go

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import (
88
"crypto/rand"
99
"fmt"
1010
"io/ioutil"
11+
"net/http"
1112
"net/url"
1213
"os"
14+
"path"
1315
"path/filepath"
1416
"testing"
1517
"time"
1618

1719
"code.gitea.io/git"
20+
"code.gitea.io/gitea/models"
1821

1922
"github.com/stretchr/testify/assert"
2023
)
@@ -39,6 +42,8 @@ func testGit(t *testing.T, u *url.URL) {
3942
httpContext.Reponame = "repo-tmp-17"
4043

4144
dstPath, err := ioutil.TempDir("", httpContext.Reponame)
45+
var little, big, littleLFS, bigLFS string
46+
4247
assert.NoError(t, err)
4348
defer os.RemoveAll(dstPath)
4449
t.Run("Standard", func(t *testing.T) {
@@ -53,10 +58,10 @@ func testGit(t *testing.T, u *url.URL) {
5358

5459
t.Run("PushCommit", func(t *testing.T) {
5560
t.Run("Little", func(t *testing.T) {
56-
commitAndPush(t, littleSize, dstPath)
61+
little = commitAndPush(t, littleSize, dstPath)
5762
})
5863
t.Run("Big", func(t *testing.T) {
59-
commitAndPush(t, bigSize, dstPath)
64+
big = commitAndPush(t, bigSize, dstPath)
6065
})
6166
})
6267
})
@@ -71,16 +76,60 @@ func testGit(t *testing.T, u *url.URL) {
7176
assert.NoError(t, err)
7277

7378
t.Run("Little", func(t *testing.T) {
74-
commitAndPush(t, littleSize, dstPath)
79+
littleLFS = commitAndPush(t, littleSize, dstPath)
7580
})
7681
t.Run("Big", func(t *testing.T) {
77-
commitAndPush(t, bigSize, dstPath)
82+
bigLFS = commitAndPush(t, bigSize, dstPath)
7883
})
7984
})
8085
t.Run("Locks", func(t *testing.T) {
8186
lockTest(t, u.String(), dstPath)
8287
})
8388
})
89+
t.Run("Raw", func(t *testing.T) {
90+
session := loginUser(t, "user2")
91+
92+
// Request raw paths
93+
req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", little))
94+
resp := session.MakeRequest(t, req, http.StatusOK)
95+
assert.Equal(t, littleSize, resp.Body.Len())
96+
97+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", big))
98+
nilResp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
99+
assert.Equal(t, bigSize, nilResp.Length)
100+
101+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", littleLFS))
102+
resp = session.MakeRequest(t, req, http.StatusOK)
103+
assert.NotEqual(t, littleSize, resp.Body.Len())
104+
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
105+
106+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", bigLFS))
107+
resp = session.MakeRequest(t, req, http.StatusOK)
108+
assert.NotEqual(t, bigSize, resp.Body.Len())
109+
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
110+
111+
})
112+
t.Run("Media", func(t *testing.T) {
113+
session := loginUser(t, "user2")
114+
115+
// Request media paths
116+
req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", little))
117+
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
118+
assert.Equal(t, littleSize, resp.Length)
119+
120+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", big))
121+
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
122+
assert.Equal(t, bigSize, resp.Length)
123+
124+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", littleLFS))
125+
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
126+
assert.Equal(t, littleSize, resp.Length)
127+
128+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", bigLFS))
129+
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
130+
assert.Equal(t, bigSize, resp.Length)
131+
})
132+
84133
})
85134
t.Run("SSH", func(t *testing.T) {
86135
sshContext := baseAPITestContext
@@ -97,6 +146,7 @@ func testGit(t *testing.T, u *url.URL) {
97146
dstPath, err := ioutil.TempDir("", sshContext.Reponame)
98147
assert.NoError(t, err)
99148
defer os.RemoveAll(dstPath)
149+
var little, big, littleLFS, bigLFS string
100150

101151
t.Run("Standard", func(t *testing.T) {
102152
t.Run("CreateRepo", doAPICreateRepository(sshContext, false))
@@ -107,10 +157,10 @@ func testGit(t *testing.T, u *url.URL) {
107157
//time.Sleep(5 * time.Minute)
108158
t.Run("PushCommit", func(t *testing.T) {
109159
t.Run("Little", func(t *testing.T) {
110-
commitAndPush(t, littleSize, dstPath)
160+
little = commitAndPush(t, littleSize, dstPath)
111161
})
112162
t.Run("Big", func(t *testing.T) {
113-
commitAndPush(t, bigSize, dstPath)
163+
big = commitAndPush(t, bigSize, dstPath)
114164
})
115165
})
116166
})
@@ -125,16 +175,59 @@ func testGit(t *testing.T, u *url.URL) {
125175
assert.NoError(t, err)
126176

127177
t.Run("Little", func(t *testing.T) {
128-
commitAndPush(t, littleSize, dstPath)
178+
littleLFS = commitAndPush(t, littleSize, dstPath)
129179
})
130180
t.Run("Big", func(t *testing.T) {
131-
commitAndPush(t, bigSize, dstPath)
181+
bigLFS = commitAndPush(t, bigSize, dstPath)
132182
})
133183
})
134184
t.Run("Locks", func(t *testing.T) {
135185
lockTest(t, u.String(), dstPath)
136186
})
137187
})
188+
t.Run("Raw", func(t *testing.T) {
189+
session := loginUser(t, "user2")
190+
191+
// Request raw paths
192+
req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", little))
193+
resp := session.MakeRequest(t, req, http.StatusOK)
194+
assert.Equal(t, littleSize, resp.Body.Len())
195+
196+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", big))
197+
resp = session.MakeRequest(t, req, http.StatusOK)
198+
assert.Equal(t, bigSize, resp.Body.Len())
199+
200+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", littleLFS))
201+
resp = session.MakeRequest(t, req, http.StatusOK)
202+
assert.NotEqual(t, littleSize, resp.Body.Len())
203+
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
204+
205+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", bigLFS))
206+
resp = session.MakeRequest(t, req, http.StatusOK)
207+
assert.NotEqual(t, bigSize, resp.Body.Len())
208+
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
209+
210+
})
211+
t.Run("Media", func(t *testing.T) {
212+
session := loginUser(t, "user2")
213+
214+
// Request media paths
215+
req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", little))
216+
resp := session.MakeRequest(t, req, http.StatusOK)
217+
assert.Equal(t, littleSize, resp.Body.Len())
218+
219+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", big))
220+
resp = session.MakeRequest(t, req, http.StatusOK)
221+
assert.Equal(t, bigSize, resp.Body.Len())
222+
223+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", littleLFS))
224+
resp = session.MakeRequest(t, req, http.StatusOK)
225+
assert.Equal(t, littleSize, resp.Body.Len())
226+
227+
req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", bigLFS))
228+
resp = session.MakeRequest(t, req, http.StatusOK)
229+
assert.Equal(t, bigSize, resp.Body.Len())
230+
})
138231

139232
})
140233

@@ -162,34 +255,35 @@ func lockTest(t *testing.T, remote, repoPath string) {
162255
assert.NoError(t, err)
163256
}
164257

165-
func commitAndPush(t *testing.T, size int, repoPath string) {
166-
err := generateCommitWithNewData(size, repoPath, "[email protected]", "User Two")
258+
func commitAndPush(t *testing.T, size int, repoPath string) string {
259+
name, err := generateCommitWithNewData(size, repoPath, "[email protected]", "User Two")
167260
assert.NoError(t, err)
168261
_, err = git.NewCommand("push").RunInDir(repoPath) //Push
169262
assert.NoError(t, err)
263+
return name
170264
}
171265

172-
func generateCommitWithNewData(size int, repoPath, email, fullName string) error {
266+
func generateCommitWithNewData(size int, repoPath, email, fullName string) (string, error) {
173267
//Generate random file
174268
data := make([]byte, size)
175269
_, err := rand.Read(data)
176270
if err != nil {
177-
return err
271+
return "", err
178272
}
179273
tmpFile, err := ioutil.TempFile(repoPath, "data-file-")
180274
if err != nil {
181-
return err
275+
return "", err
182276
}
183277
defer tmpFile.Close()
184278
_, err = tmpFile.Write(data)
185279
if err != nil {
186-
return err
280+
return "", err
187281
}
188282

189283
//Commit
190284
err = git.AddChanges(repoPath, false, filepath.Base(tmpFile.Name()))
191285
if err != nil {
192-
return err
286+
return "", err
193287
}
194288
err = git.CommitChanges(repoPath, git.CommitChangesOptions{
195289
Committer: &git.Signature{
@@ -204,5 +298,5 @@ func generateCommitWithNewData(size int, repoPath, email, fullName string) error
204298
},
205299
Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
206300
})
207-
return err
301+
return filepath.Base(tmpFile.Name()), err
208302
}

integrations/integration_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ import (
3535

3636
var mac *macaron.Macaron
3737

38+
type NilResponseRecorder struct {
39+
httptest.ResponseRecorder
40+
Length int
41+
}
42+
43+
func (n *NilResponseRecorder) Write(b []byte) (int, error) {
44+
n.Length = n.Length + len(b)
45+
return len(b), nil
46+
}
47+
48+
// NewRecorder returns an initialized ResponseRecorder.
49+
func NewNilResponseRecorder() *NilResponseRecorder {
50+
return &NilResponseRecorder{
51+
ResponseRecorder: *httptest.NewRecorder(),
52+
}
53+
}
54+
3855
func TestMain(m *testing.M) {
3956
initIntegrationTest()
4057
mac = routes.NewMacaron()
@@ -192,6 +209,22 @@ func (s *TestSession) MakeRequest(t testing.TB, req *http.Request, expectedStatu
192209
return resp
193210
}
194211

212+
func (s *TestSession) MakeRequestNilResponseRecorder(t testing.TB, req *http.Request, expectedStatus int) *NilResponseRecorder {
213+
baseURL, err := url.Parse(setting.AppURL)
214+
assert.NoError(t, err)
215+
for _, c := range s.jar.Cookies(baseURL) {
216+
req.AddCookie(c)
217+
}
218+
resp := MakeRequestNilResponseRecorder(t, req, expectedStatus)
219+
220+
ch := http.Header{}
221+
ch.Add("Cookie", strings.Join(resp.HeaderMap["Set-Cookie"], ";"))
222+
cr := http.Request{Header: ch}
223+
s.jar.SetCookies(baseURL, cr.Cookies())
224+
225+
return resp
226+
}
227+
195228
const userPassword = "password"
196229

197230
var loginSessionCache = make(map[string]*TestSession, 10)
@@ -305,6 +338,18 @@ func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *httptest.
305338
return recorder
306339
}
307340

341+
func MakeRequestNilResponseRecorder(t testing.TB, req *http.Request, expectedStatus int) *NilResponseRecorder {
342+
recorder := NewNilResponseRecorder()
343+
mac.ServeHTTP(recorder, req)
344+
if expectedStatus != NoExpectedStatus {
345+
if !assert.EqualValues(t, expectedStatus, recorder.Code,
346+
"Request: %s %s", req.Method, req.URL.String()) {
347+
logUnexpectedResponse(t, &recorder.ResponseRecorder)
348+
}
349+
}
350+
return recorder
351+
}
352+
308353
// logUnexpectedResponse logs the contents of an unexpected response.
309354
func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
310355
respBytes := recorder.Body.Bytes()

modules/lfs/pointers.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package lfs
6+
7+
import (
8+
"io"
9+
"strconv"
10+
"strings"
11+
12+
"code.gitea.io/gitea/models"
13+
"code.gitea.io/gitea/modules/base"
14+
"code.gitea.io/gitea/modules/setting"
15+
)
16+
17+
// ReadPointerFile will return a partially filled LFSMetaObject if the provided reader is a pointer file
18+
func ReadPointerFile(reader io.Reader) (*models.LFSMetaObject, *[]byte) {
19+
if !setting.LFS.StartServer {
20+
return nil, nil
21+
}
22+
23+
buf := make([]byte, 1024)
24+
n, _ := reader.Read(buf)
25+
buf = buf[:n]
26+
27+
if isTextFile := base.IsTextFile(buf); !isTextFile {
28+
return nil, nil
29+
}
30+
31+
return IsPointerFile(&buf), &buf
32+
}
33+
34+
// IsPointerFile will return a partially filled LFSMetaObject if the provided byte slice is a pointer file
35+
func IsPointerFile(buf *[]byte) *models.LFSMetaObject {
36+
if !setting.LFS.StartServer {
37+
return nil
38+
}
39+
40+
headString := string(*buf)
41+
if !strings.HasPrefix(headString, models.LFSMetaFileIdentifier) {
42+
return nil
43+
}
44+
45+
splitLines := strings.Split(headString, "\n")
46+
if len(splitLines) < 3 {
47+
return nil
48+
}
49+
50+
oid := strings.TrimPrefix(splitLines[1], models.LFSMetaFileOidPrefix)
51+
size, err := strconv.ParseInt(strings.TrimPrefix(splitLines[2], "size "), 10, 64)
52+
if len(oid) != 64 || err != nil {
53+
return nil
54+
}
55+
56+
contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
57+
meta := &models.LFSMetaObject{Oid: oid, Size: size}
58+
if !contentStore.Exists(meta) {
59+
return nil
60+
}
61+
62+
return meta
63+
}
64+
65+
// ReadMetaObject will read a models.LFSMetaObject and return a reader
66+
func ReadMetaObject(meta *models.LFSMetaObject) (io.ReadCloser, error) {
67+
contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
68+
return contentStore.Get(meta, 0)
69+
}

modules/markup/markdown/markdown.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byt
106106
if r.IsWiki {
107107
prefix = util.URLJoin(prefix, "wiki", "raw")
108108
}
109-
prefix = strings.Replace(prefix, "/src/", "/raw/", 1)
109+
prefix = strings.Replace(prefix, "/src/", "/media/", 1)
110110
if len(link) > 0 && !markup.IsLink(link) {
111111
lnk := string(link)
112112
lnk = util.URLJoin(prefix, lnk)

0 commit comments

Comments
 (0)