feat: Add open folder functionality for photos in file manager
This commit introduces a new feature that allows users to open the folder containing a selected photo in their system's file manager. The API has been updated with a new endpoint to handle folder opening requests, supporting various operating systems. The frontend has been enhanced to include a button for this action, providing user feedback and error handling. Documentation and tests have been updated to reflect these changes, ensuring reliability and usability.
This commit is contained in:
@@ -363,3 +363,181 @@ def get_photo_image(photo_id: int, db: Session = Depends(get_db)) -> FileRespons
|
||||
response.headers["Cache-Control"] = "public, max-age=3600"
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/{photo_id}/open-folder")
|
||||
def open_photo_folder(photo_id: int, db: Session = Depends(get_db)) -> dict:
|
||||
"""Open the folder containing the photo in the system file manager and select the file.
|
||||
|
||||
Matches desktop behavior and enhances it by selecting the specific file:
|
||||
- Windows: uses explorer /select,"file_path"
|
||||
- macOS: uses 'open -R' to reveal and select the file
|
||||
- Linux: tries file manager-specific commands (nautilus, dolphin, etc.) or opens folder
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from src.web.db.models import Photo
|
||||
|
||||
photo = db.query(Photo).filter(Photo.id == photo_id).first()
|
||||
if not photo:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Photo {photo_id} not found",
|
||||
)
|
||||
|
||||
# Ensure we have absolute path
|
||||
file_path = os.path.abspath(photo.path)
|
||||
folder = os.path.dirname(file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Photo file not found: {file_path}",
|
||||
)
|
||||
|
||||
if not os.path.exists(folder):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Folder not found: {folder}",
|
||||
)
|
||||
|
||||
try:
|
||||
# Try using showinfm package first (better cross-platform support)
|
||||
try:
|
||||
from showinfm import show_in_file_manager
|
||||
show_in_file_manager(file_path)
|
||||
return {
|
||||
"message": f"Opened folder and selected file: {os.path.basename(file_path)}",
|
||||
"folder": folder,
|
||||
"file": file_path
|
||||
}
|
||||
except ImportError:
|
||||
# showinfm not installed, fall back to manual commands
|
||||
pass
|
||||
except Exception as e:
|
||||
# showinfm failed, fall back to manual commands
|
||||
pass
|
||||
|
||||
# Fallback: Open folder and select the file using platform-specific commands
|
||||
if os.name == "nt": # Windows
|
||||
# Windows: explorer /select,"file_path" opens folder and selects the file
|
||||
subprocess.run(["explorer", "/select,", file_path], check=False)
|
||||
elif sys.platform == "darwin": # macOS
|
||||
# macOS: open -R reveals the file in Finder and selects it
|
||||
subprocess.run(["open", "-R", file_path], check=False)
|
||||
else: # Linux and others
|
||||
# Linux: Try file manager-specific commands to select the file
|
||||
# Try different file managers based on desktop environment
|
||||
opened = False
|
||||
|
||||
# Detect desktop environment first
|
||||
desktop_env = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
|
||||
# Try Nautilus (GNOME) - supports --select option
|
||||
if "gnome" in desktop_env or not desktop_env:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["nautilus", "--select", file_path],
|
||||
check=False,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
opened = True
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Try Nemo (Cinnamon/MATE) - doesn't support --select, but we can try folder with navigation
|
||||
if not opened and ("cinnamon" in desktop_env or "mate" in desktop_env):
|
||||
try:
|
||||
# For Nemo, we can try opening the folder first, then using a script or
|
||||
# just opening the folder (Nemo may focus on the file if we pass it)
|
||||
# Try opening with the file path - Nemo will open the folder
|
||||
file_uri = f"file://{file_path}"
|
||||
result = subprocess.Popen(
|
||||
["nemo", file_uri],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
# Nemo will open the folder - selection may not work perfectly
|
||||
# This is a limitation of Nemo
|
||||
opened = True
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Try Dolphin (KDE) - supports --select option
|
||||
if not opened and "kde" in desktop_env:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["dolphin", "--select", file_path],
|
||||
check=False,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
opened = True
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Try PCManFM (LXDE/LXQt) - supports --select option
|
||||
if not opened and ("lxde" in desktop_env or "lxqt" in desktop_env):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pcmanfm", "--select", file_path],
|
||||
check=False,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
opened = True
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Try Thunar (XFCE) - supports --select option
|
||||
if not opened and "xfce" in desktop_env:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["thunar", "--select", file_path],
|
||||
check=False,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
opened = True
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# If desktop-specific didn't work, try all file managers in order
|
||||
if not opened:
|
||||
file_managers = [
|
||||
("nautilus", ["--select", file_path]),
|
||||
("nemo", [f"file://{file_path}"]), # Nemo uses file:// URI
|
||||
("dolphin", ["--select", file_path]),
|
||||
("thunar", ["--select", file_path]),
|
||||
("pcmanfm", ["--select", file_path]),
|
||||
]
|
||||
|
||||
for fm_name, fm_args in file_managers:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[fm_name] + fm_args,
|
||||
check=False,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
opened = True
|
||||
break
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
continue
|
||||
|
||||
# Fallback: try xdg-open with the folder (will open folder but won't select file)
|
||||
if not opened:
|
||||
subprocess.run(["xdg-open", folder], check=False)
|
||||
|
||||
return {
|
||||
"message": f"Opened folder and selected file: {os.path.basename(file_path)}",
|
||||
"folder": folder,
|
||||
"file": file_path
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to open folder: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user