LEFT | RIGHT |
(no file at all) | |
1 package store | 1 package store |
2 | 2 |
3 import ( | 3 import ( |
4 "encoding/json" | 4 "encoding/json" |
5 "io" | 5 "io" |
6 "launchpad.net/juju/go/charm" | 6 "launchpad.net/juju/go/charm" |
7 "launchpad.net/juju/go/log" | 7 "launchpad.net/juju/go/log" |
8 "net/http" | 8 "net/http" |
| 9 "strconv" |
9 "strings" | 10 "strings" |
10 ) | 11 ) |
11 | 12 |
12 // Server is an http.Handler that serves the HTTP API of juju | 13 // Server is an http.Handler that serves the HTTP API of juju |
13 // so that juju clients can retrieve published charms. | 14 // so that juju clients can retrieve published charms. |
14 type Server struct { | 15 type Server struct { |
15 store *Store | 16 store *Store |
16 mux *http.ServeMux | 17 mux *http.ServeMux |
17 } | 18 } |
18 | 19 |
19 // New returns a new *Server using store. | 20 // New returns a new *Server using store. |
20 func NewServer(store *Store) (*Server, error) { | 21 func NewServer(store *Store) (*Server, error) { |
21 s := &Server{ | 22 s := &Server{ |
22 store: store, | 23 store: store, |
23 mux: http.NewServeMux(), | 24 mux: http.NewServeMux(), |
24 } | 25 } |
25 s.mux.HandleFunc("/charm-info", func(w http.ResponseWriter, r *http.Requ
est) { | 26 s.mux.HandleFunc("/charm-info", func(w http.ResponseWriter, r *http.Requ
est) { |
26 s.serveInfo(w, r) | 27 s.serveInfo(w, r) |
27 }) | 28 }) |
28 s.mux.HandleFunc("/charm/", func(w http.ResponseWriter, r *http.Request)
{ | 29 s.mux.HandleFunc("/charm/", func(w http.ResponseWriter, r *http.Request)
{ |
29 s.serveCharm(w, r) | 30 s.serveCharm(w, r) |
30 }) | 31 }) |
| 32 s.mux.HandleFunc("/stats/counter/", func(w http.ResponseWriter, r *http.
Request) { |
| 33 s.serveStats(w, r) |
| 34 }) |
| 35 |
| 36 // This is just a validation key to allow blitz.io to run |
| 37 // performance tests against the site. |
| 38 s.mux.HandleFunc("/mu-35700a31-6bf320ca-a800b670-05f845ee", func(w http.
ResponseWriter, r *http.Request) { |
| 39 s.serveBlitzKey(w, r) |
| 40 }) |
31 return s, nil | 41 return s, nil |
32 } | 42 } |
33 | 43 |
34 // ServeHTTP serves an http request. | 44 // ServeHTTP serves an http request. |
35 // This method turns *Server into an http.Handler. | 45 // This method turns *Server into an http.Handler. |
36 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 46 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 47 if r.URL.Path == "/" { |
| 48 http.Redirect(w, r, "https://juju.ubuntu.com", http.StatusSeeOth
er) |
| 49 return |
| 50 } |
37 s.mux.ServeHTTP(w, r) | 51 s.mux.ServeHTTP(w, r) |
38 } | 52 } |
39 | 53 |
40 type responseCharm struct { | 54 type responseCharm struct { |
41 // These are the fields effectively used by the client as of | 55 // These are the fields effectively used by the client as of |
42 // this writing. | 56 // this writing. |
43 Revision int `json:"revision"` // Zero is valid. Can't omitempty. | 57 Revision int `json:"revision"` // Zero is valid. Can't omitempty. |
44 Sha256 string `json:"sha256,omitempty"` | 58 Sha256 string `json:"sha256,omitempty"` |
45 Errors []string `json:"errors,omitempty"` | 59 Errors []string `json:"errors,omitempty"` |
46 Warnings []string `json:"warnings,omitempty"` | 60 Warnings []string `json:"warnings,omitempty"` |
47 } | 61 } |
48 | 62 |
| 63 func statsEnabled(req *http.Request) bool { |
| 64 // It's fine to parse the form more than once, and it avoids |
| 65 // bugs from not parsing it. |
| 66 req.ParseForm() |
| 67 return req.Form.Get("stats") != "0" |
| 68 } |
| 69 |
| 70 func charmStatsKey(curl *charm.URL, kind string) []string { |
| 71 if curl.User == "" { |
| 72 return []string{kind, curl.Series, curl.Name} |
| 73 } |
| 74 return []string{kind, curl.Series, curl.Name, curl.User} |
| 75 } |
| 76 |
49 func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) { | 77 func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) { |
50 if r.URL.Path != "/charm-info" { | 78 if r.URL.Path != "/charm-info" { |
51 w.WriteHeader(http.StatusNotFound) | 79 w.WriteHeader(http.StatusNotFound) |
52 return | 80 return |
53 } | 81 } |
54 r.ParseForm() | 82 r.ParseForm() |
55 response := map[string]*responseCharm{} | 83 response := map[string]*responseCharm{} |
56 for _, url := range r.Form["charms"] { | 84 for _, url := range r.Form["charms"] { |
57 » » r := &responseCharm{} | 85 » » c := &responseCharm{} |
58 » » response[url] = r | 86 » » response[url] = c |
59 curl, err := charm.ParseURL(url) | 87 curl, err := charm.ParseURL(url) |
60 var info *CharmInfo | 88 var info *CharmInfo |
61 if err == nil { | 89 if err == nil { |
62 info, err = s.store.CharmInfo(curl) | 90 info, err = s.store.CharmInfo(curl) |
63 } | 91 } |
| 92 var skey []string |
64 if err == nil { | 93 if err == nil { |
65 » » » r.Sha256 = info.BundleSha256() | 94 » » » skey = charmStatsKey(curl, "charm-info") |
66 » » » r.Revision = info.Revision() | 95 » » » c.Sha256 = info.BundleSha256() |
| 96 » » » c.Revision = info.Revision() |
67 } else { | 97 } else { |
68 » » » r.Errors = append(r.Errors, err.Error()) | 98 » » » if err == ErrNotFound { |
| 99 » » » » skey = charmStatsKey(curl, "charm-missing") |
| 100 » » » } |
| 101 » » » c.Errors = append(c.Errors, err.Error()) |
| 102 » » } |
| 103 » » if skey != nil && statsEnabled(r) { |
| 104 » » » go s.store.IncCounter(skey) |
69 } | 105 } |
70 } | 106 } |
71 data, err := json.Marshal(response) | 107 data, err := json.Marshal(response) |
72 if err == nil { | 108 if err == nil { |
73 w.Header().Set("Content-Type", "application/json") | 109 w.Header().Set("Content-Type", "application/json") |
74 _, err = w.Write(data) | 110 _, err = w.Write(data) |
75 } | 111 } |
76 if err != nil { | 112 if err != nil { |
77 log.Printf("can't write content: %v", err) | 113 log.Printf("can't write content: %v", err) |
78 w.WriteHeader(http.StatusInternalServerError) | 114 w.WriteHeader(http.StatusInternalServerError) |
79 return | 115 return |
80 } | 116 } |
81 } | 117 } |
82 | 118 |
83 func (s *Server) serveCharm(w http.ResponseWriter, r *http.Request) { | 119 func (s *Server) serveCharm(w http.ResponseWriter, r *http.Request) { |
84 if !strings.HasPrefix(r.URL.Path, "/charm/") { | 120 if !strings.HasPrefix(r.URL.Path, "/charm/") { |
85 panic("serveCharm: bad url") | 121 panic("serveCharm: bad url") |
86 } | 122 } |
87 curl, err := charm.ParseURL("cs:" + r.URL.Path[len("/charm/"):]) | 123 curl, err := charm.ParseURL("cs:" + r.URL.Path[len("/charm/"):]) |
88 if err != nil { | 124 if err != nil { |
89 w.WriteHeader(http.StatusNotFound) | 125 w.WriteHeader(http.StatusNotFound) |
90 return | 126 return |
91 } | 127 } |
92 » _, rc, err := s.store.OpenCharm(curl) | 128 » info, rc, err := s.store.OpenCharm(curl) |
93 if err == ErrNotFound { | 129 if err == ErrNotFound { |
94 w.WriteHeader(http.StatusNotFound) | 130 w.WriteHeader(http.StatusNotFound) |
95 return | 131 return |
96 } | 132 } |
97 if err != nil { | 133 if err != nil { |
98 w.WriteHeader(http.StatusInternalServerError) | 134 w.WriteHeader(http.StatusInternalServerError) |
99 log.Printf("can't open charm %q: %v", curl, err) | 135 log.Printf("can't open charm %q: %v", curl, err) |
100 return | 136 return |
101 } | 137 } |
| 138 if statsEnabled(r) { |
| 139 go s.store.IncCounter(charmStatsKey(curl, "charm-bundle")) |
| 140 } |
102 defer rc.Close() | 141 defer rc.Close() |
| 142 w.Header().Set("Connection", "close") // No keep-alive for now. |
103 w.Header().Set("Content-Type", "application/octet-stream") | 143 w.Header().Set("Content-Type", "application/octet-stream") |
| 144 w.Header().Set("Content-Length", strconv.FormatInt(info.BundleSize(), 10
)) |
104 _, err = io.Copy(w, rc) | 145 _, err = io.Copy(w, rc) |
105 if err != nil { | 146 if err != nil { |
106 log.Printf("failed to stream charm %q: %v", curl, err) | 147 log.Printf("failed to stream charm %q: %v", curl, err) |
107 } | 148 } |
108 } | 149 } |
| 150 |
| 151 func (s *Server) serveStats(w http.ResponseWriter, r *http.Request) { |
| 152 // TODO: Adopt a smarter mux that simplifies this logic. |
| 153 const dir = "/stats/counter/" |
| 154 if !strings.HasPrefix(r.URL.Path, dir) { |
| 155 panic("bad url") |
| 156 } |
| 157 base := r.URL.Path[len(dir):] |
| 158 if strings.Index(base, "/") > 0 { |
| 159 w.WriteHeader(http.StatusNotFound) |
| 160 return |
| 161 } |
| 162 if base == "" { |
| 163 w.WriteHeader(http.StatusForbidden) |
| 164 return |
| 165 } |
| 166 key := strings.Split(base, ":") |
| 167 prefix := false |
| 168 if key[len(key)-1] == "*" { |
| 169 prefix = true |
| 170 key = key[:len(key)-1] |
| 171 if len(key) == 0 { |
| 172 // No point in counting something unknown. |
| 173 w.WriteHeader(http.StatusForbidden) |
| 174 return |
| 175 } |
| 176 } |
| 177 r.ParseForm() |
| 178 sum, err := s.store.SumCounter(key, prefix) |
| 179 if err != nil { |
| 180 log.Printf("can't sum counter: %v", err) |
| 181 w.WriteHeader(http.StatusInternalServerError) |
| 182 return |
| 183 } |
| 184 data := []byte(strconv.FormatInt(sum, 10)) |
| 185 w.Header().Set("Content-Type", "text/plain") |
| 186 w.Header().Set("Content-Length", strconv.Itoa(len(data))) |
| 187 _, err = w.Write(data) |
| 188 if err != nil { |
| 189 log.Printf("can't write content: %v", err) |
| 190 w.WriteHeader(http.StatusInternalServerError) |
| 191 } |
| 192 } |
| 193 |
| 194 func (s *Server) serveBlitzKey(w http.ResponseWriter, r *http.Request) { |
| 195 w.Header().Set("Connection", "close") |
| 196 w.Header().Set("Content-Type", "text/plain") |
| 197 w.Header().Set("Content-Length", "2") |
| 198 w.Write([]byte("42")) |
| 199 } |
LEFT | RIGHT |