web.go 9.52 KiB
package liber
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"html/template"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"git.autistici.org/ale/liber/Godeps/_workspace/src/github.com/gorilla/mux"
"git.autistici.org/ale/liber/Godeps/_workspace/src/github.com/nfnt/resize"
)
var (
htdocsDir = flag.String("htdocs", "/usr/share/liber/htdocs", "location of static html content")
tpl *template.Template
)
type uiServer struct {
db *Database
storage *RWFileStorage
cache *RWFileStorage
}
type pagination struct {
PageSize int
Page int
Total int
}
func (p *pagination) IsFirstPage() bool {
return p.Page == 0
}
func (p *pagination) IsLastPage() bool {
return (p.PageSize * (p.Page + 1)) >= p.Total
}
func (p *pagination) Prev() int {
return p.Page - 1
}
func (p *pagination) Next() int {
return p.Page + 1
}
func (p *pagination) Start() int {
return (p.PageSize * p.Page) + 1
}
func (p *pagination) End() int {
end := (p.PageSize*(p.Page+1) - 1)
if end >= p.Total {
end = p.Total - 1
}
return end + 1
}
func (s *uiServer) handleSearch(w http.ResponseWriter, req *http.Request) {
query := req.FormValue("q")
page, _ := strconv.Atoi(req.FormValue("p"))
pageSize := 15
result, err := s.db.Search(query, page*pageSize, pageSize)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ctx := struct {
Query string
Results []*Book
Pagination *pagination
}{
Query: query,
Results: result.Results,
Pagination: &pagination{
PageSize: pageSize,
Page: page,
Total: result.NumResults,
},
}
render("search.html", w, &ctx)
}
type autocompleteResult struct {
// Id has to be a string since Javascript can't reliably
// represent a uint64 (due to its usage of floating point
// for all numbers).
Id string `json:"book_id"`
Label string `json:"label"`
}
func (s *uiServer) handleSuggest(w http.ResponseWriter, req *http.Request) {
term := req.FormValue("term")
if term == "" {
http.Error(w, "No Query", http.StatusBadRequest)
return
}
result, err := s.db.Suggest(term)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var out []autocompleteResult
for _, book := range result.Results {
out = append(out, autocompleteResult{
Id: book.Id.String(),
Label: book.Metadata.String(),
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(out)
}
func (s *uiServer) withBook(f func(*Book, http.ResponseWriter, *http.Request)) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
bookid := ParseID(vars["id"])
if bookid == 0 {
http.Error(w, "Bad ID", http.StatusBadRequest)
return
}
book, err := s.db.GetBook(bookid)
if err != nil {
http.NotFound(w, req)
return
}
f(book, w, req)
})
}
func (s *uiServer) withFile(f func(*Book, *File, int, http.ResponseWriter, *http.Request)) http.HandlerFunc {
return s.withBook(func(book *Book, w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
fileIndex, err := strconv.Atoi(vars["fid"])
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
files, err := s.db.GetBookFiles(book.Id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if fileIndex < 0 || fileIndex >= len(files) {
http.NotFound(w, req)
return
}
f(book, files[fileIndex], fileIndex, w, req)
})
}
func findEpub(files []*File) int {
for i, f := range files {
if f.FileType == ".epub" {
return i
}
}
return -1
}
func (s *uiServer) handleShowBook(book *Book, w http.ResponseWriter, req *http.Request) {
files, err := s.db.GetBookFiles(book.Id)
if err != nil {
log.Printf("ShowBook(%d): %v", book.Id, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Only offer the online reader if we have an EPUB file.
epubIndex := findEpub(files)
// The book's Description, if present, should be rendered
// directly as HTML. So we deal with it separately.
ctx := struct {
Query string
Book *Book
Files []*File
Description template.HTML
HasEpub bool
EpubIndex int
}{
Book: book,
Files: files,
Description: template.HTML(book.Metadata.Description),
HasEpub: (epubIndex >= 0),
EpubIndex: epubIndex,
}
render("book.html", w, &ctx)
}
func (s *uiServer) handleReadBook(book *Book, file *File, fileIndex int, w http.ResponseWriter, req *http.Request) {
if file.FileType != ".epub" {
http.NotFound(w, req)
return
}
ctx := struct {
Book *Book
File *File
FileIndex int
}{
Book: book,
File: file,
FileIndex: fileIndex,
}
render("reader.html", w, &ctx)
}
var contentTypeMap = map[string]string{
".epub": "application/epub+zip",
".mobi": "application/octet-stream",
".pdf": "application/pdf",
}
func (s *uiServer) handleDownloadBook(book *Book, file *File, fileIndex int, w http.ResponseWriter, req *http.Request) {
f, err := s.storage.Open(file.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
w.Header().Set("ETag", book.Id.String())
w.Header().Set("Content-Type", contentTypeMap[file.FileType])
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%d%s\"", book.Id, file.FileType))
http.ServeContent(w, req, "", time.Time{}, f)
}
func (s *uiServer) handleShowCover(book *Book, w http.ResponseWriter, req *http.Request) {
if book.CoverPath == "" {
http.NotFound(w, req)
return
}
f, err := s.storage.Open(book.CoverPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
w.Header().Set("ETag", "C"+book.Id.String())
w.Header().Set("Cache-Control", "public")
http.ServeContent(w, req, "", time.Time{}, f)
}
var thumbnailSize uint = 150
func makeThumbnail(inputfile string) ([]byte, error) {
file, err := os.Open(inputfile)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
thumb := resize.Thumbnail(thumbnailSize, thumbnailSize, img, resize.Lanczos2)
var buf bytes.Buffer
if err := jpeg.Encode(&buf, thumb, &jpeg.Options{Quality: 65}); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (s *uiServer) handleShowThumbnail(book *Book, w http.ResponseWriter, req *http.Request) {
if book.CoverPath == "" {
http.NotFound(w, req)
return
}
w.Header().Set("Cache-Control", "public")
cachedPath := s.cache.Path(fmt.Sprintf("%d.thumb.jpg", book.Id))
if cachedFile, err := s.cache.Open(cachedPath); err == nil {
// Thumbnail is cached.
defer cachedFile.Close()
w.Header().Set("ETag", "T"+book.Id.String())
http.ServeContent(w, req, "", time.Time{}, cachedFile)
return
}
cachedFile, err := s.cache.Create(cachedPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cachedFile.Close()
data, err := makeThumbnail(s.storage.Abs(book.CoverPath))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Header().Set("ETag", "T"+book.Id.String())
cachedFile.Write(data)
w.Write(data)
}
func (s *uiServer) handleHome(w http.ResponseWriter, req *http.Request) {
render("index.html", w, nil)
}
func render(templateName string, w http.ResponseWriter, ctx interface{}) {
var buf bytes.Buffer
if err := tpl.ExecuteTemplate(&buf, templateName, ctx); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.Copy(w, &buf)
}
func fullURL(req *http.Request) *url.URL {
u := *req.URL
u.Host = req.Host
if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" {
u.Scheme = "https"
} else {
u.Scheme = "http"
}
return &u
}
func handleOpenSearchXml(w http.ResponseWriter, req *http.Request) {
searchURL := fullURL(req)
searchURL.Path = "/search"
searchURL.RawQuery = "q={searchTerms}&pw={startPage}"
ctx := struct {
URL string
}{searchURL.String()}
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
render("opensearch_xml.html", w, &ctx)
}
func NewHttpServer(db *Database, storage, cache *RWFileStorage, addr string) *http.Server {
var err error
tpl, err = template.New("liber").Funcs(template.FuncMap{
"join": strings.Join,
}).ParseGlob(filepath.Join(*htdocsDir, "templates", "*.html"))
if err != nil {
log.Fatal(err)
}
syncsrv := &syncServer{db, storage}
uisrv := &uiServer{db, storage, cache}
r := mux.NewRouter()
r.Handle("/static/{path:.*}", http.StripPrefix("/static/",
http.FileServer(http.Dir(filepath.Join(*htdocsDir, "static")))))
r.HandleFunc("/api/sync/upload", syncsrv.handleSyncUpload).Methods("POST")
r.HandleFunc("/api/sync/diff", syncsrv.handleDiffRequest).Methods("POST")
r.Handle("/book/cover/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowCover))
r.Handle("/book/thumb/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowThumbnail))
r.Handle("/book/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowBook))
r.Handle("/read/{id:[0-9]+}/{fid:[0-9]+}", uisrv.withFile(uisrv.handleReadBook))
r.Handle("/dl/{id:[0-9]+}/{fid:[0-9]+}", uisrv.withFile(uisrv.handleDownloadBook))
r.HandleFunc("/opensearch.xml", handleOpenSearchXml)
r.HandleFunc("/suggest", uisrv.handleSuggest)
r.HandleFunc("/search", uisrv.handleSearch)
r.HandleFunc("/", uisrv.handleHome)
return &http.Server{
Addr: addr,
Handler: r,
}
}