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  }