Welcome to mirror list, hosted at ThFree Co, Russian Federation.

http_reader.go « http_range « zip « vfs « internal - gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: e9db1255bc91225f9f6974e0053fb8ad2f8e6639 (plain)
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}
}