diff --git a/internal/handlers/library.go b/internal/handlers/library.go index 40d1520..0cfc895 100644 --- a/internal/handlers/library.go +++ b/internal/handlers/library.go @@ -250,6 +250,25 @@ func (h *LibraryHandler) DeleteLibraryFolder(c echo.Context) error { return c.NoContent(http.StatusNoContent) } +// BrowseDirectories returns directory listings for folder browser UI +func (h *LibraryHandler) BrowseDirectories(c echo.Context) error { + path := c.QueryParam("path") + if path == "" { + path = "/" // Start from root + } + + dirs, currentPath, parentPath, err := h.libraryService.BrowseDirectories(c.Request().Context(), path) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "current_path": currentPath, + "parent_path": parentPath, + "directories": dirs, + }) +} + // SetLibraryVisibility sets library visibility for a user func (h *LibraryHandler) SetLibraryVisibility(c echo.Context) error { user := c.Get("user").(database.Users) diff --git a/internal/router/library.go b/internal/router/library.go index bf056de..f09dc4c 100644 --- a/internal/router/library.go +++ b/internal/router/library.go @@ -28,6 +28,7 @@ func registerLibraryRoutes(cfg *Config) { adminLibrary := library.Group("", handlers.AdminMiddleware) adminLibrary.POST("", cfg.LibraryHandler.CreateLibrary) adminLibrary.GET("", cfg.LibraryHandler.ListLibraries) + adminLibrary.GET("/browse", cfg.LibraryHandler.BrowseDirectories) adminLibrary.GET("/:id", cfg.LibraryHandler.GetLibrary) adminLibrary.PUT("/:id", cfg.LibraryHandler.UpdateLibrary) adminLibrary.DELETE("/:id", cfg.LibraryHandler.DeleteLibrary) diff --git a/internal/services/library_service.go b/internal/services/library_service.go index f526bc4..28e7d2a 100644 --- a/internal/services/library_service.go +++ b/internal/services/library_service.go @@ -4,6 +4,7 @@ import ( "bookhoard/internal/database" "context" "fmt" + "os" "path/filepath" "strings" @@ -207,3 +208,49 @@ func (s *LibraryService) HasFolders(ctx context.Context, libraryID pgtype.UUID) } return len(folders) > 0, nil } + +// BrowseDirectories lists directories at a given path for folder browser UI +// Returns: (directories, currentPath, parentPath, error) +func (s *LibraryService) BrowseDirectories(ctx context.Context, path string) ([]string, string, string, error) { + // Security: path traversal protection + if strings.Contains(path, "..") { + return nil, "", "", fmt.Errorf("path traversal not allowed") + } + + cleanPath := filepath.Clean(path) + + // Check if path exists and is accessible + fileInfo, err := os.Stat(cleanPath) + if err != nil { + if os.IsNotExist(err) { + return nil, "", "", fmt.Errorf("path does not exist") + } + return nil, "", "", fmt.Errorf("path not accessible: %w", err) + } + + if !fileInfo.IsDir() { + return nil, "", "", fmt.Errorf("not a directory") + } + + // Read directory contents + entries, err := os.ReadDir(cleanPath) + if err != nil { + return nil, "", "", fmt.Errorf("failed to read directory: %w", err) + } + + // Filter only directories + var dirs []string + for _, entry := range entries { + if entry.IsDir() { + dirs = append(dirs, entry.Name()) + } + } + + // Get parent path for navigation + parentPath := filepath.Dir(cleanPath) + if parentPath == cleanPath { + parentPath = "" // At root + } + + return dirs, cleanPath, parentPath, nil +}