Add server-side folder picker

New GET /api/browse endpoint lists subdirectories at any path.
UI gets a folder icon button next to each path input that opens
a browsable directory tree modal. Escape or Cancel closes it,
clicking a folder navigates into it, Select confirms the choice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tocmo
2026-04-04 23:55:42 -04:00
parent 868da9016d
commit c19825c523
8 changed files with 214 additions and 4 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -22,9 +22,22 @@ from pydantic import BaseModel
import scanner as sc
app = FastAPI(title="Duplicate Finder")
templates = Jinja2Templates(directory="/app/templates")
app.mount("/static", StaticFiles(directory="/app/static"), name="static")
# Resolve paths relative to this file so it works both in Docker and locally
_BASE = Path(__file__).parent
_TEMPLATES_DIR = (
str(_BASE / "templates") if (_BASE / "templates").exists()
else str(_BASE.parent / "templates") if (_BASE.parent / "templates").exists()
else "/app/templates"
)
_STATIC_DIR = str(_BASE / "static")
_STATIC_DIR = _STATIC_DIR if Path(_STATIC_DIR).exists() else "/app/static"
# Ensure static dir exists
Path(_STATIC_DIR).mkdir(parents=True, exist_ok=True)
templates = Jinja2Templates(directory=_TEMPLATES_DIR)
app.mount("/static", StaticFiles(directory=_STATIC_DIR), name="static")
METHOD_META = {
"sha256": {"color": "#378ADD", "label": "Exact copy"},
@@ -502,6 +515,32 @@ def get_file_meta(file_id: int):
# ── Stats ─────────────────────────────────────────────────────────────────────
@app.get("/api/browse")
def browse(path: str = Query("/")):
"""List subdirectories at the given path for the folder picker."""
try:
p = Path(path).resolve()
except Exception:
raise HTTPException(400, "Invalid path")
if not p.exists() or not p.is_dir():
raise HTTPException(404, "Path not found")
dirs = []
try:
for entry in sorted(p.iterdir()):
if entry.is_dir() and not entry.name.startswith("."):
dirs.append(entry.name)
except PermissionError:
pass
parent = str(p.parent) if p != p.parent else None
return {
"current": str(p),
"parent": parent,
"dirs": dirs,
}
@app.get("/api/stats")
def get_stats():
con = get_db()

View File

@@ -35,7 +35,9 @@ VIDEO_EXT = {
SUPPORTED_EXT = PHOTO_EXT | VIDEO_EXT
DB_PATH = "/data/dupfinder.db"
_DATA_DIR = Path("/data") if Path("/data").exists() else Path(__file__).parent.parent / "data"
_DATA_DIR.mkdir(parents=True, exist_ok=True)
DB_PATH = str(_DATA_DIR / "dupfinder.db")
# Shared scan state (updated by background thread, read by status endpoint)
scan_state = {