diff --git a/cmd/liber/liber.go b/cmd/liber/liber.go index d2a2d348eb0ded9a03851b2c207b11716edc3155..c25ef769b8cc670073aff8f924ff940039d4fa10 100644 --- a/cmd/liber/liber.go +++ b/cmd/liber/liber.go @@ -168,11 +168,9 @@ func doSearch(db *liber.Database, query string) { } func doHttpServer(db *liber.Database, addr string) { - storage := &liber.FileStorage{ - Root: *bookDir, - Nesting: 2, - } - server := liber.NewHttpServer(db, storage, addr) + storage := liber.NewFileStorage(expandTilde(*bookDir), 2) + cache := liber.NewFileStorage(filepath.Join(expandTilde(*databaseDir), "cache"), 2) + server := liber.NewHttpServer(db, storage, cache, addr) log.Fatal(server.ListenAndServe()) } @@ -217,7 +215,7 @@ func main() { log.SetOutput(logf) } - doUpdate(db, *bookDir) + doUpdate(db, expandTilde(*bookDir)) } else if *remotesync != "" { doSync(db, *remotesync) } else if *search { diff --git a/database.go b/database.go index 0b7ef2b8e6e45f2b07b079d2eaaabc4d71de7185..fb88395b798bbcb268b0467ec5eadc984643ac73 100644 --- a/database.go +++ b/database.go @@ -2,6 +2,7 @@ package liber import ( "bytes" + cryptorand "crypto/rand" "encoding/binary" "encoding/json" "errors" @@ -34,6 +35,13 @@ func (id BookId) Key() []byte { return buf.Bytes() } +func init() { + // Seed the RNG to a random value. + var seed int64 + binary.Read(cryptorand.Reader, binary.LittleEndian, &seed) + rand.Seed(seed) +} + func NewID() BookId { return BookId(rand.Int63()) } diff --git a/files.go b/files.go index 013f2e4473dae58801c38e99d957e63ef7f4ac89..2f399f2be868179cdb74b73b11c5b9ab956a508f 100644 --- a/files.go +++ b/files.go @@ -11,6 +11,13 @@ type FileStorage struct { Nesting int } +func NewFileStorage(root string, nesting int) *FileStorage { + return &FileStorage{ + Root: root, + Nesting: nesting, + } +} + // Path of the file corresponding to the given key, relative to the // root directory. func (s *FileStorage) Path(key string) string { diff --git a/htdocs/templates/_book.html b/htdocs/templates/_book.html index 51b92fd6bdffd93c72b2158e61507028ea371eae..bb154cee1d38bfd1687b13c6e04e59506c8f766e 100644 --- a/htdocs/templates/_book.html +++ b/htdocs/templates/_book.html @@ -2,7 +2,7 @@ <div class="row"> <div class="book-icon"> {{if .CoverPath}} - <img src="/book/cover/{{.Id}}"> + <img src="/book/thumb/{{.Id}}"> {{end}} </div> <div class="book-body"> diff --git a/web.go b/web.go index 6f2d0f9aca3e7c79fe12e3684ba84c687d6766c2..3a173c9db76301e5e4572235012ab42f1187b68f 100644 --- a/web.go +++ b/web.go @@ -9,6 +9,7 @@ import ( "io" "log" "net/http" + "os/exec" "path/filepath" "strconv" "strings" @@ -26,6 +27,7 @@ var ( type uiServer struct { db *Database storage *FileStorage + cache *FileStorage } type pagination struct { @@ -186,6 +188,46 @@ func (s *uiServer) handleShowCover(book *Book, w http.ResponseWriter, req *http. http.ServeContent(w, req, "", time.Time{}, f) } +const thumbnailSize = "150x150" + +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 := exec.Command("convert", book.CoverPath, "-geometry", thumbnailSize, + "-quality", "60", "jpeg:-").Output() + 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) } @@ -199,7 +241,7 @@ func render(templateName string, w http.ResponseWriter, ctx interface{}) { io.Copy(w, &buf) } -func NewHttpServer(db *Database, storage *FileStorage, addr string) *http.Server { +func NewHttpServer(db *Database, storage, cache *FileStorage, addr string) *http.Server { var err error tpl, err = template.New("liber").Funcs(template.FuncMap{ "join": strings.Join, @@ -209,7 +251,7 @@ func NewHttpServer(db *Database, storage *FileStorage, addr string) *http.Server } syncsrv := &syncServer{db, storage} - uisrv := &uiServer{db, storage} + uisrv := &uiServer{db, storage, cache} r := mux.NewRouter() r.Handle("/static/{path:.*}", http.StripPrefix("/static/", @@ -217,6 +259,7 @@ func NewHttpServer(db *Database, storage *FileStorage, addr string) *http.Server 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("/dl/{id:[0-9]+}", uisrv.withBook(uisrv.handleDownloadBook)) r.HandleFunc("/suggest", uisrv.handleAutocomplete)