feat: implement multiple folder ebook scanner and API handlers

- Create EbookScanner service with multiple folder support
- Update scanner to handle array of folder paths instead of single path
- Add EPUB metadata extraction for rich ebook information
- Implement folder monitoring with fsnotify for real-time updates
- Add API endpoints for folder management:
  - POST /api/auth/ebook-folders (add folder)
  - GET /api/auth/ebook-folders (list folders)
  - DELETE /api/auth/ebook-folders/:folderPath (remove folder)
- Update scanner endpoints to accept folder_paths array
- Add go-epub and fsnotify dependencies for metadata extraction and file watching
This commit is contained in:
2026-01-23 09:02:53 -05:00
parent 9019e524e0
commit 9fc5d3baea
5 changed files with 493 additions and 7 deletions
+93
View File
@@ -315,6 +315,99 @@ func (h *AuthHandler) ListUsers(c echo.Context) error {
return c.JSON(http.StatusOK, userList)
}
type AddEbookFolderRequest struct {
FolderPath string `json:"folder_path" validate:"required"`
}
type EbookFolderResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
FolderPath string `json:"folder_path"`
CreatedAt string `json:"created_at"`
}
// AddEbookFolder handles POST /api/auth/ebook-folders
func (h *AuthHandler) AddEbookFolder(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
}
var req AddEbookFolderRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
folder, err := h.db.AddUserEbookFolder(c.Request().Context(), database.AddUserEbookFolderParams{
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
FolderPath: req.FolderPath,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, EbookFolderResponse{
ID: uuid.UUID(folder.ID.Bytes).String(),
UserID: uuid.UUID(folder.UserID.Bytes).String(),
FolderPath: folder.FolderPath,
CreatedAt: folder.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
})
}
// GetEbookFolders handles GET /api/auth/ebook-folders
func (h *AuthHandler) GetEbookFolders(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
}
folders, err := h.db.GetUserEbookFolders(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
var response []EbookFolderResponse
for _, folder := range folders {
response = append(response, EbookFolderResponse{
ID: uuid.UUID(folder.ID.Bytes).String(),
UserID: uuid.UUID(folder.UserID.Bytes).String(),
FolderPath: folder.FolderPath,
CreatedAt: folder.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
})
}
return c.JSON(http.StatusOK, response)
}
// DeleteEbookFolder handles DELETE /api/auth/ebook-folders/:folderPath
func (h *AuthHandler) DeleteEbookFolder(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
}
folderPath := c.Param("folderPath")
if folderPath == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "folder path is required"})
}
err = h.db.DeleteUserEbookFolder(c.Request().Context(), database.DeleteUserEbookFolderParams{
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
FolderPath: folderPath,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"message": "ebook folder removed"})
}
func (h *AuthHandler) generateJWT(userID string) (string, error) {
claims := jwtgo.MapClaims{
"user_id": userID,