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