package webserver
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
fp "path/filepath"
"strconv"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/go-shiori/warc"
"github.com/julienschmidt/httprouter"
"github.com/go-shiori/shiori/internal/model"
)
// ServeBookmarkContent is handler for GET /bookmark/:id/content
func (h *Handler) ServeBookmarkContent(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
ctx := r.Context()
// Get bookmark ID from URL
strID := ps.ByName("id")
id, err := strconv.Atoi(strID)
checkError(err)
// Get bookmark in database
bookmark, exist, err := h.DB.GetBookmark(ctx, id, "")
checkError(err)
if !exist {
panic(fmt.Errorf("bookmark not found"))
}
// If it's not public, make sure session still valid
if bookmark.Public != 1 {
err = h.validateSession(r)
if err != nil {
newPath := path.Join(h.RootPath, "/login")
redirectURL := createRedirectURL(newPath, r.URL.String())
redirectPage(w, r, redirectURL)
return
}
}
// Check if it has archive.
ebookPath := fp.Join(h.DataDir, "ebook", strID+".epub")
if fileExists(ebookPath) {
bookmark.HasEbook = true
}
archivePath := fp.Join(h.DataDir, "archive", strID)
if fileExists(archivePath) {
bookmark.HasArchive = true
// Open archive, look in cache first
var archive *warc.Archive
cacheData, found := h.ArchiveCache.Get(strID)
if found {
archive = cacheData.(*warc.Archive)
} else {
archivePath := fp.Join(h.DataDir, "archive", strID)
archive, err = warc.Open(archivePath)
checkError(err)
h.ArchiveCache.Set(strID, archive, 0)
}
// Find all image and convert its source to use the archive URL.
createArchivalURL := func(archivalName string) string {
archivalURL := *r.URL
archivalURL.Path = path.Join(h.RootPath, "bookmark", strID, "archive", archivalName)
return archivalURL.String()
}
buffer := strings.NewReader(bookmark.HTML)
doc, err := goquery.NewDocumentFromReader(buffer)
checkError(err)
doc.Find("img, picture, figure, source").Each(func(_ int, node *goquery.Selection) {
// Get the needed attributes
src, _ := node.Attr("src")
strSrcSets, _ := node.Attr("srcset")
// Convert `src` attributes
if src != "" {
archivalName := getArchivalName(src)
if archivalName != "" && archive.HasResource(archivalName) {
node.SetAttr("src", createArchivalURL(archivalName))
}
}
// Split srcset by comma, then process it like any URLs
srcSets := strings.Split(strSrcSets, ",")
for i, srcSet := range srcSets {
srcSet = strings.TrimSpace(srcSet)
parts := strings.SplitN(srcSet, " ", 2)
if parts[0] == "" {
continue
}
archivalName := getArchivalName(parts[0])
if archivalName != "" && archive.HasResource(archivalName) {
archivalURL := createArchivalURL(archivalName)
srcSets[i] = strings.Replace(srcSets[i], parts[0], archivalURL, 1)
}
}
if len(srcSets) > 0 {
node.SetAttr("srcset", strings.Join(srcSets, ","))
}
})
bookmark.HTML, err = goquery.OuterHtml(doc.Selection)
checkError(err)
}
// Execute template
if developmentMode {
if err := h.PrepareTemplates(); err != nil {
log.Printf("error during template preparation: %s", err)
}
}
tplData := struct {
RootPath string
Book model.Bookmark
}{h.RootPath, bookmark}
err = h.templates["content"].Execute(w, &tplData)
checkError(err)
}
// ServeThumbnailImage is handler for GET /bookmark/:id/thumb
func (h *Handler) ServeThumbnailImage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get bookmark ID from URL
ctx := r.Context()
// Get bookmark ID from URL
strID := ps.ByName("id")
// Get bookmark from database
id, err := strconv.Atoi(strID)
checkError(err)
bookmark, exist, err := h.DB.GetBookmark(ctx, id, "")
checkError(err)
if !exist {
log.Println("error: bookmark not found")
return
}
// If it's not public, make sure session still valid
if bookmark.Public != 1 {
err = h.validateSession(r)
if err != nil {
newPath := path.Join(h.RootPath, "/login")
redirectURL := createRedirectURL(newPath, r.URL.String())
redirectPage(w, r, redirectURL)
return
}
}
// Open image
imgPath := fp.Join(h.DataDir, "thumb", strID)
img, err := os.Open(imgPath)
checkError(err)
defer img.Close()
// Get image type from its 512 first bytes
buffer := make([]byte, 512)
_, err = img.Read(buffer)
checkError(err)
mimeType := http.DetectContentType(buffer)
w.Header().Set("Content-Type", mimeType)
// Set cache value
info, err := img.Stat()
checkError(err)
etag := fmt.Sprintf(`W/"%x-%x"`, info.ModTime().Unix(), info.Size())
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "max-age=86400")
// Serve image
if _, err := img.Seek(0, 0); err != nil {
log.Printf("error during image seek: %s", err)
}
_, err = io.Copy(w, img)
checkError(err)
}
// ServeBookmarkArchive is handler for GET /bookmark/:id/archive/*filepath
func (h *Handler) ServeBookmarkArchive(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
ctx := r.Context()
// Get parameter from URL
strID := ps.ByName("id")
resourcePath := ps.ByName("filepath")
resourcePath = strings.TrimPrefix(resourcePath, "/")
// Get bookmark from database
id, err := strconv.Atoi(strID)
checkError(err)
bookmark, exist, err := h.DB.GetBookmark(ctx, id, "")
checkError(err)
if !exist {
panic(fmt.Errorf("bookmark not found"))
}
// If it's not public, make sure session still valid
if bookmark.Public != 1 {
err = h.validateSession(r)
if err != nil {
newPath := path.Join(h.RootPath, "/login")
redirectURL := createRedirectURL(newPath, r.URL.String())
redirectPage(w, r, redirectURL)
return
}
}
// Open archive, look in cache first
var archive *warc.Archive
cacheData, found := h.ArchiveCache.Get(strID)
if found {
archive = cacheData.(*warc.Archive)
} else {
archivePath := fp.Join(h.DataDir, "archive", strID)
archive, err = warc.Open(archivePath)
checkError(err)
h.ArchiveCache.Set(strID, archive, 0)
}
content, contentType, err := archive.Read(resourcePath)
checkError(err)
// Set response header
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Type", contentType)
// If this is HTML and root, inject shiori header
if strings.Contains(strings.ToLower(contentType), "text/html") && resourcePath == "" {
// Extract gzip
buffer := bytes.NewBuffer(content)
gzipReader, err := gzip.NewReader(buffer)
checkError(err)
// Parse gzipped content
doc, err := goquery.NewDocumentFromReader(gzipReader)
checkError(err)
// Add Shiori overlay
tplOutput := bytes.NewBuffer(nil)
err = h.templates["archive"].Execute(tplOutput, &bookmark)
checkError(err)
archiveCSSPath := path.Join(h.RootPath, "/assets/css/archive.css")
sourceSansProCSSPath := path.Join(h.RootPath, "/assets/css/source-sans-pro.min.css")
docHead := doc.Find("head")
docHead.PrependHtml(``)
docHead.AppendHtml(``)
docHead.AppendHtml(``)
doc.Find("body").PrependHtml(tplOutput.String())
// Revert back to HTML
outerHTML, err := goquery.OuterHtml(doc.Selection)
checkError(err)
// Gzip it again and send to response writer
gzipWriter := gzip.NewWriter(w)
if _, err := gzipWriter.Write([]byte(outerHTML)); err != nil {
log.Printf("error writting gzip file: %s", err)
}
gzipWriter.Flush()
return
}
// Serve content
if _, err := w.Write(content); err != nil {
log.Printf("error writting response: %s", err)
}
}
// serveEbook is handler for GET /bookmark/:id/ebook
func (h *Handler) ServeBookmarkEbook(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
ctx := r.Context()
// Get bookmark ID from URL
strID := ps.ByName("id")
id, err := strconv.Atoi(strID)
checkError(err)
// Get bookmark in database
bookmark, exist, err := h.DB.GetBookmark(ctx, id, "")
checkError(err)
if !exist {
http.Error(w, "bookmark not found", http.StatusNotFound)
return
}
// If it's not public, make sure session still valid
if bookmark.Public != 1 {
err = h.validateSession(r)
if err != nil {
newPath := path.Join(h.RootPath, "/login")
redirectURL := createRedirectURL(newPath, r.URL.String())
redirectPage(w, r, redirectURL)
return
}
}
// Check if it has ebook.
ebookPath := fp.Join(h.DataDir, "ebook", strID+".epub")
if !fileExists(ebookPath) {
http.Error(w, "ebook not found", http.StatusNotFound)
return
}
epub, err := os.Open(ebookPath)
if err != nil {
http.Error(w, "Internal server error", http.StatusNotFound)
return
}
defer epub.Close()
// Set content type
w.Header().Set("Content-Type", "application/epub+zip")
// Set cache value
info, err := epub.Stat()
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
etag := fmt.Sprintf(`W/"%x-%x"`, info.ModTime().Unix(), info.Size())
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "max-age=86400")
// Serve epub file
if _, err := epub.Seek(0, 0); err != nil {
log.Printf("error during epub seek: %s", err)
}
_, err = io.Copy(w, epub)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}