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
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user