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  }