Skip to content

Commit 5f69850

Browse files
feat: improve CLI handling (#575)
* fix: better url parsing * improve implementation
1 parent adbe2d6 commit 5f69850

6 files changed

Lines changed: 180 additions & 411 deletions

File tree

src/cli/app/builder/urls.py

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,81 @@
1-
from urllib.parse import urljoin, urlparse
2-
1+
from urllib.parse import urljoin, urlparse, urlunparse
32
from app.settings import settings
3+
import typer
44

55

66
class UrlBuilder:
7-
def __init__(self, instance_url: str):
8-
# Add https:// if scheme is missing
9-
parsed = urlparse(instance_url)
10-
if not parsed.scheme:
11-
instance_url = "https://" + instance_url
7+
def __init__(self, backend_url: str, frontend_url: str):
8+
# Ensure schemes are present
9+
self.__backend_url = self.__ensure_scheme(backend_url)
10+
self.__frontend_url = self.__ensure_scheme(frontend_url)
11+
12+
@staticmethod
13+
def __ensure_scheme(url: str) -> str:
14+
if not urlparse(url).scheme:
15+
return "https://" + url.lstrip("/")
16+
return url
1217

13-
self.__instance_url = instance_url.rstrip("/") + "/"
18+
@staticmethod
19+
def __ensure_trailing_slash(url: str) -> str:
20+
parsed = urlparse(url)
21+
if not parsed.path.endswith("/"):
22+
new_path = parsed.path + "/"
23+
parsed = parsed._replace(path=new_path)
24+
return urlunparse(parsed)
1425

1526
@classmethod
16-
def resolve(cls, instance_url: str | None = None) -> "UrlBuilder":
17-
"""Resolve instance URL from arg/env/prompt and return UrlBuilder."""
18-
import typer # local import to avoid hard dep at module level
19-
20-
url = instance_url or settings.INSTANCE_URL
21-
if not url:
22-
url = typer.prompt(
23-
"Enter the Chithi instance URL (e.g. https://chithi.dev)"
27+
def resolve(cls, initial_url: str | None = None) -> "UrlBuilder":
28+
"""
29+
Logic:
30+
1. If a URL is passed from CLI/Link, use it.
31+
2. If not, check settings.
32+
3. If still nothing, prompt user for 'Same Domain' setup.
33+
"""
34+
url_candidate = initial_url or settings.INSTANCE_URL
35+
36+
if url_candidate:
37+
# If we already have a URL, we treat it as both for simplicity
38+
return cls(backend_url=url_candidate, frontend_url=url_candidate)
39+
40+
# Interactive Setup
41+
same_domain = typer.confirm(
42+
"Are the backend and frontend hosted on the same domain?", default=True
43+
)
44+
45+
if same_domain:
46+
domain = typer.prompt("Enter the base domain", default="chithi.dev").strip(
47+
"/"
48+
)
49+
# Standard structure: root for frontend, /api/ for backend
50+
return cls(
51+
backend_url=f"https://{domain}/api/", frontend_url=f"https://{domain}/"
52+
)
53+
else:
54+
frontend = typer.prompt("Enter the Frontend URL (e.g. https://chithi.dev)")
55+
backend = typer.prompt(
56+
"Enter the Backend URL (e.g. https://api.chithi.dev)"
2457
)
25-
return cls(url)
58+
return cls(backend_url=backend, frontend_url=frontend)
2659

2760
@property
2861
def instance_url(self):
29-
return self.__instance_url
62+
"""Standardized backend base URL."""
63+
return self.__ensure_trailing_slash(self.__backend_url)
3064

3165
@property
32-
def __api_url(self):
33-
return urljoin(self.instance_url, "api/")
66+
def frontend_url(self):
67+
"""Standardized frontend base URL."""
68+
return self.__ensure_trailing_slash(self.__frontend_url)
3469

3570
def upload_url(self):
36-
# SENSITIVE URL
37-
return urljoin(self.__api_url, "upload")
71+
return urljoin(self.instance_url, "upload")
3872

3973
def config_url(self):
40-
return urljoin(self.__api_url, "config")
74+
return urljoin(self.instance_url, "config")
4175

4276
def download_url(self):
43-
return urljoin(self.__api_url, "download/")
77+
return urljoin(self.instance_url, "download/")
4478

4579
def share_url(self, slug: str, key_secret: str) -> str:
46-
"""Web-facing download link: ``https://instance/download/<slug>#<key>``."""
47-
return f"{self.instance_url}download/{slug}#{key_secret}"
80+
# Share URLs should point to the Frontend UI
81+
return f"{self.frontend_url}download/{slug}#{key_secret}"

src/cli/app/commands/download.py

Lines changed: 16 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,96 +14,63 @@
1414

1515
@app.command()
1616
def download(
17-
link: Annotated[
18-
str,
19-
typer.Argument(
20-
help="Chithi share URL (https://…/download/<slug>#<key>) or '<slug>#<key>'.",
21-
),
22-
],
23-
instance_url: Annotated[
24-
str | None,
25-
typer.Option("--url", "-u", help="Chithi backend URL."),
26-
] = None,
27-
password: Annotated[
28-
str | None,
29-
typer.Option(
30-
"--password",
31-
"-p",
32-
help="Decryption password (required if the file was password-protected).",
33-
prompt=False,
34-
),
35-
] = None,
36-
output: Annotated[
37-
Path,
38-
typer.Option(
39-
"--output", "-o", help="Destination directory for extracted files."
40-
),
41-
] = Path("."),
17+
link: Annotated[str, typer.Argument(help="Chithi share URL or 'slug#key'")],
18+
instance_url: Annotated[str | None, typer.Option("--url", "-u")] = None,
19+
password: Annotated[str | None, typer.Option("--password", "-p")] = None,
20+
output: Annotated[Path, typer.Option("--output", "-o")] = Path("."),
4221
) -> None:
4322
"""Download, decrypt, and extract a file."""
44-
# Parse Input
4523
try:
4624
slug: str
4725
key_secret: str
4826
inferred_url: str | None = None
4927

50-
# Full URL -> https://instance.com/download/SLUG#KEY
28+
# Case A: Full URL provided
5129
if "://" in link:
5230
parsed = urlparse(link)
53-
fragment = parsed.fragment
31+
key_secret = parsed.fragment
5432
path_parts = parsed.path.strip("/").split("/")
5533

56-
# Expecting path ending in /download/SLUG
57-
if len(path_parts) >= 2 and path_parts[-2] == "download":
58-
slug = path_parts[-1]
59-
elif len(path_parts) >= 1:
34+
if len(path_parts) >= 1:
6035
slug = path_parts[-1]
6136
else:
6237
raise ValueError("Could not extract slug from URL.")
6338

64-
# Reconstruct base URL (everything before /download/...)
39+
# Reconstruct the base (e.g., https://chithi.dev)
6540
inferred_url = f"{parsed.scheme}://{parsed.netloc}"
66-
key_secret = fragment
6741

68-
# SLUG#KEY
42+
# Case B: Just SLUG#KEY provided
6943
elif "#" in link:
7044
slug, key_secret = link.split("#", 1)
71-
7245
else:
73-
raise ValueError("Invalid link format. Expected URL or SLUG#KEY")
46+
raise ValueError("Invalid format. Use URL or SLUG#KEY")
7447

7548
except ValueError as e:
7649
typer.echo(f"✗ Input parsing failed: {e}", err=True)
77-
raise typer.Exit(code=1)
50+
raise typer.Exit(1)
7851

79-
base_url = instance_url or inferred_url
80-
urls = UrlBuilder.resolve(base_url)
52+
# Resolve URLs: Priority is --url option > Link metadata > Interactive Prompt
53+
urls = UrlBuilder.resolve(initial_url=(instance_url or inferred_url))
8154

82-
# Process
55+
# Process Download
8356
fd, tmp_run = tempfile.mkstemp(prefix="chithi_")
8457
os.close(fd)
85-
8658
tmp_dl = Path(f"{tmp_run}.dl")
8759
tmp_zip = Path(f"{tmp_run}.zip")
8860

8961
try:
90-
# Download
9162
with client.Client(urls) as c:
9263
c.download_to_file(slug, tmp_dl)
9364

94-
# Decrypt
9565
ikm = crypto.base64url_to_ikm(key_secret)
9666
crypto.decrypt(tmp_dl, tmp_zip, ikm=ikm, password=password)
9767

98-
# Extract
9968
out_path = output.resolve()
10069
archive.decompress(tmp_zip, out_path, password=password)
101-
102-
typer.echo(f"\n✓ Download complete! Files extracted to {out_path}")
70+
typer.echo(f"\n✓ Success! Extracted to {out_path}")
10371

10472
except Exception as exc:
10573
typer.echo(f"✗ Download failed: {exc}", err=True)
106-
raise typer.Exit(code=1)
107-
74+
raise typer.Exit(1)
10875
finally:
10976
cleanup(tmp_dl, tmp_zip)

0 commit comments

Comments
 (0)