github.com/bcampbell/scrapeomat@v0.0.0-20220820232205-23e64141c89e/cmd/slurpserver/server.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "github.com/bcampbell/scrapeomat/store" 7 "github.com/elazarl/go-bindata-assetfs" 8 "github.com/gorilla/handlers" 9 "html/template" 10 "net/http" 11 //"time" 12 ) 13 14 type Logger interface { 15 Printf(format string, v ...interface{}) 16 } 17 18 func EmitError(w http.ResponseWriter, statusCode int) { 19 txt := fmt.Sprintf("%d - %s", statusCode, http.StatusText(statusCode)) 20 http.Error(w, txt, statusCode) 21 } 22 23 type nullLogger struct{} 24 25 func (l nullLogger) Printf(format string, v ...interface{}) { 26 } 27 28 type SlurpServer struct { 29 ErrLog Logger 30 InfoLog Logger 31 Port int 32 Prefix string 33 34 db store.Store 35 enableBrowse bool 36 tmpls struct { 37 browse *template.Template 38 art *template.Template 39 } 40 } 41 42 func NewServer(db store.Store, enableBrowse bool, port int, prefix string, infoLog Logger, errLog Logger) (*SlurpServer, error) { 43 srv := &SlurpServer{db: db, enableBrowse: enableBrowse, Port: port, Prefix: prefix, InfoLog: infoLog, ErrLog: errLog} 44 45 baseTmpl := string(MustAsset("templates/base.html")) 46 browseTmpl := string(MustAsset("templates/browse.html")) 47 artTmpl := string(MustAsset("templates/art.html")) 48 49 t := template.New("browse.html") 50 t.Parse(browseTmpl) 51 t.Parse(baseTmpl) 52 srv.tmpls.browse = t 53 54 t = template.New("art.html") 55 t.Parse(artTmpl) 56 t.Parse(baseTmpl) 57 srv.tmpls.art = t 58 59 return srv, nil 60 } 61 62 func (srv *SlurpServer) Run() error { 63 64 // add middleware for compressing response, and for 65 // observing Forwarded/X-Fowarded headers to get 66 // real client IP address 67 wrap := func(f http.HandlerFunc) http.Handler { 68 return handlers.ProxyHeaders( 69 handlers.CompressHandler( 70 http.HandlerFunc(f))) 71 } 72 73 http.Handle(srv.Prefix+"/api/slurp", 74 wrap( 75 func(w http.ResponseWriter, r *http.Request) { 76 srv.slurpHandler(&Context{}, w, r) 77 })) 78 79 http.Handle(srv.Prefix+"/api/pubs", 80 wrap( 81 func(w http.ResponseWriter, r *http.Request) { 82 srv.pubsHandler(&Context{}, w, r) 83 })) 84 85 http.Handle(srv.Prefix+"/api/summary", 86 wrap( 87 func(w http.ResponseWriter, r *http.Request) { 88 srv.summaryHandler(&Context{}, w, r) 89 })) 90 91 http.Handle(srv.Prefix+"/api/count", 92 wrap( 93 func(w http.ResponseWriter, r *http.Request) { 94 srv.countHandler(&Context{}, w, r) 95 })) 96 97 if srv.enableBrowse { 98 http.HandleFunc(srv.Prefix+"/browse", func(w http.ResponseWriter, r *http.Request) { 99 srv.browseHandler(w, r) 100 }) 101 http.HandleFunc(srv.Prefix+"/browse/art", func(w http.ResponseWriter, r *http.Request) { 102 srv.artHandler(w, r) 103 }) 104 105 // serve up stuff in /static 106 http.Handle(srv.Prefix+"/static/", 107 http.StripPrefix(srv.Prefix+"/static/", 108 http.FileServer( 109 &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "static"}))) 110 } 111 112 srv.InfoLog.Printf("Started at localhost:%d%s/\n", srv.Port, srv.Prefix) 113 return http.ListenAndServe(fmt.Sprintf(":%d", srv.Port), nil) 114 } 115 116 // for auth etc... one day. 117 type Context struct { 118 Prefix string 119 } 120 121 type Msg struct { 122 Article *store.Article `json:"article,omitempty"` 123 Error string `json:"error,omitempty"` 124 Next struct { 125 SinceID int `json:"since_id,omitempty"` 126 } `json:"next,omitempty"` 127 /* 128 Info struct { 129 Sent int 130 Total int 131 } `json:"info,omitempty"` 132 */ 133 } 134 135 // implement the main article slurp API 136 func (srv *SlurpServer) slurpHandler(ctx *Context, w http.ResponseWriter, r *http.Request) { 137 138 filt, err := getFilter(r) 139 if err != nil { 140 EmitError(w, 400) 141 return 142 } 143 144 // srv.InfoLog.Printf("%+v\n", filt) 145 146 /* 147 totalArts, err := srv.db.FetchCount(filt) 148 if err != nil { 149 // TODO: should send error via json 150 http.Error(w, fmt.Sprintf("DB error: %s", err), 500) 151 return 152 } 153 srv.InfoLog.Printf("%d articles to send\n", totalArts) 154 */ 155 156 err, artCnt, byteCnt := srv.performSlurp(w, filt) 157 status := "OK" 158 if err != nil { 159 status = fmt.Sprintf("FAIL (%s)", err) 160 } 161 162 srv.InfoLog.Printf("%s %s %d arts %d bytes %s\n", r.RemoteAddr, status, artCnt, byteCnt, filt.Describe()) 163 } 164 165 // helper fn 166 func writeMsg(w http.ResponseWriter, msg *Msg) (int, error) { 167 outBuf, err := json.Marshal(msg) 168 if err != nil { 169 return 0, fmt.Errorf("json encoding error: %s", err) 170 } 171 n, err := w.Write(outBuf) 172 if err != nil { 173 return n, fmt.Errorf("write error: %s", err) 174 } 175 176 return n, nil 177 } 178 179 func (srv *SlurpServer) performSlurp(w http.ResponseWriter, filt *store.Filter) (error, int, int) { 180 181 artCnt := 0 182 byteCnt := 0 183 it := srv.db.Fetch(filt) 184 defer it.Close() 185 maxID := 0 186 for it.Next() { 187 art := it.Article() 188 189 msg := Msg{Article: art} 190 n, err := writeMsg(w, &msg) 191 if err != nil { 192 return err, artCnt, byteCnt 193 } 194 byteCnt += n 195 artCnt++ 196 if art.ID > maxID { 197 maxID = art.ID 198 } 199 } 200 201 if it.Err() != nil { 202 // uhoh - some sort of database error... log and send it on to the client 203 msg := Msg{Error: fmt.Sprintf("fetch error: %s\n", it.Err)} 204 srv.ErrLog.Printf("%s\n", msg.Error) 205 n, err := writeMsg(w, &msg) 206 if err != nil { 207 return err, artCnt, byteCnt 208 } 209 byteCnt += n 210 return it.Err(), artCnt, byteCnt 211 } 212 213 // looks like more articles to fetch? 214 if artCnt == filt.Count { 215 // send a "Next" message with a new since_id 216 msg := Msg{} 217 msg.Next.SinceID = maxID 218 n, err := writeMsg(w, &msg) 219 if err != nil { 220 return err, artCnt, byteCnt 221 } 222 byteCnt += n 223 } 224 225 return nil, artCnt, byteCnt 226 } 227 228 // implement the publication list API 229 func (srv *SlurpServer) pubsHandler(ctx *Context, w http.ResponseWriter, r *http.Request) { 230 231 pubs, err := srv.db.FetchPublications() 232 if err != nil { 233 srv.ErrLog.Printf("/pubs DB Error: %s\n", err) 234 EmitError(w, 500) 235 return 236 } 237 238 out := struct { 239 Publications []store.Publication `json:"publications"` 240 }{ 241 pubs, 242 } 243 244 outBuf, err := json.Marshal(out) 245 if err != nil { 246 srv.ErrLog.Printf("/pubs json encoding error: %s\n", err) 247 EmitError(w, 500) 248 return 249 } 250 _, err = w.Write(outBuf) 251 if err != nil { 252 srv.ErrLog.Printf("Write error: %s\n", err) 253 return 254 } 255 256 srv.InfoLog.Printf("%s publications\n", r.RemoteAddr) 257 } 258 259 // implement the summary API 260 func (srv *SlurpServer) summaryHandler(ctx *Context, w http.ResponseWriter, r *http.Request) { 261 filt, err := getFilter(r) 262 if err != nil { 263 srv.ErrLog.Printf("/summary bad params: %s\n", err) 264 EmitError(w, 400) 265 return 266 } 267 268 rawCounts, err := srv.db.FetchSummary(filt, "published") 269 if err != nil { 270 srv.ErrLog.Printf("/summary DB error: %s\n", err) 271 EmitError(w, 500) 272 return 273 } 274 275 srv.InfoLog.Printf("%d summary counts\n", len(rawCounts)) 276 277 cooked := make(map[string]map[string]int) 278 279 for _, raw := range rawCounts { 280 mm, ok := cooked[raw.PubCode] 281 if !ok { 282 mm = make(map[string]int) 283 cooked[raw.PubCode] = mm 284 } 285 var day string 286 if !raw.Date.IsZero() { 287 day = raw.Date.Format("2006-01-02") 288 } 289 mm[day] = raw.Count 290 } 291 292 out := struct { 293 Counts map[string]map[string]int `json:"counts"` 294 }{ 295 cooked, 296 } 297 298 outBuf, err := json.Marshal(out) 299 if err != nil { 300 srv.ErrLog.Printf("/summary json encoding error: %s\n", err) 301 EmitError(w, 500) 302 return 303 } 304 _, err = w.Write(outBuf) 305 if err != nil { 306 srv.ErrLog.Printf("Write error: %s\n", err) 307 return 308 } 309 310 srv.InfoLog.Printf("%s summary (%d rows)\n", r.RemoteAddr, len(rawCounts)) 311 }