main
1package snapshot
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "time"
10)
11
12func Capture(ctx context.Context, target *url.URL, opts ...Option) (*Result, error) {
13
14 options := defaults()
15 for _, o := range opts {
16 o.apply(options)
17 }
18
19 req, err := http.NewRequestWithContext(ctx, "GET", target.String(), nil)
20 if err != nil {
21 return nil, err
22 }
23 req.Header.Set("User-Agent", options.UserAgent)
24
25 client := &http.Client{
26 Timeout: options.Timeout,
27 }
28
29 started := time.Now().UTC()
30 result := &Result{
31 Request: Request{
32 Method: req.Method,
33 URL: target.String(),
34 Started: started,
35
36 Headers: req.Header.Clone(),
37 UserAgent: options.UserAgent,
38 TimeoutMS: options.Timeout.Milliseconds(),
39 },
40 }
41
42 resp, err := client.Do(req)
43 if err != nil {
44 result.OK = false
45 result.Failure = &Failure{Stage: _do_request, Error: err.Error()}
46 return result, nil
47 }
48 defer resp.Body.Close()
49
50 result.StatusCode = resp.StatusCode
51 result.FinalURL = resp.Request.URL.String()
52
53 result.OK = resp.StatusCode >= 200 && resp.StatusCode < 300
54 if !result.OK {
55 result.Failure = &Failure{
56 Stage: _http_status,
57 Error: fmt.Sprintf("http status %d", resp.StatusCode),
58 Transient: resp.StatusCode == http.StatusTooManyRequests ||
59 resp.StatusCode == http.StatusRequestTimeout ||
60 resp.StatusCode > 499,
61 }
62 }
63
64 sink, err := options.BodySink()
65 if err != nil {
66 result.OK = false
67 result.Failure = &Failure{
68 Stage: _body_open,
69 Error: err.Error(),
70 }
71 return result, nil
72 }
73
74 _, err = io.Copy(sink, resp.Body)
75 if err != nil {
76 sinkAborter, ok := sink.WriteCloser.(aborter)
77 if ok {
78 sinkAborter.Abort()
79 } else {
80 sink.Close()
81 }
82 result.OK = false
83 result.Failure = &Failure{
84 Stage: _body_write,
85 Error: err.Error(),
86 }
87 return result, nil
88 }
89
90 err = sink.Close()
91 if err != nil {
92 result.OK = false
93 result.Failure = &Failure{
94 Stage: _body_commit,
95 Error: err.Error(),
96 }
97 return result, nil
98 }
99 if sink.Path != "" {
100 result.BodyPath = sink.Path
101 }
102
103 return result, nil
104}