github.com/soulteary/pocket-bookcase@v0.0.0-20240428065142-0b5a9a0fc98a/internal/http/routes/bookmark.go (about)

     1  package routes
     2  
     3  import (
     4  	"fmt"
     5  	"html/template"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/gin-gonic/gin"
    11  	"github.com/gofrs/uuid/v5"
    12  	"github.com/sirupsen/logrus"
    13  	"github.com/soulteary/pocket-bookcase/internal/dependencies"
    14  	"github.com/soulteary/pocket-bookcase/internal/http/context"
    15  	"github.com/soulteary/pocket-bookcase/internal/http/response"
    16  	"github.com/soulteary/pocket-bookcase/internal/model"
    17  )
    18  
    19  type BookmarkRoutes struct {
    20  	logger *logrus.Logger
    21  	deps   *dependencies.Dependencies
    22  }
    23  
    24  func NewBookmarkRoutes(logger *logrus.Logger, deps *dependencies.Dependencies) *BookmarkRoutes {
    25  	return &BookmarkRoutes{
    26  		logger: logger,
    27  		deps:   deps,
    28  	}
    29  }
    30  
    31  func (r *BookmarkRoutes) Setup(group *gin.RouterGroup) model.Routes {
    32  	group.GET("/:id/archive", r.bookmarkArchiveHandler)
    33  	group.GET("/:id/archive/file/*filepath", r.bookmarkArchiveFileHandler)
    34  	group.GET("/:id/content", r.bookmarkContentHandler)
    35  	group.GET("/:id/thumb", r.bookmarkThumbnailHandler)
    36  	group.GET("/:id/ebook", r.bookmarkEbookHandler)
    37  
    38  	return r
    39  }
    40  
    41  func (r *BookmarkRoutes) getBookmark(c *context.Context) (*model.BookmarkDTO, error) {
    42  	bookmarkIDParam, present := c.Params.Get("id")
    43  	if !present {
    44  		response.SendError(c.Context, http.StatusBadRequest, "Invalid bookmark ID")
    45  		return nil, model.ErrBookmarkInvalidID
    46  	}
    47  
    48  	bookmarkID, err := strconv.Atoi(bookmarkIDParam)
    49  	if err != nil {
    50  		r.logger.WithError(err).Error("error parsing bookmark ID parameter")
    51  		response.SendInternalServerError(c.Context)
    52  		return nil, err
    53  	}
    54  
    55  	if bookmarkID == 0 {
    56  		response.SendError(c.Context, http.StatusNotFound, nil)
    57  		return nil, model.ErrBookmarkNotFound
    58  	}
    59  
    60  	bookmark, err := r.deps.Domains.Bookmarks.GetBookmark(c.Context, model.DBID(bookmarkID))
    61  	if err != nil {
    62  		response.SendError(c.Context, http.StatusNotFound, nil)
    63  		return nil, model.ErrBookmarkNotFound
    64  	}
    65  
    66  	if bookmark.Public != 1 && !c.UserIsLogged() {
    67  		response.RedirectToLogin(c.Context, c.Request.URL.String())
    68  		return nil, model.ErrUnauthorized
    69  	}
    70  
    71  	return bookmark, nil
    72  }
    73  
    74  func (r *BookmarkRoutes) bookmarkContentHandler(c *gin.Context) {
    75  	ctx := context.NewContextFromGin(c)
    76  
    77  	bookmark, err := r.getBookmark(ctx)
    78  	if err != nil {
    79  		return
    80  	}
    81  
    82  	ctx.HTML(http.StatusOK, "content.html", gin.H{
    83  		"RootPath": r.deps.Config.Http.RootPath,
    84  		"Version":  model.BuildVersion,
    85  		"Book":     bookmark,
    86  		"HTML":     template.HTML(bookmark.HTML),
    87  	})
    88  }
    89  
    90  func (r *BookmarkRoutes) bookmarkArchiveHandler(c *gin.Context) {
    91  	ctx := context.NewContextFromGin(c)
    92  
    93  	bookmark, err := r.getBookmark(ctx)
    94  	if err != nil {
    95  		return
    96  	}
    97  
    98  	if !r.deps.Domains.Bookmarks.HasArchive(bookmark) {
    99  		response.NotFound(c)
   100  		return
   101  	}
   102  
   103  	c.HTML(http.StatusOK, "archive.html", gin.H{
   104  		"RootPath": r.deps.Config.Http.RootPath,
   105  		"Version":  model.BuildVersion,
   106  		"Book":     bookmark,
   107  	})
   108  }
   109  
   110  func (r *BookmarkRoutes) bookmarkArchiveFileHandler(c *gin.Context) {
   111  	ctx := context.NewContextFromGin(c)
   112  
   113  	bookmark, err := r.getBookmark(ctx)
   114  	if err != nil {
   115  		return
   116  	}
   117  
   118  	if !r.deps.Domains.Bookmarks.HasArchive(bookmark) {
   119  		response.NotFound(c)
   120  		return
   121  	}
   122  
   123  	resourcePath, _ := c.Params.Get("filepath")
   124  	resourcePath = strings.TrimPrefix(resourcePath, "/")
   125  
   126  	archive, err := r.deps.Domains.Archiver.GetBookmarkArchive(bookmark)
   127  	if err != nil {
   128  		r.logger.WithError(err).Error("error opening archive")
   129  		response.SendInternalServerError(c)
   130  		return
   131  	}
   132  	defer archive.Close()
   133  
   134  	if !archive.HasResource(resourcePath) {
   135  		response.NotFound(c)
   136  		return
   137  	}
   138  
   139  	content, resourceContentType, err := archive.Read(resourcePath)
   140  	if err != nil {
   141  		r.logger.WithError(err).Error("error reading archive file")
   142  		response.SendInternalServerError(c)
   143  		return
   144  	}
   145  
   146  	// Generate weak ETAG
   147  	shioriUUID := uuid.NewV5(uuid.NamespaceURL, model.ShioriURLNamespace)
   148  	c.Header("Etag", fmt.Sprintf("W/%s", uuid.NewV5(shioriUUID, fmt.Sprintf("%x-%x-%x", bookmark.ID, resourcePath, len(content)))))
   149  	c.Header("Cache-Control", "max-age=31536000")
   150  
   151  	c.Header("Content-Encoding", "gzip")
   152  	c.Data(http.StatusOK, resourceContentType, content)
   153  }
   154  
   155  func (r *BookmarkRoutes) bookmarkThumbnailHandler(c *gin.Context) {
   156  	ctx := context.NewContextFromGin(c)
   157  
   158  	bookmark, err := r.getBookmark(ctx)
   159  	if err != nil {
   160  		return
   161  	}
   162  
   163  	if !r.deps.Domains.Bookmarks.HasThumbnail(bookmark) {
   164  		response.NotFound(c)
   165  		return
   166  	}
   167  
   168  	response.SendFile(c, r.deps.Domains.Storage, model.GetThumbnailPath(bookmark))
   169  }
   170  
   171  func (r *BookmarkRoutes) bookmarkEbookHandler(c *gin.Context) {
   172  	ctx := context.NewContextFromGin(c)
   173  
   174  	bookmark, err := r.getBookmark(ctx)
   175  	if err != nil {
   176  		return
   177  	}
   178  
   179  	ebookPath := model.GetEbookPath(bookmark)
   180  
   181  	if !r.deps.Domains.Storage.FileExists(ebookPath) {
   182  		response.SendError(c, http.StatusNotFound, nil)
   183  		return
   184  	}
   185  
   186  	// TODO: Potentially improve this
   187  	c.Header("Content-Disposition", `attachment; filename="`+bookmark.Title+`.epub"`)
   188  	response.SendFile(c, r.deps.Domains.Storage, model.GetEbookPath(bookmark))
   189  }