feat(backend): add server-side directory browsing API
- Add BrowseDirectories() to library service with path traversal protection - Add BrowseDirectories handler with proper error handling - Register GET /api/libraries/browse endpoint (admin-only) - Returns current path, parent path, and list of subdirectories - Security: blocks "..", validates path exists, checks is directory Fixes: Issue 2 (backend)
This commit is contained in:
@@ -250,6 +250,25 @@ func (h *LibraryHandler) DeleteLibraryFolder(c echo.Context) error {
|
|||||||
return c.NoContent(http.StatusNoContent)
|
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
|
// SetLibraryVisibility sets library visibility for a user
|
||||||
func (h *LibraryHandler) SetLibraryVisibility(c echo.Context) error {
|
func (h *LibraryHandler) SetLibraryVisibility(c echo.Context) error {
|
||||||
user := c.Get("user").(database.Users)
|
user := c.Get("user").(database.Users)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func registerLibraryRoutes(cfg *Config) {
|
|||||||
adminLibrary := library.Group("", handlers.AdminMiddleware)
|
adminLibrary := library.Group("", handlers.AdminMiddleware)
|
||||||
adminLibrary.POST("", cfg.LibraryHandler.CreateLibrary)
|
adminLibrary.POST("", cfg.LibraryHandler.CreateLibrary)
|
||||||
adminLibrary.GET("", cfg.LibraryHandler.ListLibraries)
|
adminLibrary.GET("", cfg.LibraryHandler.ListLibraries)
|
||||||
|
adminLibrary.GET("/browse", cfg.LibraryHandler.BrowseDirectories)
|
||||||
adminLibrary.GET("/:id", cfg.LibraryHandler.GetLibrary)
|
adminLibrary.GET("/:id", cfg.LibraryHandler.GetLibrary)
|
||||||
adminLibrary.PUT("/:id", cfg.LibraryHandler.UpdateLibrary)
|
adminLibrary.PUT("/:id", cfg.LibraryHandler.UpdateLibrary)
|
||||||
adminLibrary.DELETE("/:id", cfg.LibraryHandler.DeleteLibrary)
|
adminLibrary.DELETE("/:id", cfg.LibraryHandler.DeleteLibrary)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -207,3 +208,49 @@ func (s *LibraryService) HasFolders(ctx context.Context, libraryID pgtype.UUID)
|
|||||||
}
|
}
|
||||||
return len(folders) > 0, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user