Skip to content

Commit cb666d2

Browse files
committed
Merge pull request git-lfs#173 from hawser/download-from-metadata
Download from metadata
2 parents b366856 + 2dd5983 commit cb666d2

File tree

4 files changed

+202
-72
lines changed

4 files changed

+202
-72
lines changed

hawser/client.go

Lines changed: 123 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,21 @@ import (
1414
"net/http"
1515
"os"
1616
"path/filepath"
17+
"strings"
1718
)
1819

1920
const (
20-
gitMediaType = "application/vnd.git-media"
21-
gitMediaMetaType = gitMediaType + "+json; charset=utf-8"
22-
)
23-
24-
type linkMeta struct {
25-
Links map[string]*link `json:"_links,omitempty"`
26-
}
27-
28-
func (l *linkMeta) Rel(name string) (*link, bool) {
29-
if l.Links == nil {
30-
return nil, false
31-
}
21+
// Legacy type
22+
gitMediaType = "application/vnd.git-media"
3223

33-
lnk, ok := l.Links[name]
34-
return lnk, ok
35-
}
36-
37-
type link struct {
38-
Href string `json:"href"`
39-
Header map[string]string `json:"header,omitempty"`
40-
}
24+
// The main type, sub type, and suffix. Use this when ensuring the type from
25+
// an HTTP response is correct.
26+
gitMediaMetaTypePrefix = gitMediaType + "+json"
4127

42-
type UploadRequest struct {
43-
OidPath string
44-
Filename string
45-
CopyCallback CopyCallback
46-
}
28+
// Adds the extra mime params. Use this when sending the type in an HTTP
29+
// request.
30+
gitMediaMetaType = gitMediaMetaTypePrefix + "; charset=utf-8"
31+
)
4732

4833
func Download(oidPath string) (io.ReadCloser, int64, *WrappedError) {
4934
oid := filepath.Base(oidPath)
@@ -66,6 +51,47 @@ func Download(oidPath string) (io.ReadCloser, int64, *WrappedError) {
6651
return nil, 0, wErr
6752
}
6853

54+
if strings.HasPrefix(contentType, gitMediaMetaTypePrefix) {
55+
obj := &objectResource{}
56+
err := json.NewDecoder(res.Body).Decode(obj)
57+
res.Body.Close()
58+
if err != nil {
59+
wErr := Error(err)
60+
setErrorResponseContext(wErr, res)
61+
return nil, 0, wErr
62+
}
63+
64+
dlReq, err := obj.NewRequest("download", "GET")
65+
if err != nil {
66+
wErr := Error(err)
67+
setErrorResponseContext(wErr, res)
68+
return nil, 0, wErr
69+
}
70+
71+
dlCreds, err := setRequestHeaders(dlReq)
72+
if err != nil {
73+
return nil, 0, Errorf(err, "Error attempting to GET %s", oidPath)
74+
}
75+
76+
dlRes, err := DoHTTP(Config, dlReq)
77+
if err != nil {
78+
wErr := Error(err)
79+
setErrorResponseContext(wErr, res)
80+
return nil, 0, wErr
81+
}
82+
83+
saveCredentials(dlCreds, dlRes)
84+
85+
contentType := dlRes.Header.Get("Content-Type")
86+
if contentType == "" {
87+
wErr = Error(errors.New("Empty Content-Type"))
88+
setErrorResponseContext(wErr, res)
89+
return nil, 0, wErr
90+
}
91+
92+
res = dlRes
93+
}
94+
6995
ok, headerSize, wErr := validateMediaHeader(contentType, res.Body)
7096
if !ok {
7197
setErrorResponseContext(wErr, res)
@@ -111,6 +137,46 @@ func Upload(oidPath, filename string, cb CopyCallback) *WrappedError {
111137
return nil
112138
}
113139

140+
type objectResource struct {
141+
Oid string `json:"oid,omitempty"`
142+
Size int64 `json:"size,omitempty"`
143+
Links map[string]*linkRelation `json:"_links,omitempty"`
144+
}
145+
146+
var objectRelationDoesNotExist = errors.New("relation does not exist")
147+
148+
func (o *objectResource) NewRequest(relation, method string) (*http.Request, error) {
149+
rel, ok := o.Rel(relation)
150+
if !ok {
151+
return nil, objectRelationDoesNotExist
152+
}
153+
154+
req, err := http.NewRequest(method, rel.Href, nil)
155+
if err != nil {
156+
return nil, err
157+
}
158+
159+
for h, v := range rel.Header {
160+
req.Header.Set(h, v)
161+
}
162+
163+
return req, nil
164+
}
165+
166+
func (o *objectResource) Rel(name string) (*linkRelation, bool) {
167+
if o.Links == nil {
168+
return nil, false
169+
}
170+
171+
rel, ok := o.Links[name]
172+
return rel, ok
173+
}
174+
175+
type linkRelation struct {
176+
Href string `json:"href"`
177+
Header map[string]string `json:"header,omitempty"`
178+
}
179+
114180
func callOptions(filehash string) (int, *WrappedError) {
115181
oid := filepath.Base(filehash)
116182
_, err := os.Stat(filehash)
@@ -180,18 +246,22 @@ func callPut(filehash, filename string, cb CopyCallback) *WrappedError {
180246
return wErr
181247
}
182248

183-
func callExternalPut(filehash, filename string, lm *linkMeta, cb CopyCallback) *WrappedError {
184-
if lm == nil {
249+
func callExternalPut(filehash, filename string, obj *objectResource, cb CopyCallback) *WrappedError {
250+
if obj == nil {
185251
return Errorf(errors.New("No hypermedia links provided"),
186252
"Error attempting to PUT %s", filename)
187253
}
188254

189-
link, ok := lm.Rel("upload")
190-
if !ok {
255+
req, err := obj.NewRequest("upload", "PUT")
256+
if err == objectRelationDoesNotExist {
191257
return Errorf(errors.New("No upload link provided"),
192258
"Error attempting to PUT %s", filename)
193259
}
194260

261+
if err != nil {
262+
return Errorf(err, "Error attempting to PUT %s", filename)
263+
}
264+
195265
file, err := os.Open(filehash)
196266
if err != nil {
197267
return Errorf(err, "Error attempting to PUT %s", filename)
@@ -209,14 +279,6 @@ func callExternalPut(filehash, filename string, lm *linkMeta, cb CopyCallback) *
209279
Reader: file,
210280
}
211281

212-
req, err := http.NewRequest("PUT", link.Href, nil)
213-
if err != nil {
214-
return Errorf(err, "Error attempting to PUT %s", filename)
215-
}
216-
for h, v := range link.Header {
217-
req.Header.Set(h, v)
218-
}
219-
220282
creds, err := setRequestHeaders(req)
221283
if err != nil {
222284
return Errorf(err, "Error attempting to PUT %s", filename)
@@ -238,39 +300,36 @@ func callExternalPut(filehash, filename string, lm *linkMeta, cb CopyCallback) *
238300
saveCredentials(creds, res)
239301

240302
// Run the verify callback
241-
if cb, ok := lm.Rel("verify"); ok {
242-
oid := filepath.Base(filehash)
243-
244-
verifyReq, err := http.NewRequest("POST", cb.Href, nil)
245-
if err != nil {
246-
return Errorf(err, "Error attempting to verify %s", filename)
247-
}
303+
verifyReq, err := obj.NewRequest("verify", "POST")
304+
if err == objectRelationDoesNotExist {
305+
return nil
306+
}
248307

249-
for h, v := range cb.Header {
250-
verifyReq.Header.Set(h, v)
251-
}
308+
if err != nil {
309+
return Errorf(err, "Error attempting to verify %s", filename)
310+
}
252311

253-
verifyCreds, err := setRequestHeaders(verifyReq)
254-
if err != nil {
255-
return Errorf(err, "Error attempting to verify %s", filename)
256-
}
312+
verifyCreds, err := setRequestHeaders(verifyReq)
313+
if err != nil {
314+
return Errorf(err, "Error attempting to verify %s", filename)
315+
}
257316

258-
d := fmt.Sprintf(`{"oid":"%s", "size":%d}`, oid, fileSize)
259-
verifyReq.Body = ioutil.NopCloser(bytes.NewBufferString(d))
317+
oid := filepath.Base(filehash)
318+
d := fmt.Sprintf(`{"oid":"%s", "size":%d}`, oid, fileSize)
319+
verifyReq.Body = ioutil.NopCloser(bytes.NewBufferString(d))
260320

261-
tracerx.Printf("verify: %s %s", oid, cb.Href)
262-
verifyRes, err := DoHTTP(Config, verifyReq)
263-
if err != nil {
264-
return Errorf(err, "Error attempting to verify %s", filename)
265-
}
266-
tracerx.Printf("verify_status: %d", verifyRes.StatusCode)
267-
saveCredentials(verifyCreds, verifyRes)
321+
tracerx.Printf("verify: %s %s", oid, verifyReq.URL.String())
322+
verifyRes, err := DoHTTP(Config, verifyReq)
323+
if err != nil {
324+
return Errorf(err, "Error attempting to verify %s", filename)
268325
}
326+
tracerx.Printf("verify_status: %d", verifyRes.StatusCode)
327+
saveCredentials(verifyCreds, verifyRes)
269328

270329
return nil
271330
}
272331

273-
func callPost(filehash, filename string) (*linkMeta, int, *WrappedError) {
332+
func callPost(filehash, filename string) (*objectResource, int, *WrappedError) {
274333
oid := filepath.Base(filehash)
275334
req, creds, err := request("POST", "")
276335
if err != nil {
@@ -302,14 +361,13 @@ func callPost(filehash, filename string) (*linkMeta, int, *WrappedError) {
302361
tracerx.Printf("api_post_status: %d", res.StatusCode)
303362

304363
if res.StatusCode == 202 {
305-
lm := &linkMeta{}
306-
dec := json.NewDecoder(res.Body)
307-
err := dec.Decode(lm)
364+
obj := &objectResource{}
365+
err := json.NewDecoder(res.Body).Decode(obj)
308366
if err != nil {
309367
return nil, res.StatusCode, Errorf(err, "Error decoding JSON from %s %s.", req.Method, req.URL)
310368
}
311369

312-
return lm, res.StatusCode, nil
370+
return obj, res.StatusCode, nil
313371
}
314372

315373
return nil, res.StatusCode, nil

hawser/client_download_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package hawser
22

33
import (
4+
"encoding/json"
45
"io/ioutil"
56
"net/http"
67
"net/http/httptest"
@@ -49,6 +50,77 @@ func TestDownload(t *testing.T) {
4950
}
5051
}
5152

53+
func TestDownloadFromMeta(t *testing.T) {
54+
mux := http.NewServeMux()
55+
server := httptest.NewServer(mux)
56+
tmp := tempdir(t)
57+
defer server.Close()
58+
defer os.RemoveAll(tmp)
59+
60+
// simulates an endpoint that returns the meta data for every request.
61+
// this way downloads keep working with the older prototype server during
62+
// the pre-release.
63+
mux.HandleFunc("/media/objects/oid", func(w http.ResponseWriter, r *http.Request) {
64+
if r.Method != "GET" {
65+
w.WriteHeader(405)
66+
return
67+
}
68+
69+
obj := &objectResource{
70+
Oid: "oid",
71+
Size: 4,
72+
Links: map[string]*linkRelation{
73+
"download": &linkRelation{
74+
Href: server.URL + "/media/download/oid",
75+
},
76+
},
77+
}
78+
79+
by, err := json.Marshal(obj)
80+
if err != nil {
81+
t.Errorf("Error marshaling json: %s", err)
82+
}
83+
84+
head := w.Header()
85+
head.Set("Content-Type", "application/vnd.git-media+json")
86+
w.WriteHeader(200)
87+
w.Write(by)
88+
})
89+
90+
mux.HandleFunc("/media/download/oid", func(w http.ResponseWriter, r *http.Request) {
91+
if r.Method != "GET" {
92+
w.WriteHeader(405)
93+
return
94+
}
95+
96+
head := w.Header()
97+
head.Set("Content-Type", "application/octet-stream")
98+
head.Set("Content-Length", "4")
99+
w.WriteHeader(200)
100+
w.Write([]byte("test"))
101+
})
102+
103+
Config.SetConfig("hawser.url", server.URL+"/media")
104+
reader, size, wErr := Download("whatever/oid")
105+
if wErr != nil {
106+
t.Fatalf("unexpected error: %s", wErr)
107+
}
108+
defer reader.Close()
109+
110+
if size != 4 {
111+
t.Errorf("unexpected size: %d", size)
112+
}
113+
114+
by, err := ioutil.ReadAll(reader)
115+
if err != nil {
116+
t.Fatalf("unexpected error: %s", err)
117+
}
118+
119+
if body := string(by); body != "test" {
120+
t.Errorf("unexpected body: %s", body)
121+
}
122+
}
123+
52124
func TestDownloadWithRedirect(t *testing.T) {
53125
mux := http.NewServeMux()
54126
server := httptest.NewServer(mux)

hawser/client_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ func TestExternalPut(t *testing.T) {
8888
w.WriteHeader(200)
8989
})
9090

91-
link := &linkMeta{
92-
Links: map[string]*link{
91+
obj := &objectResource{
92+
Links: map[string]*linkRelation{
9393
"upload": {
9494
Href: server.URL + "/media/objects/oid",
9595
Header: map[string]string{"a": "1"},
@@ -101,7 +101,7 @@ func TestExternalPut(t *testing.T) {
101101
},
102102
}
103103

104-
if err := callExternalPut(oidPath, "", link, nil); err != nil {
104+
if err := callExternalPut(oidPath, "", obj, nil); err != nil {
105105
t.Error(err)
106106
}
107107

hawser/client_upload_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ func TestUploadWithVerify(t *testing.T) {
4343

4444
posted = true
4545

46-
link := &linkMeta{
47-
Links: map[string]*link{
46+
obj := &objectResource{
47+
Links: map[string]*linkRelation{
4848
"upload": {
4949
Href: server.URL + "/media/objects/oid",
5050
Header: map[string]string{"a": "1"},
@@ -56,9 +56,9 @@ func TestUploadWithVerify(t *testing.T) {
5656
},
5757
}
5858

59-
by, err := json.Marshal(link)
59+
by, err := json.Marshal(obj)
6060
if err != nil {
61-
t.Errorf("Error marshaling link json: %s", link)
61+
t.Errorf("Error marshaling link json: %s", obj)
6262
w.WriteHeader(500)
6363
return
6464
}

0 commit comments

Comments
 (0)