Select Git revision
layout.html
-
David Wilson authored
New template breaks sidebar somehow, don't care, don't like new theme, so using own theme.
David Wilson authoredNew template breaks sidebar somehow, don't care, don't like new theme, so using own theme.
web.go 9.84 KiB
package liber
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"html/template"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"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) handleDumpDatabase(w http.ResponseWriter, req *http.Request) {
// Only allow requests from localhost.
host, _, _ := net.SplitHostPort(req.RemoteAddr)
ip := net.ParseIP(host)
if !ip.IsLoopback() {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
s.db.Dump(w)
}
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.HandleFunc("/internal/dump_db", uisrv.handleDumpDatabase)
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,
}
}