package archive
import (
"bufio"
"fmt"
"html"
"io/ioutil"
"math"
"net/http"
"net/url"
"os"
"regexp"
"strings"
)
const (
pageNotSet int = -1
)
type ArchiveState struct {
svr *ArchiveServer
log *LogSource
logBestSlug string
query string
queryIsRegex bool
ym YearMonth
page int
highestPage int
}
func NewArchiveState(svr *ArchiveServer) *ArchiveState {
return &ArchiveState{
svr: svr,
page: pageNotSet,
}
}
func (this *ArchiveState) URL() string {
if len(this.query) > 0 {
if this.queryIsRegex {
return fmt.Sprintf(`/%s/rx/%s`, this.logBestSlug, url.QueryEscape(this.query))
} else {
return fmt.Sprintf(`/%s/search/%s`, this.logBestSlug, url.QueryEscape(this.query))
}
} else {
if this.page == pageNotSet {
return fmt.Sprintf(`/%s/%s/%s`, this.logBestSlug, this.ym.Year, this.ym.Month)
} else {
return fmt.Sprintf(`/%s/%s/%s/page-%d`, this.logBestSlug, this.ym.Year, this.ym.Month, this.page)
}
}
}
func (this *ArchiveState) selectSource(log *LogSource, slug string) {
this.log = log
this.logBestSlug = slug
}
// renderView renders a single page of log entries.
// - Mandatory: log, ym
// - Optional: page
func (this *ArchiveState) renderView(w http.ResponseWriter) {
fname, err := this.svr.LogFile(this.log, this.ym)
if err != nil {
this.renderError(w, err.Error())
return
}
fc, err := ioutil.ReadFile(fname)
if err != nil {
this.renderError(w, err.Error())
return
}
lines := strings.Split(string(fc), "\n")
// Work around #newpage: if the last line is blank, skip
if len(lines) > 0 && len(lines[len(lines)-1]) == 0 {
lines = lines[:len(lines)-1]
}
this.highestPage = int(math.Ceil(float64(len(lines))/float64(this.svr.cfg.LinesPerPage))) - 1
if this.page == pageNotSet || this.page > this.highestPage {
this.page = this.highestPage
}
if this.page < 0 {
this.page = 0
}
startLine := this.svr.cfg.LinesPerPage * this.page
endLine := this.svr.cfg.LinesPerPage * (this.page + 1)
if endLine > len(lines) {
endLine = len(lines)
}
output := ""
for i := startLine; i < endLine; i += 1 {
output += html.EscapeString(lines[i]) + "\n"
}
this.renderTemplate(w, []byte(output))
}
// renderSearch renders the search results.
// - Mandatory: log, query, queryIsRegex
func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
var matcher func(line string) bool
if this.queryIsRegex {
rx, err := regexp.Compile(`(?i)` + this.query)
if err != nil {
this.renderError(w, "Invalid regular expression "+this.query+" ("+err.Error()+")")
return
}
matcher = rx.MatchString
} else {
queryLower := strings.ToLower(this.query)
matcher = func(line string) bool {
return strings.Contains(strings.ToLower(line), queryLower)
}
}
this.renderTemplateHead(w)
totalResults := 0
w.Write([]byte(`
`))
limit := this.log.LatestDate().Next() // one off the end
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
fname, err := this.svr.LogFile(this.log, ympair)
if err != nil {
continue // no log exists for this ym
}
fh, err := os.Open(fname)
if err != nil {
continue // can't open this log file
}
func() {
defer fh.Close()
scanner := bufio.NewScanner(fh)
for i := 0; scanner.Scan(); i += 1 {
if !matcher(scanner.Text()) {
continue
}
totalResults += 1
page := i / this.svr.cfg.LinesPerPage
lineNo := i % this.svr.cfg.LinesPerPage
url := fmt.Sprintf(`/%s/%d/%d/page-%d#line-%d`, this.logBestSlug, ympair.Year, ympair.Month, page, lineNo)
w.Write([]byte(`- » ` + html.EscapeString(scanner.Text()) + `
`))
}
}()
}
w.Write([]byte(`
`))
if totalResults == 0 {
w.Write([]byte(`No search results for "` + html.EscapeString(this.query) + `"`))
} else {
w.Write([]byte(`
Found ` + fmt.Sprintf("%d", totalResults) + ` total result(s).
`))
}
this.renderTemplateFoot(w)
}
// renderError renders a plain text string, escaping it for HTML use.
func (this *ArchiveState) renderError(w http.ResponseWriter, msg string) {
this.renderTemplate(w, []byte(html.EscapeString(msg)))
}
func (this *ArchiveState) renderTemplate(w http.ResponseWriter, body []byte) {
this.renderTemplateHead(w)
w.Write(body)
this.renderTemplateFoot(w)
}
func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
w.Header().Set(`Content-Type`, `text/html; charset=UTF-8`)
w.Header().Set(`X-UA-Compatible`, `IE=Edge`)
w.WriteHeader(200)
title := `Archives`
if this.log != nil {
title = this.log.Description + ` Archives`
}
showPageURLs := (this.log != nil && len(this.query) == 0)
latestUrl := `/`
if this.log != nil {
latestUrl = fmt.Sprintf(`/%s/%d/%d`, url.PathEscape(this.logBestSlug), this.log.LatestDate().Year, this.log.LatestDate().Month)
}
w.Write([]byte(`
` + html.EscapeString(title) + `
`))
if showPageURLs {
w.Write([]byte(`
`))
//
pageBase := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, this.ym.Year, this.ym.Month)
previousPage := this.page - 1
if previousPage < 0 {
previousPage = 0
}
nextPage := this.page + 1
if nextPage > this.highestPage {
nextPage = this.highestPage
}
prevMonth := this.ym.Prev() // FIXME check bounds
prevMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, prevMonth.Year, prevMonth.Month)
nextMonth := this.ym.Next() // FIXME check bounds
nextMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, nextMonth.Year, nextMonth.Month)
w.Write([]byte(`
«
‹
` + fmt.Sprintf("%d", this.page) + `
›
»
`))
}
w.Write([]byte(`
`,
))
// Header ends
}
func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) {
w.Write([]byte(`
`))
}