feat: Implement directory browsing functionality
CI / skip-ci-check (pull_request) Successful in 10s
CI / python-lint (pull_request) Has been cancelled
CI / test-backend (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / secret-scanning (pull_request) Has been cancelled
CI / dependency-scan (pull_request) Has been cancelled
CI / sast-scan (pull_request) Has been cancelled
CI / workflow-summary (pull_request) Has been cancelled
CI / lint-and-type-check (pull_request) Has been cancelled

- Add `browseDirectory` API endpoint to list directory contents.
- Create `FolderBrowser` component for user interface to navigate directories.
- Update `Scan` page to integrate folder browsing feature.
- Define `DirectoryItem` and `BrowseDirectoryResponse` schemas for API responses.
This commit is contained in:
tanyar09
2026-01-30 16:09:24 +00:00
parent 920fe97c09
commit f4bdb5d9b3
5 changed files with 460 additions and 150 deletions
+108
View File
@@ -29,6 +29,8 @@ from backend.schemas.photos import (
BulkDeletePhotosResponse,
BulkRemoveFavoritesRequest,
BulkRemoveFavoritesResponse,
BrowseDirectoryResponse,
DirectoryItem,
)
from backend.schemas.search import (
PhotoSearchResult,
@@ -436,6 +438,112 @@ async def upload_photos(
}
@router.get("/browse-directory", response_model=BrowseDirectoryResponse)
def browse_directory(
current_user: Annotated[dict, Depends(get_current_user)],
path: str = Query("/", description="Directory path to list"),
) -> BrowseDirectoryResponse:
"""List directories and files in a given path.
No GUI required - uses os.listdir() to read filesystem.
Returns JSON with directory structure for web-based folder browser.
Args:
path: Directory path to list (can be relative or absolute)
Returns:
BrowseDirectoryResponse with current path, parent path, and items list
Raises:
HTTPException: If path doesn't exist, is not a directory, or access is denied
"""
import os
from pathlib import Path
try:
# Convert to absolute path
abs_path = os.path.abspath(path)
# Normalize path separators
abs_path = os.path.normpath(abs_path)
# Security: Optional - restrict to certain base paths
# For now, allow any path (server admin should configure file permissions)
# You can uncomment and customize this for production:
# allowed_bases = ["/home", "/mnt", "/opt/punimtag", "/media"]
# if not any(abs_path.startswith(base) for base in allowed_bases):
# raise HTTPException(
# status_code=status.HTTP_403_FORBIDDEN,
# detail=f"Path not allowed: {abs_path}"
# )
if not os.path.exists(abs_path):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Path does not exist: {abs_path}",
)
if not os.path.isdir(abs_path):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Path is not a directory: {abs_path}",
)
# Read directory contents
items = []
try:
for item in os.listdir(abs_path):
item_path = os.path.join(abs_path, item)
full_path = os.path.abspath(item_path)
# Skip if we can't access it (permission denied)
try:
is_dir = os.path.isdir(full_path)
is_file = os.path.isfile(full_path)
except (OSError, PermissionError):
# Skip items we can't access
continue
items.append(
DirectoryItem(
name=item,
path=full_path,
is_directory=is_dir,
is_file=is_file,
)
)
except PermissionError:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permission denied reading directory: {abs_path}",
)
# Sort: directories first, then files, both alphabetically
items.sort(key=lambda x: (not x.is_directory, x.name.lower()))
# Get parent path (None if at root)
parent_path = None
if abs_path != "/" and abs_path != os.path.dirname(abs_path):
parent_path = os.path.dirname(abs_path)
# Normalize parent path
parent_path = os.path.normpath(parent_path)
return BrowseDirectoryResponse(
current_path=abs_path,
parent_path=parent_path,
items=items,
)
except HTTPException:
# Re-raise HTTP exceptions as-is
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error reading directory: {str(e)}",
)
@router.post("/browse-folder")
def browse_folder() -> dict:
"""Open native folder picker dialog and return selected folder path.