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 }