From c19825c523b5b263560e939e03eb11611112de7e Mon Sep 17 00:00:00 2001 From: tocmo Date: Sat, 4 Apr 2026 23:55:42 -0400 Subject: [PATCH] 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 --- .claude/launch.json | 11 ++ app/__pycache__/main.cpython-313.pyc | Bin 0 -> 26778 bytes app/__pycache__/scanner.cpython-313.pyc | Bin 0 -> 33324 bytes app/__pycache__/takeout.cpython-313.pyc | Bin 0 -> 6464 bytes app/main.py | 43 ++++++- app/scanner.py | 4 +- data/dupfinder.db | Bin 0 -> 49152 bytes templates/index.html | 160 +++++++++++++++++++++++- 8 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 .claude/launch.json create mode 100644 app/__pycache__/main.cpython-313.pyc create mode 100644 app/__pycache__/scanner.cpython-313.pyc create mode 100644 app/__pycache__/takeout.cpython-313.pyc create mode 100644 data/dupfinder.db diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..9a58f23 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dup-finder-api", + "runtimeExecutable": "uvicorn", + "runtimeArgs": ["main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"], + "port": 8000 + } + ] +} diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea39c67fdf1a83ec89b729f67e18c7d50bd0d713 GIT binary patch literal 26778 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 literal 0 HcmV?d00001 diff --git a/app/__pycache__/scanner.cpython-313.pyc b/app/__pycache__/scanner.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4aed4608ddeefb9b9777d7054f1508f30fc7b3d7 GIT binary patch literal 33324 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 DzIjFC literal 0 HcmV?d00001 diff --git a/app/__pycache__/takeout.cpython-313.pyc b/app/__pycache__/takeout.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c3c545751ffa457ff2293f8ad577e1ececdff20 GIT binary patch literal 6464 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 Rk@lftw2zek*8kQw{XaG?hdKZN literal 0 HcmV?d00001 diff --git a/app/main.py b/app/main.py index 9a74274..c3d577d 100644 --- a/app/main.py +++ b/app/main.py @@ -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() diff --git a/app/scanner.py b/app/scanner.py index 108db0d..8a38fbd 100644 --- a/app/scanner.py +++ b/app/scanner.py @@ -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 = { diff --git a/data/dupfinder.db b/data/dupfinder.db new file mode 100644 index 0000000000000000000000000000000000000000..66c1209bb7d2b56f36717c2fa017f2897f545b5a GIT binary patch literal 49152 zcmeI)?{C{g7zc1WZR4gt)CnQ^1wtH}P#d+SX-bPU4T&thp@=l?Qb(-t!t&xv<1VqY z*xAb7F>Sm+;%`B`<2CR3b3nZ0?rbM<-OwTtDXPBK;wHYk$9JFSe2z7>r%$R*z(~vM zcddZzDfg62M){HuMNu;RwZdQVmg8^c;}86&I`)3j+l=z*Pw%g;{jDr#FO(H~?T?lB zmVV3m+F#l)+J&~7{gofa1_1~_00MVffG%hCk9E~@T$^1w_ND0uRxt2ePKWs`Qv}M|X484>J9hHuCGCX1hEO5F!eE5;Qbeb$*Ud-wr z>8d%5-D_KZTSm^MBFi#9`cbFlkv!F|BDSsK{(+3Ln2O@d!tp5Kk^Gz2Sxw*BQQzGU ztY;l&4|*M^$$M>fS@#+1`>6w36x%30s#20FCL2qlv+?HG@w-aRpkL8CIjL8UO7%1H zH9aGxhH+e}@qkCPW^5%Ip7*^$&lJt$hIRUw)@iLwPsh$GtN9y_ofkfbv`YEhDVI*m zr9+wwC{r!Fb4@_0cAF@UZ$4+N$NIx=9n#08M%5sNbq5m=r=9bm%D`ompt@88nwKR&H84~3fjEGla2Ud)&DO8OAjYk4 zbxb*hZ)6UaPMMIX*Rk+=f8e@~d%ixBJ(#YljowN=LP>t;849z$B)4b`L2=7k^TCOs0dEXdnTc>4JK$;2eC8eUO^87@uhJ?7ft!b!GCENG(5&1+gR zz@TT3R>a!{7+t#gOjiFwR~7jcBf3_tLEy>H;aXwt%FAqyf7M(RUpH69?w;6Tg8&2| z009U<00Izz00bZa0SG|gt_m!Pf#Lc8uBI2`f&c^{009U<00Izz00bZa0SHI|@&Eto z+Mf#lhYbP{fB*y_009U<00Izz00bZafx9G-Q|EM5{%TZ4JpX6b-YRRq^Bpz_KmY;| zfB*y_009U<00Izz00gEc@Gzq(AMEWdez?Y7S{3;_s000Izz00bZa0SG_<0uY!Pfqwuv CH@k8G literal 0 HcmV?d00001 diff --git a/templates/index.html b/templates/index.html index a085d6e..3d9fb81 100644 --- a/templates/index.html +++ b/templates/index.html @@ -513,6 +513,83 @@ #export-view tr:hover td { background: rgba(255,255,255,.02); } /* ── Confirm dialog ── */ + /* ── Folder picker ── */ + #picker-overlay { + position: fixed; inset: 0; + background: rgba(0,0,0,.75); + display: none; + align-items: center; + justify-content: center; + z-index: 110; + } + #picker-overlay.show { display: flex; } + #picker-box { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + width: 520px; + max-width: 95vw; + display: flex; + flex-direction: column; + max-height: 70vh; + } + #picker-header { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 16px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + } + #picker-header h3 { font-size: 14px; flex: 1; } + #picker-path { + padding: 8px 16px; + font-family: monospace; + font-size: 12px; + color: var(--text-dim); + background: var(--surface2); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + #picker-list { + overflow-y: auto; + flex: 1; + padding: 6px 0; + } + .picker-row { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 16px; + cursor: pointer; + font-size: 13px; + transition: background .1s; + } + .picker-row:hover { background: var(--surface2); } + .picker-row .icon { color: var(--warning); font-size: 15px; flex-shrink: 0; } + .picker-row.up-row .icon { color: var(--text-dim); } + #picker-footer { + padding: 12px 16px; + border-top: 1px solid var(--border); + display: flex; + gap: 8px; + align-items: center; + flex-shrink: 0; + } + #picker-selected-path { + flex: 1; + font-family: monospace; + font-size: 12px; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 10px; + } + #confirm-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.7); @@ -660,6 +737,7 @@
+
@@ -688,6 +766,7 @@
+
@@ -786,6 +865,23 @@
+ +
+
+
+

Browse for folder

+ +
+
/
+
+ +
+
+
@@ -1363,10 +1459,72 @@ async function loadExport() { } } +// ── Folder picker ───────────────────────────────────────────────────────────── +let _pickerTargetId = null; + +async function openPicker(inputId) { + _pickerTargetId = inputId; + const currentVal = el(inputId).value.trim() || '/'; + el('picker-overlay').classList.add('show'); + await pickerNavigate(currentVal); +} + +function closePicker() { + el('picker-overlay').classList.remove('show'); + _pickerTargetId = null; +} + +function confirmPicker() { + const path = el('picker-selected-path').value.trim(); + if (path && _pickerTargetId) { + el(_pickerTargetId).value = path; + } + closePicker(); +} + +async function pickerNavigate(path) { + try { + const data = await api('GET', `/api/browse?path=${encodeURIComponent(path)}`); + el('picker-path').textContent = data.current; + el('picker-selected-path').value = data.current; + + const list = el('picker-list'); + list.innerHTML = ''; + + // Up button + if (data.parent) { + const row = document.createElement('div'); + row.className = 'picker-row up-row'; + row.innerHTML = ` ..`; + row.onclick = () => pickerNavigate(data.parent); + list.appendChild(row); + } + + if (data.dirs.length === 0) { + list.innerHTML += `
No subfolders
`; + } + + data.dirs.forEach(name => { + const row = document.createElement('div'); + row.className = 'picker-row'; + const fullPath = data.current.replace(/\\/g, '/').replace(/\/$/, '') + '/' + name; + row.innerHTML = `📁 ${name}`; + row.onclick = () => { + el('picker-selected-path').value = fullPath; + pickerNavigate(fullPath); + }; + list.appendChild(row); + }); + } catch(e) { + el('picker-list').innerHTML = `
Cannot open this path.
`; + } +} + // ── Keyboard shortcuts ──────────────────────────────────────────────────────── document.addEventListener('keydown', e => { if (e.key === 'Escape') { - if (el('confirm-overlay').classList.contains('show')) closeConfirm(); + if (el('picker-overlay').classList.contains('show')) closePicker(); + else if (el('confirm-overlay').classList.contains('show')) closeConfirm(); else closeDetail(); } });