akashcli 3.4.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.4.0 → akashcli-4.2.0}/PKG-INFO +1 -1
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/cli.py +34 -30
- akashcli-4.2.0/akashcli/downloader.py +47 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-3.4.0 → akashcli-4.2.0}/pyproject.toml +1 -1
- akashcli-3.4.0/akashcli/downloader.py +0 -28
- {akashcli-3.4.0 → akashcli-4.2.0}/README.md +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/__init__.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/auth.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/client.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/config.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/crypto.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/exceptions.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/models.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/uploader.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli/utils.py +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-3.4.0 → akashcli-4.2.0}/setup.cfg +0 -0
|
@@ -5,8 +5,9 @@ import os
|
|
|
5
5
|
import subprocess
|
|
6
6
|
import webbrowser
|
|
7
7
|
import httpx
|
|
8
|
+
import urllib.parse
|
|
8
9
|
import questionary
|
|
9
|
-
from typing import Optional
|
|
10
|
+
from typing import Optional, List
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from rich.console import Console
|
|
12
13
|
from rich.table import Table
|
|
@@ -193,21 +194,32 @@ def files(json_output: bool = typer.Option(False, "--json", help="Output in raw
|
|
|
193
194
|
@app.command()
|
|
194
195
|
def download(
|
|
195
196
|
code: str,
|
|
196
|
-
output: Optional[Path] = typer.Option(None, "--output", "-o"
|
|
197
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o")
|
|
197
198
|
):
|
|
198
|
-
"""📥 Download a file from the vault to your
|
|
199
|
+
"""📥 Download a file from the vault to your current folder."""
|
|
199
200
|
try:
|
|
200
201
|
client = AkashClient()
|
|
201
|
-
# Attach the console to the client for the progress bar
|
|
202
|
-
client.console = console
|
|
203
202
|
|
|
204
|
-
|
|
205
|
-
|
|
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)
|
|
206
214
|
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
|
|
209
221
|
except Exception as e:
|
|
210
|
-
console.print(f"[bold red]✗
|
|
222
|
+
console.print(f"[bold red]✗ Error:[/bold red] {e}")
|
|
211
223
|
|
|
212
224
|
@app.command()
|
|
213
225
|
def delete(code: str):
|
|
@@ -323,30 +335,22 @@ def share(code: Optional[str] = typer.Argument(None)):
|
|
|
323
335
|
|
|
324
336
|
@app.command()
|
|
325
337
|
def get(otp: str):
|
|
326
|
-
"""✨ Download a file using a 6-digit
|
|
327
|
-
|
|
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")
|
|
328
341
|
try:
|
|
329
342
|
with console.status("[bold cyan]Redeeming code..."):
|
|
330
|
-
resp = httpx.get(f"{
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
stream_url = data['stream_url']
|
|
337
|
-
# We don't have the original filename easily, so we probe the headers
|
|
338
|
-
head = httpx.head(stream_url)
|
|
339
|
-
cd = head.headers.get("Content-Disposition", "")
|
|
340
|
-
filename = cd.split("filename*=UTF-8''")[-1] if "''" in cd else f"shared_{otp}.bin"
|
|
341
|
-
filename = urllib.parse.unquote(filename)
|
|
342
|
-
|
|
343
|
-
# Re-use your high-performance downloader logic
|
|
344
|
-
from .downloader import download_from_url
|
|
345
|
-
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)
|
|
346
349
|
|
|
347
|
-
|
|
350
|
+
if saved_path:
|
|
351
|
+
console.print(f"[bold green]✓ Successfully retrieved:[/bold green] [cyan]{saved_path}[/cyan]")
|
|
348
352
|
except Exception as e:
|
|
349
|
-
console.print(f"[red]
|
|
353
|
+
console.print(f"[bold red]✗ Failed:[/bold red] {e}")
|
|
350
354
|
|
|
351
355
|
@app.command()
|
|
352
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
|