github.com/soulteary/pocket-bookcase@v0.0.0-20240428065142-0b5a9a0fc98a/internal/http/routes/api/v1/bookmarks.go (about) 1 package api_v1 2 3 import ( 4 "fmt" 5 "net/http" 6 "os" 7 fp "path/filepath" 8 "strconv" 9 "sync" 10 11 "github.com/gin-gonic/gin" 12 "github.com/sirupsen/logrus" 13 "github.com/soulteary/pocket-bookcase/internal/core" 14 "github.com/soulteary/pocket-bookcase/internal/database" 15 "github.com/soulteary/pocket-bookcase/internal/dependencies" 16 "github.com/soulteary/pocket-bookcase/internal/http/context" 17 "github.com/soulteary/pocket-bookcase/internal/http/middleware" 18 "github.com/soulteary/pocket-bookcase/internal/http/response" 19 "github.com/soulteary/pocket-bookcase/internal/model" 20 ) 21 22 type BookmarksAPIRoutes struct { 23 logger *logrus.Logger 24 deps *dependencies.Dependencies 25 } 26 27 func (r *BookmarksAPIRoutes) Setup(g *gin.RouterGroup) model.Routes { 28 g.Use(middleware.AuthenticationRequired()) 29 g.PUT("/cache", r.updateCache) 30 g.GET("/:id/readable", r.bookmarkReadable) 31 return r 32 } 33 34 func NewBookmarksAPIRoutes(logger *logrus.Logger, deps *dependencies.Dependencies) *BookmarksAPIRoutes { 35 return &BookmarksAPIRoutes{ 36 logger: logger, 37 deps: deps, 38 } 39 } 40 41 type updateCachePayload struct { 42 Ids []int `json:"ids" validate:"required"` 43 KeepMetadata bool `json:"keep_metadata"` 44 CreateArchive bool `json:"create_archive"` 45 CreateEbook bool `json:"create_ebook"` 46 SkipExist bool `json:"skip_exist"` 47 } 48 49 func (p *updateCachePayload) IsValid() error { 50 if len(p.Ids) == 0 { 51 return fmt.Errorf("id should not be empty") 52 } 53 for _, id := range p.Ids { 54 if id <= 0 { 55 return fmt.Errorf("id should not be 0 or negative") 56 } 57 } 58 return nil 59 } 60 61 func (r *BookmarksAPIRoutes) getBookmark(c *context.Context) (*model.BookmarkDTO, error) { 62 bookmarkIDParam, present := c.Params.Get("id") 63 if !present { 64 response.SendError(c.Context, http.StatusBadRequest, "Invalid bookmark ID") 65 return nil, model.ErrBookmarkInvalidID 66 } 67 68 bookmarkID, err := strconv.Atoi(bookmarkIDParam) 69 if err != nil { 70 r.logger.WithError(err).Error("error parsing bookmark ID parameter") 71 response.SendInternalServerError(c.Context) 72 return nil, err 73 } 74 75 if bookmarkID == 0 { 76 response.SendError(c.Context, http.StatusNotFound, nil) 77 return nil, model.ErrBookmarkNotFound 78 } 79 80 bookmark, err := r.deps.Domains.Bookmarks.GetBookmark(c.Context, model.DBID(bookmarkID)) 81 if err != nil { 82 response.SendError(c.Context, http.StatusNotFound, nil) 83 return nil, model.ErrBookmarkNotFound 84 } 85 86 return bookmark, nil 87 } 88 89 type readableResponseMessage struct { 90 Content string `json:"content"` 91 Html string `json:"html"` 92 } 93 94 // Bookmark Readable godoc 95 // 96 // @Summary Get readable version of bookmark. 97 // @Tags Auth 98 // @securityDefinitions.apikey ApiKeyAuth 99 // @Produce json 100 // @Success 200 {object} contentResponseMessage 101 // @Failure 403 {object} nil "Token not provided/invalid" 102 // @Router /api/v1/bookmarks/id/readable [get] 103 func (r *BookmarksAPIRoutes) bookmarkReadable(c *gin.Context) { 104 ctx := context.NewContextFromGin(c) 105 106 bookmark, err := r.getBookmark(ctx) 107 if err != nil { 108 return 109 } 110 responseMessage := readableResponseMessage{ 111 Content: bookmark.Content, 112 Html: bookmark.HTML, 113 } 114 115 response.Send(c, 200, responseMessage) 116 } 117 118 // updateCache godoc 119 // 120 // @Summary Update Cache and Ebook on server. 121 // @Tags Auth 122 // @securityDefinitions.apikey ApiKeyAuth 123 // @Param payload body updateCachePayload true "Update Cache Payload"` 124 // @Produce json 125 // @Success 200 {object} model.BookmarkDTO 126 // @Failure 403 {object} nil "Token not provided/invalid" 127 // @Router /api/v1/bookmarks/cache [put] 128 func (r *BookmarksAPIRoutes) updateCache(c *gin.Context) { 129 ctx := context.NewContextFromGin(c) 130 if !ctx.GetAccount().Owner { 131 response.SendError(c, http.StatusForbidden, nil) 132 return 133 } 134 135 var payload updateCachePayload 136 if err := c.ShouldBindJSON(&payload); err != nil { 137 response.SendInternalServerError(c) 138 return 139 } 140 141 if err := payload.IsValid(); err != nil { 142 response.SendError(c, http.StatusBadRequest, err.Error()) 143 return 144 } 145 146 // send request to database and get bookmarks 147 filter := database.GetBookmarksOptions{ 148 IDs: payload.Ids, 149 WithContent: true, 150 } 151 152 bookmarks, err := r.deps.Database.GetBookmarks(c, filter) 153 if len(bookmarks) == 0 { 154 r.logger.WithError(err).Error("Bookmark not found") 155 response.SendError(c, 404, "Bookmark not found") 156 return 157 } 158 159 if err != nil { 160 r.logger.WithError(err).Error("error getting bookmakrs") 161 response.SendInternalServerError(c) 162 return 163 } 164 // TODO: limit request to 20 165 166 // Fetch data from internet 167 mx := sync.RWMutex{} 168 wg := sync.WaitGroup{} 169 chDone := make(chan struct{}) 170 chProblem := make(chan int, 10) 171 semaphore := make(chan struct{}, 10) 172 173 for i, book := range bookmarks { 174 wg.Add(1) 175 176 // Mark whether book will be archived or ebook generate request 177 book.CreateArchive = payload.CreateArchive 178 book.CreateEbook = payload.CreateEbook 179 180 go func(i int, book model.BookmarkDTO, keep_metadata bool) { 181 // Make sure to finish the WG 182 defer wg.Done() 183 184 // Register goroutine to semaphore 185 semaphore <- struct{}{} 186 defer func() { 187 <-semaphore 188 }() 189 190 // Download data from internet 191 content, contentType, err := core.DownloadBookmark(book.URL) 192 if err != nil { 193 chProblem <- book.ID 194 return 195 } 196 197 request := core.ProcessRequest{ 198 DataDir: r.deps.Config.Storage.DataDir, 199 Bookmark: book, 200 Content: content, 201 ContentType: contentType, 202 KeepTitle: keep_metadata, 203 KeepExcerpt: keep_metadata, 204 } 205 206 if payload.SkipExist && book.CreateEbook { 207 strID := strconv.Itoa(book.ID) 208 ebookPath := fp.Join(request.DataDir, "ebook", strID+".epub") 209 _, err = os.Stat(ebookPath) 210 if err == nil { 211 request.Bookmark.CreateEbook = false 212 request.Bookmark.HasEbook = true 213 } 214 } 215 216 book, _, err = core.ProcessBookmark(r.deps, request) 217 content.Close() 218 219 if err != nil { 220 chProblem <- book.ID 221 return 222 } 223 224 // Update list of bookmarks 225 mx.Lock() 226 bookmarks[i] = book 227 mx.Unlock() 228 }(i, book, payload.KeepMetadata) 229 } 230 // Receive all problematic bookmarks 231 idWithProblems := []int{} 232 go func() { 233 for { 234 select { 235 case <-chDone: 236 return 237 case id := <-chProblem: 238 idWithProblems = append(idWithProblems, id) 239 } 240 } 241 }() 242 243 // Wait until all download finished 244 wg.Wait() 245 close(chDone) 246 247 // Update database 248 _, err = r.deps.Database.SaveBookmarks(c, false, bookmarks...) 249 if err != nil { 250 r.logger.WithError(err).Error("error update bookmakrs on deatabas") 251 response.SendInternalServerError(c) 252 return 253 } 254 255 response.Send(c, 200, bookmarks) 256 }