1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
package http_range
import (
"errors"
"fmt"
"io"
"net/http"
"time"
"gitlab.com/gitlab-org/gitlab-pages/internal/httptransport"
"gitlab.com/gitlab-org/gitlab-pages/metrics"
)
var (
// ErrRangeRequestsNotSupported is returned by Seek and Read
// when the remote server does not allow range requests (Accept-Ranges was not set)
ErrRangeRequestsNotSupported = errors.New("range requests are not supported by the remote server")
// ErrInvalidRange is returned by Read when trying to read past the end of the file
ErrInvalidRange = errors.New("invalid range")
// ErrContentHasChanged is returned by Read when the content has changed since the first request
ErrContentHasChanged = errors.New("content has changed since first request")
)
type Reader struct {
R *Resource
offset, size int64
res *http.Response
}
var httpClient = &http.Client{
// TODO: we need connect timeout
// The longest time the request can be executed
Timeout: 30 * time.Minute,
Transport: httptransport.NewTransportWithMetrics(metrics.ZIPHttpReaderReqDuration, metrics.ZIPHttpReaderReqTotal),
}
func (h *Reader) ensureRequest() (err error) {
if h.res != nil {
return nil
}
if h.offset < 0 || h.size < 0 || h.offset+h.size > h.R.Size {
return ErrInvalidRange
}
req, err := http.NewRequest("GET", h.R.URL, nil)
if err != nil {
return err
}
if h.R.LastModified != "" {
req.Header.Set("If-Range", h.R.LastModified)
} else if h.R.Etag != "" {
req.Header.Set("If-Range", h.R.Etag)
}
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", h.offset, h.offset+h.size-1))
res, err := httpClient.Do(req)
if err != nil {
return err
}
// cleanup body on failure to avoid memory leak
defer func() {
if err != nil {
res.Body.Close()
}
}()
switch res.StatusCode {
case http.StatusOK:
// some servers return 200 OK for bytes=0-
if h.offset > 0 || h.R.Etag != "" && h.R.Etag != res.Header.Get("ETag") {
return ErrContentHasChanged
}
break
case http.StatusPartialContent:
break
case http.StatusRequestedRangeNotSatisfiable:
return ErrRangeRequestsNotSupported
default:
return fmt.Errorf("failed with %d: %q", res.StatusCode, res.Status)
}
h.res = res
return nil
}
// WithinRange checks if a given data can be read efficiently
func (h *Reader) WithinRange(offset, n int64) bool {
return h.offset == offset && n <= h.size
}
// Read reads a data into a given buffer
func (h *Reader) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
if err := h.ensureRequest(); err != nil {
return 0, err
}
n, err := h.res.Body.Read(p)
if err == nil || err == io.EOF {
h.offset += int64(n)
h.size -= int64(n)
}
return n, err
}
// Close closes a requests body
func (h *Reader) Close() error {
if h.res != nil {
// TODO: should we read till end?
return h.res.Body.Close()
}
return nil
}
// NewReader creates a Reader object on a given resource for a given range
func NewReader(resource *Resource, offset, n int64) *Reader {
return &Reader{R: resource, offset: offset, size: n}
}
|