akashcli 3.6.0__tar.gz → 4.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {akashcli-3.6.0 → akashcli-4.2.0}/PKG-INFO +1 -1
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/cli.py +33 -30
- akashcli-4.2.0/akashcli/downloader.py +47 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-3.6.0 → akashcli-4.2.0}/pyproject.toml +1 -1
- akashcli-3.6.0/akashcli/downloader.py +0 -28
- {akashcli-3.6.0 → akashcli-4.2.0}/README.md +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/__init__.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/auth.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/client.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/config.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/crypto.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/exceptions.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/models.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/uploader.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli/utils.py +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-3.6.0 → akashcli-4.2.0}/setup.cfg +0 -0
|
@@ -7,7 +7,7 @@ import webbrowser
|
|
|
7
7
|
import httpx
|
|
8
8
|
import urllib.parse
|
|
9
9
|
import questionary
|
|
10
|
-
from typing import Optional
|
|
10
|
+
from typing import Optional, List
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
@@ -194,21 +194,32 @@ def files(json_output: bool = typer.Option(False, "--json", help="Output in raw
|
|
|
194
194
|
@app.command()
|
|
195
195
|
def download(
|
|
196
196
|
code: str,
|
|
197
|
-
output: Optional[Path] = typer.Option(None, "--output", "-o"
|
|
197
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o")
|
|
198
198
|
):
|
|
199
|
-
"""📥 Download a file from the vault to your
|
|
199
|
+
"""📥 Download a file from the vault to your current folder."""
|
|
200
200
|
try:
|
|
201
201
|
client = AkashClient()
|
|
202
|
-
# Attach the console to the client for the progress bar
|
|
203
|
-
client.console = console
|
|
204
202
|
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
# 1. Get the secure link
|
|
204
|
+
with console.status("[bold cyan]Requesting link..."):
|
|
205
|
+
resp = client.generate_secure_link(code)
|
|
206
|
+
if not resp or 'stream_url' not in resp:
|
|
207
|
+
raise Exception("Failed to generate download link.")
|
|
208
|
+
|
|
209
|
+
# 2. Start the download
|
|
210
|
+
from .downloader import smart_download
|
|
211
|
+
# Convert Path to string for the downloader
|
|
212
|
+
out_str = str(output) if output else None
|
|
213
|
+
saved_path = smart_download(resp['stream_url'], out_str)
|
|
207
214
|
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
if saved_path:
|
|
216
|
+
console.print(f"[bold green]✓ Download Complete![/bold green]")
|
|
217
|
+
console.print(f"📍 Saved as: [bold cyan]{saved_path}[/bold cyan]")
|
|
218
|
+
else:
|
|
219
|
+
console.print("[red]❌ Download failed (Empty stream).[/red]")
|
|
220
|
+
|
|
210
221
|
except Exception as e:
|
|
211
|
-
console.print(f"[bold red]✗
|
|
222
|
+
console.print(f"[bold red]✗ Error:[/bold red] {e}")
|
|
212
223
|
|
|
213
224
|
@app.command()
|
|
214
225
|
def delete(code: str):
|
|
@@ -324,30 +335,22 @@ def share(code: Optional[str] = typer.Argument(None)):
|
|
|
324
335
|
|
|
325
336
|
@app.command()
|
|
326
337
|
def get(otp: str):
|
|
327
|
-
"""✨ Download a file using a 6-digit
|
|
328
|
-
|
|
338
|
+
"""✨ Download a file using a 6-digit OTP code."""
|
|
339
|
+
config = ConfigManager.load()
|
|
340
|
+
base_url = os.getenv("AKASH_API_URL") or config.get("api_url", "https://jstore.2bd.net")
|
|
329
341
|
try:
|
|
330
342
|
with console.status("[bold cyan]Redeeming code..."):
|
|
331
|
-
resp = httpx.get(f"{
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
stream_url = data['stream_url']
|
|
338
|
-
# We don't have the original filename easily, so we probe the headers
|
|
339
|
-
head = httpx.head(stream_url)
|
|
340
|
-
cd = head.headers.get("Content-Disposition", "")
|
|
341
|
-
filename = cd.split("filename*=UTF-8''")[-1] if "''" in cd else f"shared_{otp}.bin"
|
|
342
|
-
filename = urllib.parse.unquote(filename)
|
|
343
|
-
|
|
344
|
-
# Re-use your high-performance downloader logic
|
|
345
|
-
from .downloader import download_from_url
|
|
346
|
-
download_from_url(stream_url, filename)
|
|
343
|
+
resp = httpx.get(f"{base_url}/api/otp/redeem/{otp}")
|
|
344
|
+
resp.raise_for_status()
|
|
345
|
+
stream_url = resp.json()['stream_url']
|
|
346
|
+
|
|
347
|
+
from .downloader import smart_download
|
|
348
|
+
saved_path = smart_download(stream_url)
|
|
347
349
|
|
|
348
|
-
|
|
350
|
+
if saved_path:
|
|
351
|
+
console.print(f"[bold green]✓ Successfully retrieved:[/bold green] [cyan]{saved_path}[/cyan]")
|
|
349
352
|
except Exception as e:
|
|
350
|
-
console.print(f"[red]
|
|
353
|
+
console.print(f"[bold red]✗ Failed:[/bold red] {e}")
|
|
351
354
|
|
|
352
355
|
@app.command()
|
|
353
356
|
def bulk(path: Path):
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.progress import Progress, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn
|
|
9
|
+
|
|
10
|
+
def smart_download(url: str, custom_filename: Optional[str] = None):
|
|
11
|
+
"""Unified downloader that detects server errors properly."""
|
|
12
|
+
# We use a standard get first to check the response
|
|
13
|
+
with httpx.stream("GET", url, follow_redirects=True, timeout=None) as response:
|
|
14
|
+
# 🌟 THE FIX: If not 200/206, read the error message 🌟
|
|
15
|
+
if response.status_code not in [200, 206]:
|
|
16
|
+
# Load the first few bytes to see the error
|
|
17
|
+
error_text = response.read().decode('utf-8', errors='ignore')
|
|
18
|
+
print(f"\n[red]❌ Server Error ({response.status_code}):[/red] {error_text[:200]}")
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
# Robust Filename Extraction
|
|
22
|
+
raw_name = response.headers.get("x-vault-filename")
|
|
23
|
+
if raw_name:
|
|
24
|
+
filename = urllib.parse.unquote(raw_name)
|
|
25
|
+
else:
|
|
26
|
+
cd = response.headers.get("Content-Disposition", "")
|
|
27
|
+
match = re.search(r"filename\*=UTF-8''([^;]+)", cd, re.IGNORECASE)
|
|
28
|
+
filename = urllib.parse.unquote(match.group(1)) if match else f"file_{os.urandom(2).hex()}.bin"
|
|
29
|
+
|
|
30
|
+
final_name = custom_filename or filename
|
|
31
|
+
total_size = int(response.headers.get("x-vault-size") or response.headers.get("Content-Length", 0))
|
|
32
|
+
|
|
33
|
+
# Stream the actual data
|
|
34
|
+
with Progress(
|
|
35
|
+
TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
|
|
36
|
+
BarColumn(bar_width=None),
|
|
37
|
+
"[progress.percentage]{task.percentage:>3.1f}%",
|
|
38
|
+
DownloadColumn(),
|
|
39
|
+
console=Console()
|
|
40
|
+
) as progress:
|
|
41
|
+
task = progress.add_task("download", total=total_size, filename=final_name)
|
|
42
|
+
with open(final_name, "wb") as f:
|
|
43
|
+
for chunk in response.iter_bytes():
|
|
44
|
+
f.write(chunk)
|
|
45
|
+
progress.update(task, advance=len(chunk))
|
|
46
|
+
|
|
47
|
+
return final_name
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# akashcli/downloader.py
|
|
2
|
-
import httpx
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from rich.console import Console # 🌟 ADD THIS
|
|
6
|
-
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, DownloadColumn
|
|
7
|
-
|
|
8
|
-
def download_from_url(url: str, filename: str):
|
|
9
|
-
"""Standalone downloader for OTP/Guest links."""
|
|
10
|
-
# 🌟 FIX: Define the path
|
|
11
|
-
final_path = Path(os.getcwd()) / filename
|
|
12
|
-
|
|
13
|
-
with httpx.stream("GET", url, follow_redirects=True) as response:
|
|
14
|
-
total = int(response.headers.get("Content-Length", 0))
|
|
15
|
-
|
|
16
|
-
with Progress(
|
|
17
|
-
TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
|
|
18
|
-
BarColumn(bar_width=None),
|
|
19
|
-
"[progress.percentage]{task.percentage:>3.1f}%",
|
|
20
|
-
DownloadColumn(),
|
|
21
|
-
console=Console()
|
|
22
|
-
) as progress:
|
|
23
|
-
task = progress.add_task("download", total=total, filename=filename)
|
|
24
|
-
with open(final_path, "wb") as f:
|
|
25
|
-
for chunk in response.iter_bytes():
|
|
26
|
-
f.write(chunk)
|
|
27
|
-
progress.update(task, advance=len(chunk))
|
|
28
|
-
return final_path # 🌟 Now defined
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|