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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user