]> Sergey Matveev's repositories - btrtrc.git/blob - webseed/client.go
Further fixes to webseed path encoding
[btrtrc.git] / webseed / client.go
1 package webseed
2
3 import (
4         "bytes"
5         "context"
6         "errors"
7         "fmt"
8         "io"
9         "net/http"
10
11         "github.com/anacrolix/torrent/metainfo"
12         "github.com/anacrolix/torrent/segments"
13 )
14
15 type RequestSpec = segments.Extent
16
17 type requestPartResult struct {
18         resp *http.Response
19         err  error
20 }
21
22 type requestPart struct {
23         req    *http.Request
24         e      segments.Extent
25         result chan requestPartResult
26 }
27
28 type Request struct {
29         cancel func()
30         Result chan RequestResult
31 }
32
33 func (r Request) Cancel() {
34         r.cancel()
35 }
36
37 type Client struct {
38         HttpClient *http.Client
39         Url        string
40         FileIndex  segments.Index
41         Info       *metainfo.Info
42 }
43
44 type RequestResult struct {
45         Bytes []byte
46         Err   error
47 }
48
49 func (ws *Client) NewRequest(r RequestSpec) Request {
50         ctx, cancel := context.WithCancel(context.Background())
51         var requestParts []requestPart
52         if !ws.FileIndex.Locate(r, func(i int, e segments.Extent) bool {
53                 req, err := NewRequest(ws.Url, i, ws.Info, e.Start, e.Length)
54                 if err != nil {
55                         panic(err)
56                 }
57                 req = req.WithContext(ctx)
58                 part := requestPart{
59                         req:    req,
60                         result: make(chan requestPartResult, 1),
61                         e:      e,
62                 }
63                 go func() {
64                         resp, err := ws.HttpClient.Do(req)
65                         part.result <- requestPartResult{
66                                 resp: resp,
67                                 err:  err,
68                         }
69                 }()
70                 requestParts = append(requestParts, part)
71                 return true
72         }) {
73                 panic("request out of file bounds")
74         }
75         req := Request{
76                 cancel: cancel,
77                 Result: make(chan RequestResult, 1),
78         }
79         go func() {
80                 b, err := readRequestPartResponses(requestParts)
81                 req.Result <- RequestResult{
82                         Bytes: b,
83                         Err:   err,
84                 }
85         }()
86         return req
87 }
88
89 func recvPartResult(buf io.Writer, part requestPart) error {
90         result := <-part.result
91         if result.err != nil {
92                 return result.err
93         }
94         defer result.resp.Body.Close()
95         switch result.resp.StatusCode {
96         case http.StatusPartialContent:
97         case http.StatusOK:
98                 if part.e.Start != 0 {
99                         return errors.New("got status ok but request was at offset")
100                 }
101         default:
102                 return fmt.Errorf("unhandled response status code (%v)", result.resp.StatusCode)
103         }
104         copied, err := io.Copy(buf, result.resp.Body)
105         if err != nil {
106                 return err
107         }
108         if copied != part.e.Length {
109                 return fmt.Errorf("got %v bytes, expected %v", copied, part.e.Length)
110         }
111         return nil
112 }
113
114 func readRequestPartResponses(parts []requestPart) ([]byte, error) {
115         var buf bytes.Buffer
116         for _, part := range parts {
117                 err := recvPartResult(&buf, part)
118                 if err != nil {
119                         return buf.Bytes(), fmt.Errorf("reading %q at %q: %w", part.req.URL, part.req.Header.Get("Range"), err)
120                 }
121         }
122         return buf.Bytes(), nil
123 }