Skip to content
Snippets Groups Projects
Select Git revision
  • master
1 result

layout.html

Blame
  • 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,
    	}
    }