akashcli 3.0.0__tar.gz → 3.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.0.0 → akashcli-3.2.0}/PKG-INFO +2 -1
- akashcli-3.2.0/akashcli/cli.py +377 -0
- akashcli-3.2.0/akashcli/client.py +164 -0
- akashcli-3.2.0/akashcli/downloader.py +28 -0
- akashcli-3.2.0/akashcli/uploader.py +113 -0
- akashcli-3.2.0/akashcli/utils.py +38 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli.egg-info/PKG-INFO +2 -1
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli.egg-info/SOURCES.txt +1 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli.egg-info/requires.txt +1 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/pyproject.toml +2 -1
- akashcli-3.0.0/akashcli/cli.py +0 -184
- akashcli-3.0.0/akashcli/client.py +0 -74
- akashcli-3.0.0/akashcli/uploader.py +0 -123
- akashcli-3.0.0/akashcli/utils.py +0 -9
- {akashcli-3.0.0 → akashcli-3.2.0}/README.md +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli/__init__.py +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli/auth.py +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli/config.py +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli/crypto.py +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli/exceptions.py +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli/models.py +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-3.0.0 → akashcli-3.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: akashcli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Enterprise Developer SDK and CLI for Akash Vault
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,6 +8,7 @@ Requires-Dist: httpx
|
|
|
8
8
|
Requires-Dist: typer[all]
|
|
9
9
|
Requires-Dist: rich
|
|
10
10
|
Requires-Dist: cryptography
|
|
11
|
+
Requires-Dist: questionary
|
|
11
12
|
|
|
12
13
|
To get started with **AkashCLI**, your users should follow these steps. You can provide this guide as a `README.md` or a "Getting Started" message in your bot.
|
|
13
14
|
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import webbrowser
|
|
7
|
+
import httpx
|
|
8
|
+
import questionary
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, DownloadColumn, TransferSpeedColumn, FileSizeColumn, TimeRemainingColumn
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from .utils import get_file_emoji
|
|
16
|
+
# Internal Imports
|
|
17
|
+
from .client import AkashClient
|
|
18
|
+
from .auth import AuthManager
|
|
19
|
+
from .config import ConfigManager
|
|
20
|
+
from .exceptions import AkashError
|
|
21
|
+
from .uploader import VaultUploader
|
|
22
|
+
|
|
23
|
+
# 🌟 Removed the 'gh' overrides and panels
|
|
24
|
+
app = typer.Typer(help="🛡️ Akash Vault Enterprise CLI", add_completion=True)
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def login(
|
|
29
|
+
url: str = typer.Option("https://jstore.2bd.net", "--url", help="Custom Vault URL"),
|
|
30
|
+
key: str = typer.Option(..., prompt="🔑 Enter API Key", hide_input=True)
|
|
31
|
+
):
|
|
32
|
+
"""🔑 Link your device to the vault."""
|
|
33
|
+
try:
|
|
34
|
+
with console.status("[bold blue]Verifying identity..."):
|
|
35
|
+
user = AuthManager.perform_login(key, url)
|
|
36
|
+
|
|
37
|
+
console.print(f"[green]✓ Login Successful![/green] Owner: {user.owner}")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
console.print(f"[bold red]✗ Authentication Failed:[/bold red] {e}")
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def whoami():
|
|
43
|
+
"""👤 Check current login and quota status."""
|
|
44
|
+
try:
|
|
45
|
+
client = AkashClient()
|
|
46
|
+
user = client.whoami()
|
|
47
|
+
console.print(f"Logged in as: [bold cyan]{user.owner}[/bold cyan]")
|
|
48
|
+
console.print(f"Storage Quota: [bold yellow]{user.quota_used}/{user.quota_limit}[/bold yellow]")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def upload(
|
|
54
|
+
path: Optional[Path] = typer.Argument(None, help="Path to file"),
|
|
55
|
+
text: Optional[str] = typer.Option(None, "--text", "-t", help="Upload raw text content"),
|
|
56
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="Custom filename"),
|
|
57
|
+
burn: bool = typer.Option(False, "--burn", help="Self-destruct after one view"),
|
|
58
|
+
expiry: int = typer.Option(0, "--expiry", "-e", help="Expiry in days (0 for forever)"),
|
|
59
|
+
password: Optional[str] = typer.Option(None, "--pass", "-p", help="Password protect file")
|
|
60
|
+
):
|
|
61
|
+
"""📤 Vault content and display full file details."""
|
|
62
|
+
try:
|
|
63
|
+
client = AkashClient()
|
|
64
|
+
|
|
65
|
+
# 1. Prepare Metadata
|
|
66
|
+
file_size = os.path.getsize(path) if path else len(text.encode())
|
|
67
|
+
display_name = name or (path.name if path else f"snippet_{int(time.time())}.txt")
|
|
68
|
+
icon = get_file_emoji(display_name)
|
|
69
|
+
|
|
70
|
+
# 2. PHASE 1: Client -> Server (Live Progress)
|
|
71
|
+
with Progress(
|
|
72
|
+
SpinnerColumn(),
|
|
73
|
+
TextColumn("[bold cyan]{task.description}"),
|
|
74
|
+
BarColumn(bar_width=None, pulse_style="blue"),
|
|
75
|
+
"[progress.percentage]{task.percentage:>3.1f}%",
|
|
76
|
+
DownloadColumn(), # 🌟 THE FIX: Use this instead of FileSize/TotalSize
|
|
77
|
+
TransferSpeedColumn(),
|
|
78
|
+
TimeRemainingColumn(),
|
|
79
|
+
console=console,
|
|
80
|
+
transient=True
|
|
81
|
+
) as progress:
|
|
82
|
+
|
|
83
|
+
task = progress.add_task(description=f"Sending {display_name}", total=file_size)
|
|
84
|
+
|
|
85
|
+
def update_bar(current, total):
|
|
86
|
+
progress.update(task, completed=current)
|
|
87
|
+
|
|
88
|
+
if text:
|
|
89
|
+
res = client.upload_text(text, filename=name, burn=burn, expiry=expiry, password=password)
|
|
90
|
+
else:
|
|
91
|
+
if not path:
|
|
92
|
+
console.print("[red]Error:[/red] Provide a file path or use --text"); return
|
|
93
|
+
res = client.upload_file(str(path), burn=burn, expiry=expiry, password=password, progress_callback=update_bar)
|
|
94
|
+
|
|
95
|
+
# 3. PHASE 2: Server -> Telegram (Sync Monitoring)
|
|
96
|
+
file_code = res.file_code
|
|
97
|
+
with console.status(f"[bold yellow]🧬 Syncing to Cloud Vault (Code: {file_code})...") as status:
|
|
98
|
+
while True:
|
|
99
|
+
sync_status = client.get_upload_status(file_code)
|
|
100
|
+
if sync_status['status'] == "done":
|
|
101
|
+
break
|
|
102
|
+
elif sync_status['status'] == "error":
|
|
103
|
+
console.print(f"[bold red]❌ Cloud Sync Failed:[/bold red] {sync_status.get('detail')}")
|
|
104
|
+
return
|
|
105
|
+
status.update(f"[bold yellow]🧬 Cloud Syncing: [white]{sync_status.get('status', 'processing')}[/white]...[/bold yellow]")
|
|
106
|
+
time.sleep(2)
|
|
107
|
+
|
|
108
|
+
# 4. PHASE 3: THE SUCCESS DASHBOARD
|
|
109
|
+
size_mb = f"{file_size / (1024*1024):.2f} MB"
|
|
110
|
+
|
|
111
|
+
# Create a grid for aligned metadata
|
|
112
|
+
summary = Table.grid(padding=(0, 2))
|
|
113
|
+
summary.add_row("[bold cyan]NAME[/bold cyan]", f"{icon} {display_name}")
|
|
114
|
+
summary.add_row("[bold cyan]CODE[/bold cyan]", f"[bold yellow]{file_code}[/bold yellow]")
|
|
115
|
+
summary.add_row("[bold cyan]SIZE[/bold cyan]", size_mb)
|
|
116
|
+
summary.add_row("[bold cyan]DNA[/bold cyan]", f"[dim]{res.dna.get('sha256', 'N/A')[:32]}...[/dim]")
|
|
117
|
+
|
|
118
|
+
# Security details
|
|
119
|
+
sec_info = f"🔥 Burn: {'ON' if burn else 'OFF'} | ⏳ Expire: {expiry if expiry > 0 else '∞'}d"
|
|
120
|
+
summary.add_row("[bold cyan]SECURITY[/bold cyan]", sec_info)
|
|
121
|
+
|
|
122
|
+
# Print the main panel
|
|
123
|
+
console.print("\n")
|
|
124
|
+
console.print(Panel(
|
|
125
|
+
summary,
|
|
126
|
+
title="[bold green]✓ Vault Sync Successful[/bold green]",
|
|
127
|
+
border_style="green",
|
|
128
|
+
expand=False,
|
|
129
|
+
padding=(1, 3)
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
# Show actionable links
|
|
133
|
+
console.print(f"🖼️ [bold]Web Player:[/bold] [blue underline]{res.embed_url}[/blue underline]")
|
|
134
|
+
console.print(f"🎬 [bold]Stream Link:[/bold] [blue underline]{res.direct_url}[/blue underline]")
|
|
135
|
+
console.print("\n[dim]💡 Tip: Use 'akash share' to generate a 6-digit access code for this file.[/dim]")
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
console.print(f"[bold red]✗ Failed:[/bold red] {e}")
|
|
139
|
+
|
|
140
|
+
@app.command()
|
|
141
|
+
def stream(code: str, vlc: bool = typer.Option(False, "--vlc", help="Open directly in VLC")):
|
|
142
|
+
"""🎬 Get a secure link or play in VLC (Handles processing state)."""
|
|
143
|
+
client = AkashClient()
|
|
144
|
+
try:
|
|
145
|
+
# We use a status spinner if the file is still processing on the server
|
|
146
|
+
with console.status("[bold cyan]Requesting secure stream...") as status:
|
|
147
|
+
while True:
|
|
148
|
+
try:
|
|
149
|
+
resp = client.generate_secure_link(code)
|
|
150
|
+
if resp.get('status') == 'success':
|
|
151
|
+
url = resp['stream_url']
|
|
152
|
+
break
|
|
153
|
+
except Exception as e:
|
|
154
|
+
# If server returns 202 (Accepted/Processing)
|
|
155
|
+
if "202" in str(e):
|
|
156
|
+
status.update("[bold yellow]⏳ File is still vaulting to cloud. Waiting...")
|
|
157
|
+
time.sleep(3)
|
|
158
|
+
continue
|
|
159
|
+
raise e
|
|
160
|
+
|
|
161
|
+
if vlc:
|
|
162
|
+
console.print(f"🎬 [bold blue]Launching VLC...[/bold blue]")
|
|
163
|
+
subprocess.Popen(['vlc', url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
164
|
+
else:
|
|
165
|
+
console.print(Panel(f"[bold cyan]Secure Stream URL (30s):[/bold cyan]\n{url}", border_style="blue", title="Media Pipe"))
|
|
166
|
+
except Exception as e:
|
|
167
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
168
|
+
|
|
169
|
+
@app.command()
|
|
170
|
+
def files(json_output: bool = typer.Option(False, "--json", help="Output in raw JSON")):
|
|
171
|
+
"""📦 List and search files in your vault."""
|
|
172
|
+
try:
|
|
173
|
+
client = AkashClient()
|
|
174
|
+
file_list = client.list_files()
|
|
175
|
+
|
|
176
|
+
if json_output:
|
|
177
|
+
console.print_json(data=[f.__dict__ for f in file_list])
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
table = Table(title="📦 Encrypted Vault")
|
|
181
|
+
table.add_column("Filename", style="cyan")
|
|
182
|
+
table.add_column("Code", style="yellow")
|
|
183
|
+
table.add_column("Size", justify="right")
|
|
184
|
+
|
|
185
|
+
for f in file_list:
|
|
186
|
+
size_mb = f"{f.size / (1024*1024):.2f} MB"
|
|
187
|
+
table.add_row(f.name, f.code, size_mb)
|
|
188
|
+
|
|
189
|
+
console.print(table)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
192
|
+
|
|
193
|
+
@app.command()
|
|
194
|
+
def download(
|
|
195
|
+
code: str,
|
|
196
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Custom save path")
|
|
197
|
+
):
|
|
198
|
+
"""📥 Download a file from the vault to your local machine."""
|
|
199
|
+
try:
|
|
200
|
+
client = AkashClient()
|
|
201
|
+
# Attach the console to the client for the progress bar
|
|
202
|
+
client.console = console
|
|
203
|
+
|
|
204
|
+
from .downloader import download_file
|
|
205
|
+
path = download_file(client, code, output)
|
|
206
|
+
|
|
207
|
+
console.print(f"[bold green]✓ Download Complete![/bold green]")
|
|
208
|
+
console.print(f"📍 Saved to: [cyan]{path}[/cyan]")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
console.print(f"[bold red]✗ Download Failed:[/bold red] {e}")
|
|
211
|
+
|
|
212
|
+
@app.command()
|
|
213
|
+
def delete(code: str):
|
|
214
|
+
"""🗑️ Permanently remove a file."""
|
|
215
|
+
if typer.confirm(f"Are you sure you want to delete {code}?"):
|
|
216
|
+
try:
|
|
217
|
+
client = AkashClient()
|
|
218
|
+
client.delete_file(code)
|
|
219
|
+
console.print(f"[green]✓ Deleted {code}[/green]")
|
|
220
|
+
except Exception as e:
|
|
221
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
222
|
+
|
|
223
|
+
@app.command()
|
|
224
|
+
def manage(
|
|
225
|
+
code: str,
|
|
226
|
+
name: Optional[str] = typer.Option(None, "--name"),
|
|
227
|
+
burn: Optional[bool] = typer.Option(None, "--burn/--no-burn"),
|
|
228
|
+
expiry: Optional[int] = typer.Option(None, "--expiry")
|
|
229
|
+
):
|
|
230
|
+
"""⚙️ Modify settings for an existing file."""
|
|
231
|
+
try:
|
|
232
|
+
client = AkashClient()
|
|
233
|
+
updates = {}
|
|
234
|
+
if name: updates['filename'] = name
|
|
235
|
+
if burn is not None: updates['burn_on_read'] = 1 if burn else 0
|
|
236
|
+
if expiry is not None: updates['expiry_days'] = expiry
|
|
237
|
+
client.update_file(code, **updates)
|
|
238
|
+
console.print(f"[green]✓ Updated {code}[/green]")
|
|
239
|
+
except Exception as e:
|
|
240
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
241
|
+
|
|
242
|
+
@app.command()
|
|
243
|
+
def share(code: Optional[str] = typer.Argument(None)):
|
|
244
|
+
"""🔗 Share a file via 6-digit OTP (Sleek Interactive UI)."""
|
|
245
|
+
client = AkashClient()
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
if not code:
|
|
249
|
+
with console.status("[bold blue]Fetching your vault..."):
|
|
250
|
+
file_list = client.list_files()
|
|
251
|
+
|
|
252
|
+
if not file_list:
|
|
253
|
+
console.print("[yellow]Your vault is empty.[/yellow]")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
choices = []
|
|
257
|
+
for f in file_list:
|
|
258
|
+
clean_name = f.name.replace("_", " ")
|
|
259
|
+
display_name = (clean_name[:35] + '..') if len(clean_name) > 37 else clean_name
|
|
260
|
+
size_mb = f"{f.size / (1024*1024):.1f}MB"
|
|
261
|
+
|
|
262
|
+
choices.append(questionary.Choice(
|
|
263
|
+
title=[
|
|
264
|
+
("class:emoji", f"{get_file_emoji(f.name)} "),
|
|
265
|
+
("class:text", f"{display_name:<38} "),
|
|
266
|
+
("class:meta", f"{size_mb:>7} "),
|
|
267
|
+
("class:code", f"({f.code})")
|
|
268
|
+
],
|
|
269
|
+
value=f.code
|
|
270
|
+
))
|
|
271
|
+
|
|
272
|
+
choices.append(questionary.Separator())
|
|
273
|
+
choices.append(questionary.Choice("❌ Cancel", value="action_cancel"))
|
|
274
|
+
|
|
275
|
+
custom_style = questionary.Style([
|
|
276
|
+
('qmark', 'fg:#00d2ff bold'),
|
|
277
|
+
('question', 'bold'),
|
|
278
|
+
('pointer', 'fg:#00d2ff bold'),
|
|
279
|
+
('highlighted', 'fg:#000000 bg:#00d2ff bold'),
|
|
280
|
+
('emoji', 'fg:#ffffff'),
|
|
281
|
+
('text', 'fg:#eeeeee'),
|
|
282
|
+
('meta', 'fg:#888888 italic'),
|
|
283
|
+
('code', 'fg:#555555'),
|
|
284
|
+
('selected', 'fg:#00d2ff bold'),
|
|
285
|
+
])
|
|
286
|
+
|
|
287
|
+
code = questionary.select(
|
|
288
|
+
"Select a file to share:",
|
|
289
|
+
choices=choices,
|
|
290
|
+
style=custom_style,
|
|
291
|
+
pointer=" ➜"
|
|
292
|
+
).ask()
|
|
293
|
+
|
|
294
|
+
if not code or code == "action_cancel":
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
# 2. OTP Generation
|
|
298
|
+
# Standardize HTTP client usage
|
|
299
|
+
headers = client._get_headers()
|
|
300
|
+
with httpx.Client(base_url=client.base_url) as c:
|
|
301
|
+
resp = c.post(f"/api/otp/generate/{code}", headers=headers)
|
|
302
|
+
resp.raise_for_status()
|
|
303
|
+
otp = resp.json()['otp']
|
|
304
|
+
|
|
305
|
+
# 🌟 THE FIX: Used valid Rich markup tags 🌟
|
|
306
|
+
# We use [reverse] to make the code stand out instead of 'size'
|
|
307
|
+
otp_display = f"[bold yellow] {otp} [/bold yellow]"
|
|
308
|
+
|
|
309
|
+
console.print("\n")
|
|
310
|
+
console.print(Panel(
|
|
311
|
+
f"Temporary vault code generated:\n\n"
|
|
312
|
+
f"{otp_display}\n\n"
|
|
313
|
+
f"Valid for 60 seconds. Use [bold cyan]akash get {otp}[/bold cyan] on the target device.",
|
|
314
|
+
title="✨ Quick Share",
|
|
315
|
+
border_style="yellow",
|
|
316
|
+
expand=False,
|
|
317
|
+
padding=(1, 5)
|
|
318
|
+
))
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
# Prevent another MarkupError if the error message itself contains brackets
|
|
322
|
+
console.print(f"[bold red]✗ Error:[/bold red] {str(e)}")
|
|
323
|
+
|
|
324
|
+
@app.command()
|
|
325
|
+
def get(otp: str):
|
|
326
|
+
"""✨ Download a file using a 6-digit temporary code."""
|
|
327
|
+
url = os.getenv("AKASH_API_URL", "https://jstore.2bd.net").rstrip("/")
|
|
328
|
+
try:
|
|
329
|
+
with console.status("[bold cyan]Redeeming code..."):
|
|
330
|
+
resp = httpx.get(f"{url}/api/otp/redeem/{otp}")
|
|
331
|
+
if resp.status_code != 200:
|
|
332
|
+
console.print("[red]❌ Invalid or expired code.[/red]")
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
data = resp.json()
|
|
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)
|
|
346
|
+
|
|
347
|
+
console.print(f"[bold green]✓ Successfully retrieved: {filename}[/bold green]")
|
|
348
|
+
except Exception as e:
|
|
349
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
350
|
+
|
|
351
|
+
@app.command()
|
|
352
|
+
def bulk(path: Path):
|
|
353
|
+
"""🚀 Recursive folder vaulting."""
|
|
354
|
+
try:
|
|
355
|
+
client = AkashClient()
|
|
356
|
+
uploader = VaultUploader(client)
|
|
357
|
+
files = [p for p in path.glob("**/*") if p.is_file() and not p.name.startswith('.')]
|
|
358
|
+
for file in files:
|
|
359
|
+
console.print(f"Vaulting: {file.name}")
|
|
360
|
+
uploader.upload(file_path=str(file))
|
|
361
|
+
console.print("[green]✓ Batch complete.[/green]")
|
|
362
|
+
except Exception as e:
|
|
363
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
364
|
+
|
|
365
|
+
@app.command()
|
|
366
|
+
def docs():
|
|
367
|
+
"""📖 View the integration guide."""
|
|
368
|
+
webbrowser.open("https://github.com/juniorsir/akashcli")
|
|
369
|
+
|
|
370
|
+
@app.command()
|
|
371
|
+
def logout():
|
|
372
|
+
"""🚪 Clear local credentials."""
|
|
373
|
+
ConfigManager.save(b"")
|
|
374
|
+
console.print("[yellow]Logged out.[/yellow]")
|
|
375
|
+
|
|
376
|
+
if __name__ == "__main__":
|
|
377
|
+
app()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, List, Callable
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import jwt
|
|
7
|
+
|
|
8
|
+
# Internal Imports
|
|
9
|
+
from .models import UploadResult, FileInfo, UserInfo
|
|
10
|
+
from .exceptions import *
|
|
11
|
+
from .config import ConfigManager
|
|
12
|
+
|
|
13
|
+
class AkashClient:
|
|
14
|
+
def __init__(self, api_key: Optional[str] = None, api_url: Optional[str] = None):
|
|
15
|
+
config = ConfigManager.load()
|
|
16
|
+
# API Key can come from: 1. Argument, 2. Env Var, 3. Config File
|
|
17
|
+
self.api_key = api_key or os.getenv("AKASH_API_KEY") or config.get("api_key")
|
|
18
|
+
self.base_url = (api_url or os.getenv("AKASH_API_URL") or config.get("api_url", "https://jstore.2bd.net")).rstrip("/")
|
|
19
|
+
|
|
20
|
+
if not self.api_key:
|
|
21
|
+
raise AuthenticationError("No API Key found. Run 'akash login' or set AKASH_API_KEY env var.")
|
|
22
|
+
|
|
23
|
+
def _get_headers(self, custom: dict = None):
|
|
24
|
+
"""Standard headers for all Akash API requests."""
|
|
25
|
+
h = {
|
|
26
|
+
"x-api-key": self.api_key,
|
|
27
|
+
"Origin": "https://akash.cli",
|
|
28
|
+
"User-Agent": "AkashCLI/3.0.0"
|
|
29
|
+
}
|
|
30
|
+
if custom: h.update(custom)
|
|
31
|
+
return h
|
|
32
|
+
|
|
33
|
+
def whoami(self) -> UserInfo:
|
|
34
|
+
"""Fetches account identity and quota info."""
|
|
35
|
+
try:
|
|
36
|
+
with httpx.Client(base_url=self.base_url) as client:
|
|
37
|
+
resp = client.get("/api/me", headers=self._get_headers())
|
|
38
|
+
if resp.status_code != 200:
|
|
39
|
+
raise AuthenticationError(f"Invalid API Key or server rejected request ({resp.status_code})")
|
|
40
|
+
|
|
41
|
+
d = resp.json()
|
|
42
|
+
return UserInfo(
|
|
43
|
+
owner=d['owner'],
|
|
44
|
+
quota_used=d['quota']['used'],
|
|
45
|
+
quota_limit=d['quota']['limit'],
|
|
46
|
+
allowed_domains=d['allowed_domains']
|
|
47
|
+
)
|
|
48
|
+
except httpx.RequestError as e:
|
|
49
|
+
raise NetworkError(f"Vault server unreachable: {e}")
|
|
50
|
+
|
|
51
|
+
def upload_file(
|
|
52
|
+
self,
|
|
53
|
+
path: str,
|
|
54
|
+
burn: bool = False,
|
|
55
|
+
expiry: int = 0,
|
|
56
|
+
password: Optional[str] = None,
|
|
57
|
+
progress_callback: Optional[Callable[[int, int], None]] = None
|
|
58
|
+
) -> UploadResult:
|
|
59
|
+
"""
|
|
60
|
+
Uploads a physical file with optional progress tracking.
|
|
61
|
+
🌟 REFACTORED: Now correctly delegates to VaultUploader.
|
|
62
|
+
"""
|
|
63
|
+
from .uploader import VaultUploader
|
|
64
|
+
uploader = VaultUploader(self)
|
|
65
|
+
return uploader.upload(
|
|
66
|
+
file_path=path,
|
|
67
|
+
burn=burn,
|
|
68
|
+
expiry=expiry,
|
|
69
|
+
password=password,
|
|
70
|
+
progress_callback=progress_callback
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def upload_text(
|
|
74
|
+
self,
|
|
75
|
+
content: str,
|
|
76
|
+
filename: str = None,
|
|
77
|
+
burn: bool = False,
|
|
78
|
+
expiry: int = 0,
|
|
79
|
+
password: Optional[str] = None
|
|
80
|
+
) -> UploadResult:
|
|
81
|
+
"""Vaults raw text as a snippet."""
|
|
82
|
+
from .uploader import VaultUploader
|
|
83
|
+
uploader = VaultUploader(self)
|
|
84
|
+
return uploader.upload(
|
|
85
|
+
content=content,
|
|
86
|
+
filename=filename,
|
|
87
|
+
burn=burn,
|
|
88
|
+
expiry=expiry,
|
|
89
|
+
password=password
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def list_files(self) -> List[FileInfo]:
|
|
93
|
+
"""Returns a list of FileInfo objects from the vault."""
|
|
94
|
+
try:
|
|
95
|
+
with httpx.Client(base_url=self.base_url) as client:
|
|
96
|
+
resp = client.get("/api/files", headers=self._get_headers())
|
|
97
|
+
resp.raise_for_status()
|
|
98
|
+
return [
|
|
99
|
+
FileInfo(f['name'], f['code'], f['size'], f['uploaded_at'])
|
|
100
|
+
for f in resp.json()
|
|
101
|
+
]
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise AkashError(f"Failed to list files: {e}")
|
|
104
|
+
|
|
105
|
+
def delete_file(self, code: str) -> bool:
|
|
106
|
+
"""Permanently deletes a file by its vault code."""
|
|
107
|
+
try:
|
|
108
|
+
with httpx.Client(base_url=self.base_url) as client:
|
|
109
|
+
resp = client.delete(f"/api/files/{code}", headers=self._get_headers())
|
|
110
|
+
if resp.status_code == 404:
|
|
111
|
+
raise VaultFileNotFound(f"File {code} not found.")
|
|
112
|
+
resp.raise_for_status()
|
|
113
|
+
return True
|
|
114
|
+
except httpx.HTTPStatusError as e:
|
|
115
|
+
raise AkashError(f"Delete failed: {e.response.text}")
|
|
116
|
+
|
|
117
|
+
def update_file(self, code: str, **kwargs) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
Modify existing file security (filename, burn_on_read, expiry_days).
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
with httpx.Client(base_url=self.base_url, timeout=10.0) as client:
|
|
123
|
+
resp = client.patch(
|
|
124
|
+
f"/api/files/{code}",
|
|
125
|
+
headers=self._get_headers(),
|
|
126
|
+
json=kwargs
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if resp.status_code == 404:
|
|
130
|
+
raise VaultFileNotFound(f"File code '{code}' does not exist.")
|
|
131
|
+
if resp.status_code in [401, 403]:
|
|
132
|
+
raise AuthenticationError("Insufficient permissions to modify this content.")
|
|
133
|
+
|
|
134
|
+
resp.raise_for_status()
|
|
135
|
+
return resp.json()
|
|
136
|
+
|
|
137
|
+
except httpx.RequestError as e:
|
|
138
|
+
raise NetworkError(f"Network error: {e}")
|
|
139
|
+
|
|
140
|
+
def get_stats(self) -> dict:
|
|
141
|
+
"""Admin Only: Fetch global vault diagnostics."""
|
|
142
|
+
# Note: Reuses whoami logic as per current API structure
|
|
143
|
+
return self.whoami().__dict__
|
|
144
|
+
|
|
145
|
+
# Inside AkashClient class in client.py
|
|
146
|
+
|
|
147
|
+
def get_upload_status(self, code: str) -> dict:
|
|
148
|
+
"""Polls the server to check if background vaulting is finished."""
|
|
149
|
+
try:
|
|
150
|
+
with httpx.Client(base_url=self.base_url) as client:
|
|
151
|
+
resp = client.get(f"/api/status/{code}", headers=self._get_headers())
|
|
152
|
+
resp.raise_for_status()
|
|
153
|
+
return resp.json()
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return {"status": "request_error", "detail": str(e)}
|
|
156
|
+
|
|
157
|
+
def generate_secure_link(self, code: str) -> dict:
|
|
158
|
+
"""Fetches the short-lived IP-bound token."""
|
|
159
|
+
with httpx.Client(base_url=self.base_url) as client:
|
|
160
|
+
resp = client.get(f"/api/stream/generate/{code}", headers=self._get_headers())
|
|
161
|
+
# If server returns 202, this will raise an error containing '202'
|
|
162
|
+
# which our CLI 'stream' command catches to show the 'Waiting' status.
|
|
163
|
+
resp.raise_for_status()
|
|
164
|
+
return resp.json()
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import httpx
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Callable, List
|
|
5
|
+
from .models import UploadResult
|
|
6
|
+
from .exceptions import UploadFailed, QuotaExceeded
|
|
7
|
+
|
|
8
|
+
class ProgressStream:
|
|
9
|
+
"""
|
|
10
|
+
A file-wrapper that updates a progress bar while the
|
|
11
|
+
network library reads data from it.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, file_path: str, callback: Optional[Callable]):
|
|
14
|
+
self.file = open(file_path, "rb")
|
|
15
|
+
self.total_size = os.path.getsize(file_path)
|
|
16
|
+
self.callback = callback
|
|
17
|
+
self.bytes_read = 0
|
|
18
|
+
|
|
19
|
+
def __len__(self):
|
|
20
|
+
return self.total_size
|
|
21
|
+
|
|
22
|
+
def read(self, size=-1):
|
|
23
|
+
chunk = self.file.read(size)
|
|
24
|
+
if chunk:
|
|
25
|
+
self.bytes_read += len(chunk)
|
|
26
|
+
if self.callback:
|
|
27
|
+
self.callback(self.bytes_read, self.total_size)
|
|
28
|
+
return chunk
|
|
29
|
+
|
|
30
|
+
def close(self):
|
|
31
|
+
self.file.close()
|
|
32
|
+
|
|
33
|
+
class VaultUploader:
|
|
34
|
+
def __init__(self, client):
|
|
35
|
+
self.client = client
|
|
36
|
+
|
|
37
|
+
def upload(
|
|
38
|
+
self,
|
|
39
|
+
file_path: Optional[str] = None,
|
|
40
|
+
content: Optional[str] = None,
|
|
41
|
+
filename: Optional[str] = None,
|
|
42
|
+
burn: bool = False,
|
|
43
|
+
expiry: int = 0,
|
|
44
|
+
password: Optional[str] = None,
|
|
45
|
+
progress_callback: Optional[Callable[[int, int], None]] = None
|
|
46
|
+
) -> UploadResult:
|
|
47
|
+
|
|
48
|
+
# 1. Prepare Headers (V3 Spec)
|
|
49
|
+
headers = {
|
|
50
|
+
"x-vault-burn": "1" if burn else "0",
|
|
51
|
+
"x-vault-expiry": str(expiry),
|
|
52
|
+
}
|
|
53
|
+
if password:
|
|
54
|
+
headers["x-vault-password"] = password
|
|
55
|
+
|
|
56
|
+
url = f"{self.client.base_url}/api/upload"
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# --- CASE A: TEXT SNIPPET (JSON) ---
|
|
60
|
+
if content is not None:
|
|
61
|
+
payload = {
|
|
62
|
+
"content": content,
|
|
63
|
+
"filename": filename or "snippet.txt"
|
|
64
|
+
}
|
|
65
|
+
resp = httpx.post(
|
|
66
|
+
url,
|
|
67
|
+
json=payload,
|
|
68
|
+
headers=self.client._get_headers(headers),
|
|
69
|
+
timeout=None
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# --- CASE B: FILE UPLOAD (Multipart) ---
|
|
73
|
+
else:
|
|
74
|
+
if not file_path or not os.path.exists(file_path):
|
|
75
|
+
raise UploadFailed(f"File not found: {file_path}")
|
|
76
|
+
|
|
77
|
+
# 🌟 THE FIX: Use the ProgressStream wrapper instead of a generator
|
|
78
|
+
stream = ProgressStream(file_path, progress_callback)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
files = {
|
|
82
|
+
"file": (os.path.basename(file_path), stream, "application/octet-stream")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resp = httpx.post(
|
|
86
|
+
url,
|
|
87
|
+
files=files,
|
|
88
|
+
headers=self.client._get_headers(headers),
|
|
89
|
+
timeout=None
|
|
90
|
+
)
|
|
91
|
+
finally:
|
|
92
|
+
stream.close()
|
|
93
|
+
|
|
94
|
+
# 2. Handle Response
|
|
95
|
+
if resp.status_code == 429:
|
|
96
|
+
raise QuotaExceeded("Upload quota reached.")
|
|
97
|
+
|
|
98
|
+
resp.raise_for_status()
|
|
99
|
+
d = resp.json()
|
|
100
|
+
|
|
101
|
+
return UploadResult(
|
|
102
|
+
status=d['status'],
|
|
103
|
+
file_code=d['file_code'],
|
|
104
|
+
embed_url=d['links']['embed'],
|
|
105
|
+
direct_url=d['links']['direct'],
|
|
106
|
+
input_type=d.get('input_type', 'file'),
|
|
107
|
+
dna=d.get('dna', {})
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
except httpx.HTTPStatusError as e:
|
|
111
|
+
raise UploadFailed(f"Server Error ({e.response.status_code}): {e.response.text}")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise UploadFailed(f"Connection Error: {e}")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import hashlib
|
|
3
|
+
import platform
|
|
4
|
+
|
|
5
|
+
def get_device_id() -> str:
|
|
6
|
+
"""Generates a unique hardware fingerprint for encrypted config binding."""
|
|
7
|
+
# Combine node (MAC address) and system info for a stable ID
|
|
8
|
+
raw_id = f"{uuid.getnode()}-{platform.node()}-{platform.processor()}"
|
|
9
|
+
return hashlib.sha256(raw_id.encode()).hexdigest()
|
|
10
|
+
|
|
11
|
+
def get_file_emoji(filename: str) -> str:
|
|
12
|
+
"""Returns a specific emoji based on the file extension for the CLI UI."""
|
|
13
|
+
if not filename:
|
|
14
|
+
return "📄"
|
|
15
|
+
|
|
16
|
+
fn = filename.lower()
|
|
17
|
+
|
|
18
|
+
# Videos
|
|
19
|
+
if fn.endswith(('.mp4', '.mkv', '.mov', '.webm', '.avi', '.m4v')):
|
|
20
|
+
return "🎬"
|
|
21
|
+
# Images
|
|
22
|
+
if fn.endswith(('.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp', '.svg', '.ico')):
|
|
23
|
+
return "🖼️"
|
|
24
|
+
# Audio
|
|
25
|
+
if fn.endswith(('.mp3', '.ogg', '.wav', '.flac', '.m4a', '.opus', '.aac')):
|
|
26
|
+
return "🎵"
|
|
27
|
+
# Code / Developer Files
|
|
28
|
+
if fn.endswith(('.py', '.js', '.ts', '.sh', '.bash', '.html', '.css', '.json', '.yaml', '.yml', '.c', '.cpp', '.rs', '.go', '.php', '.sql')):
|
|
29
|
+
return "💻"
|
|
30
|
+
# Documents
|
|
31
|
+
if fn.endswith(('.pdf', '.docx', '.doc', '.xlsx', '.pptx', '.txt', '.rtf')):
|
|
32
|
+
return "📑"
|
|
33
|
+
# Compressed Archives
|
|
34
|
+
if fn.endswith(('.zip', '.rar', '.7z', '.tar', '.gz')):
|
|
35
|
+
return "📦"
|
|
36
|
+
|
|
37
|
+
# Default
|
|
38
|
+
return "📄"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: akashcli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Enterprise Developer SDK and CLI for Akash Vault
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,6 +8,7 @@ Requires-Dist: httpx
|
|
|
8
8
|
Requires-Dist: typer[all]
|
|
9
9
|
Requires-Dist: rich
|
|
10
10
|
Requires-Dist: cryptography
|
|
11
|
+
Requires-Dist: questionary
|
|
11
12
|
|
|
12
13
|
To get started with **AkashCLI**, your users should follow these steps. You can provide this guide as a `README.md` or a "Getting Started" message in your bot.
|
|
13
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "akashcli"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.2.0"
|
|
4
4
|
description = "Enterprise Developer SDK and CLI for Akash Vault"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -9,6 +9,7 @@ dependencies = [
|
|
|
9
9
|
"typer[all]",
|
|
10
10
|
"rich",
|
|
11
11
|
"cryptography",
|
|
12
|
+
"questionary",
|
|
12
13
|
]
|
|
13
14
|
|
|
14
15
|
[project.scripts]
|
akashcli-3.0.0/akashcli/cli.py
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import subprocess
|
|
5
|
-
import webbrowser
|
|
6
|
-
import httpx
|
|
7
|
-
from typing import Optional
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from rich.console import Console
|
|
10
|
-
from rich.table import Table
|
|
11
|
-
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, DownloadColumn, TransferSpeedColumn
|
|
12
|
-
from rich.panel import Panel
|
|
13
|
-
|
|
14
|
-
# Internal Imports
|
|
15
|
-
from .client import AkashClient
|
|
16
|
-
from .auth import AuthManager
|
|
17
|
-
from .config import ConfigManager
|
|
18
|
-
from .exceptions import AkashError
|
|
19
|
-
from .uploader import VaultUploader
|
|
20
|
-
|
|
21
|
-
# 🌟 Removed the 'gh' overrides and panels
|
|
22
|
-
app = typer.Typer(help="🛡️ Akash Vault Enterprise CLI", add_completion=True)
|
|
23
|
-
console = Console()
|
|
24
|
-
|
|
25
|
-
@app.command()
|
|
26
|
-
def login(
|
|
27
|
-
url: str = typer.Option("https://jstore.2bd.net", "--url", help="Custom Vault URL"),
|
|
28
|
-
key: str = typer.Option(..., prompt="🔑 Enter API Key", hide_input=True)
|
|
29
|
-
):
|
|
30
|
-
"""🔑 Link your device to the vault."""
|
|
31
|
-
try:
|
|
32
|
-
with console.status("[bold blue]Verifying identity..."):
|
|
33
|
-
user = AuthManager.perform_login(key, url)
|
|
34
|
-
|
|
35
|
-
console.print(f"[green]✓ Login Successful![/green] Owner: {user.owner}")
|
|
36
|
-
except Exception as e:
|
|
37
|
-
console.print(f"[bold red]✗ Authentication Failed:[/bold red] {e}")
|
|
38
|
-
|
|
39
|
-
@app.command()
|
|
40
|
-
def whoami():
|
|
41
|
-
"""👤 Check current login and quota status."""
|
|
42
|
-
try:
|
|
43
|
-
client = AkashClient()
|
|
44
|
-
user = client.whoami()
|
|
45
|
-
console.print(f"Logged in as: [bold cyan]{user.owner}[/bold cyan]")
|
|
46
|
-
console.print(f"Storage Quota: [bold yellow]{user.quota_used}/{user.quota_limit}[/bold yellow]")
|
|
47
|
-
except Exception as e:
|
|
48
|
-
console.print(f"[red]Error:[/red] {e}")
|
|
49
|
-
|
|
50
|
-
@app.command()
|
|
51
|
-
def upload(
|
|
52
|
-
path: Optional[Path] = typer.Argument(None, help="Path to file"),
|
|
53
|
-
text: Optional[str] = typer.Option(None, "--text", "-t", help="Upload raw text content"),
|
|
54
|
-
name: Optional[str] = typer.Option(None, "--name", "-n", help="Custom filename"),
|
|
55
|
-
burn: bool = typer.Option(False, "--burn", help="Self-destruct after one view"),
|
|
56
|
-
expiry: int = typer.Option(0, "--expiry", "-e", help="Expiry in days (0 for forever)"),
|
|
57
|
-
password: Optional[str] = typer.Option(None, "--pass", "-p", help="Password protect file")
|
|
58
|
-
):
|
|
59
|
-
"""📤 Vault a single file or text snippet."""
|
|
60
|
-
try:
|
|
61
|
-
client = AkashClient()
|
|
62
|
-
|
|
63
|
-
with Progress(
|
|
64
|
-
SpinnerColumn(),
|
|
65
|
-
TextColumn("[progress.description]{task.description}"),
|
|
66
|
-
BarColumn(),
|
|
67
|
-
console=console,
|
|
68
|
-
transient=True
|
|
69
|
-
) as progress:
|
|
70
|
-
task = progress.add_task(description="[cyan]Processing...", total=100)
|
|
71
|
-
def update_bar(current, total):
|
|
72
|
-
progress.update(task, completed=(current/total)*100)
|
|
73
|
-
|
|
74
|
-
if text:
|
|
75
|
-
res = client.upload_text(text, filename=name, burn=burn, expiry=expiry, password=password)
|
|
76
|
-
else:
|
|
77
|
-
if not path:
|
|
78
|
-
console.print("[red]Error:[/red] Provide a file path or use --text")
|
|
79
|
-
return
|
|
80
|
-
res = client.upload_file(str(path), burn=burn, expiry=expiry, password=password, progress_callback=update_bar)
|
|
81
|
-
|
|
82
|
-
console.print(f"[bold green]✓ Vaulted![/bold green] Code: [yellow]{res.file_code}[/yellow]")
|
|
83
|
-
except Exception as e:
|
|
84
|
-
console.print(f"[bold red]✗ Upload Failed:[/bold red] {e}")
|
|
85
|
-
|
|
86
|
-
@app.command()
|
|
87
|
-
def files(json_output: bool = typer.Option(False, "--json", help="Output in raw JSON")):
|
|
88
|
-
"""📦 List and search files in your vault."""
|
|
89
|
-
try:
|
|
90
|
-
client = AkashClient()
|
|
91
|
-
file_list = client.list_files()
|
|
92
|
-
|
|
93
|
-
if json_output:
|
|
94
|
-
console.print_json(data=[f.__dict__ for f in file_list])
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
table = Table(title="📦 Encrypted Vault")
|
|
98
|
-
table.add_column("Filename", style="cyan")
|
|
99
|
-
table.add_column("Code", style="yellow")
|
|
100
|
-
table.add_column("Size", justify="right")
|
|
101
|
-
|
|
102
|
-
for f in file_list:
|
|
103
|
-
size_mb = f"{f.size / (1024*1024):.2f} MB"
|
|
104
|
-
table.add_row(f.name, f.code, size_mb)
|
|
105
|
-
|
|
106
|
-
console.print(table)
|
|
107
|
-
except Exception as e:
|
|
108
|
-
console.print(f"[red]Error:[/red] {e}")
|
|
109
|
-
|
|
110
|
-
@app.command()
|
|
111
|
-
def stream(code: str, vlc: bool = typer.Option(False, "--vlc", help="Open directly in VLC")):
|
|
112
|
-
"""🎬 Get a secure streaming link."""
|
|
113
|
-
client = AkashClient()
|
|
114
|
-
try:
|
|
115
|
-
with httpx.Client(base_url=client.base_url) as c:
|
|
116
|
-
resp = c.get(f"/api/stream/generate/{code}", headers=client._get_headers())
|
|
117
|
-
resp.raise_for_status()
|
|
118
|
-
url = resp.json()['stream_url']
|
|
119
|
-
|
|
120
|
-
if vlc:
|
|
121
|
-
console.print(f"🎬 Launching VLC for: {code}")
|
|
122
|
-
subprocess.Popen(['vlc', url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
123
|
-
else:
|
|
124
|
-
console.print(f"🎬 Secure URL: {url}")
|
|
125
|
-
except Exception as e:
|
|
126
|
-
console.print(f"[red]Error:[/red] {e}")
|
|
127
|
-
|
|
128
|
-
@app.command()
|
|
129
|
-
def delete(code: str):
|
|
130
|
-
"""🗑️ Permanently remove a file."""
|
|
131
|
-
if typer.confirm(f"Are you sure you want to delete {code}?"):
|
|
132
|
-
try:
|
|
133
|
-
client = AkashClient()
|
|
134
|
-
client.delete_file(code)
|
|
135
|
-
console.print(f"[green]✓ Deleted {code}[/green]")
|
|
136
|
-
except Exception as e:
|
|
137
|
-
console.print(f"[red]Error:[/red] {e}")
|
|
138
|
-
|
|
139
|
-
@app.command()
|
|
140
|
-
def manage(
|
|
141
|
-
code: str,
|
|
142
|
-
name: Optional[str] = typer.Option(None, "--name"),
|
|
143
|
-
burn: Optional[bool] = typer.Option(None, "--burn/--no-burn"),
|
|
144
|
-
expiry: Optional[int] = typer.Option(None, "--expiry")
|
|
145
|
-
):
|
|
146
|
-
"""⚙️ Modify settings for an existing file."""
|
|
147
|
-
try:
|
|
148
|
-
client = AkashClient()
|
|
149
|
-
updates = {}
|
|
150
|
-
if name: updates['filename'] = name
|
|
151
|
-
if burn is not None: updates['burn_on_read'] = 1 if burn else 0
|
|
152
|
-
if expiry is not None: updates['expiry_days'] = expiry
|
|
153
|
-
client.update_file(code, **updates)
|
|
154
|
-
console.print(f"[green]✓ Updated {code}[/green]")
|
|
155
|
-
except Exception as e:
|
|
156
|
-
console.print(f"[red]Error:[/red] {e}")
|
|
157
|
-
|
|
158
|
-
@app.command()
|
|
159
|
-
def bulk(path: Path):
|
|
160
|
-
"""🚀 Recursive folder vaulting."""
|
|
161
|
-
try:
|
|
162
|
-
client = AkashClient()
|
|
163
|
-
uploader = VaultUploader(client)
|
|
164
|
-
files = [p for p in path.glob("**/*") if p.is_file() and not p.name.startswith('.')]
|
|
165
|
-
for file in files:
|
|
166
|
-
console.print(f"Vaulting: {file.name}")
|
|
167
|
-
uploader.upload(file_path=str(file))
|
|
168
|
-
console.print("[green]✓ Batch complete.[/green]")
|
|
169
|
-
except Exception as e:
|
|
170
|
-
console.print(f"[red]Error:[/red] {e}")
|
|
171
|
-
|
|
172
|
-
@app.command()
|
|
173
|
-
def docs():
|
|
174
|
-
"""📖 View the integration guide."""
|
|
175
|
-
webbrowser.open("https://github.com/juniorsir/akashcli")
|
|
176
|
-
|
|
177
|
-
@app.command()
|
|
178
|
-
def logout():
|
|
179
|
-
"""🚪 Clear local credentials."""
|
|
180
|
-
ConfigManager.save(b"")
|
|
181
|
-
console.print("[yellow]Logged out.[/yellow]")
|
|
182
|
-
|
|
183
|
-
if __name__ == "__main__":
|
|
184
|
-
app()
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import httpx
|
|
2
|
-
import time
|
|
3
|
-
from typing import List, Optional
|
|
4
|
-
from .models import UploadResult, FileInfo, UserInfo
|
|
5
|
-
from .exceptions import *
|
|
6
|
-
from .config import ConfigManager
|
|
7
|
-
|
|
8
|
-
class AkashClient:
|
|
9
|
-
def __init__(self, api_key: Optional[str] = None, api_url: Optional[str] = None):
|
|
10
|
-
config = ConfigManager.load()
|
|
11
|
-
self.api_key = api_key or config.get("api_key")
|
|
12
|
-
self.base_url = (api_url or config.get("api_url", "https://jstore.2bd.net")).rstrip("/")
|
|
13
|
-
|
|
14
|
-
if not self.api_key:
|
|
15
|
-
raise AuthenticationError("No API Key found. Run 'akash login'")
|
|
16
|
-
|
|
17
|
-
def _get_headers(self, custom: dict = None):
|
|
18
|
-
h = {"x-api-key": self.api_key, "Origin": "https://akash.cli"}
|
|
19
|
-
if custom: h.update(custom)
|
|
20
|
-
return h
|
|
21
|
-
|
|
22
|
-
def whoami(self) -> UserInfo:
|
|
23
|
-
with httpx.Client(base_url=self.base_url) as client:
|
|
24
|
-
resp = client.get("/api/me", headers=self._get_headers())
|
|
25
|
-
if resp.status_code != 200: raise AuthenticationError("Invalid Key")
|
|
26
|
-
d = resp.json()
|
|
27
|
-
return UserInfo(d['owner'], d['quota']['used'], d['quota']['limit'], d['allowed_domains'])
|
|
28
|
-
|
|
29
|
-
def upload_file(self, path: str, burn: bool = False, expiry: int = 0) -> UploadResult:
|
|
30
|
-
files = {"file": open(path, "rb")}
|
|
31
|
-
headers = {
|
|
32
|
-
"x-vault-burn": "1" if burn else "0",
|
|
33
|
-
"x-vault-expiry": str(expiry)
|
|
34
|
-
}
|
|
35
|
-
with httpx.Client(base_url=self.base_url, timeout=None) as client:
|
|
36
|
-
resp = client.post("/api/upload", headers=self._get_headers(headers), files=files)
|
|
37
|
-
resp.raise_for_status()
|
|
38
|
-
d = resp.json()
|
|
39
|
-
return UploadResult(
|
|
40
|
-
d['status'], d['file_code'], d['links']['embed'],
|
|
41
|
-
d['links']['direct'], d['input_type'], d.get('dna', {})
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
def list_files(self) -> List[FileInfo]:
|
|
45
|
-
with httpx.Client(base_url=self.base_url) as client:
|
|
46
|
-
resp = client.get("/api/files", headers=self._get_headers())
|
|
47
|
-
return [FileInfo(f['name'], f['code'], f['size'], f['uploaded_at']) for f in resp.json()]
|
|
48
|
-
|
|
49
|
-
def delete_file(self, code: str):
|
|
50
|
-
with httpx.Client(base_url=self.base_url) as client:
|
|
51
|
-
client.delete(f"/api/files/{code}", headers=self._get_headers())
|
|
52
|
-
|
|
53
|
-
def upload_text(self, content: str, filename: str = None, burn: bool = False, expiry: int = 0, password: str = None):
|
|
54
|
-
from .uploader import VaultUploader
|
|
55
|
-
uploader = VaultUploader(self)
|
|
56
|
-
return uploader.upload(content=content, filename=filename, burn=burn, expiry=expiry, password=password)
|
|
57
|
-
|
|
58
|
-
def update_file(self, code: str, **kwargs) -> dict:
|
|
59
|
-
"""Update file settings (filename, burn_on_read, expiry_days)."""
|
|
60
|
-
with httpx.Client(base_url=self.base_url) as client:
|
|
61
|
-
resp = client.patch(
|
|
62
|
-
f"/api/files/{code}",
|
|
63
|
-
headers=self._get_headers(),
|
|
64
|
-
json=kwargs
|
|
65
|
-
)
|
|
66
|
-
resp.raise_for_status()
|
|
67
|
-
return resp.json()
|
|
68
|
-
|
|
69
|
-
def get_stats(self) -> dict:
|
|
70
|
-
"""Admin Only: Fetch global vault statistics."""
|
|
71
|
-
with httpx.Client(base_url=self.base_url) as client:
|
|
72
|
-
# Reuses the logic from your bot's show_stats
|
|
73
|
-
resp = client.get("/api/me", headers=self._get_headers())
|
|
74
|
-
return resp.json()
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import httpx, asyncio
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Optional, Callable, Dict, Any, List
|
|
5
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
-
from .models import UploadResult
|
|
7
|
-
from .exceptions import UploadFailed, QuotaExceeded
|
|
8
|
-
|
|
9
|
-
class VaultUploader:
|
|
10
|
-
def __init__(self, client):
|
|
11
|
-
self.client = client # Reference to AkashClient for headers/url
|
|
12
|
-
|
|
13
|
-
def upload(
|
|
14
|
-
self,
|
|
15
|
-
file_path: Optional[str] = None,
|
|
16
|
-
content: Optional[str] = None,
|
|
17
|
-
filename: Optional[str] = None,
|
|
18
|
-
burn: bool = False,
|
|
19
|
-
expiry: int = 0,
|
|
20
|
-
password: Optional[str] = None,
|
|
21
|
-
progress_callback: Optional[Callable[[int, int], None]] = None
|
|
22
|
-
) -> UploadResult:
|
|
23
|
-
"""
|
|
24
|
-
Standardizes both File and Text uploads into a single stream.
|
|
25
|
-
progress_callback signature: (bytes_read, total_bytes)
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
# 1. Prepare Headers based on V3 Power-Control Specs
|
|
29
|
-
headers = {
|
|
30
|
-
"x-vault-burn": "1" if burn else "0",
|
|
31
|
-
"x-vault-expiry": str(expiry),
|
|
32
|
-
}
|
|
33
|
-
if password:
|
|
34
|
-
headers["x-vault-password"] = password
|
|
35
|
-
|
|
36
|
-
url = f"{self.client.base_url}/api/upload"
|
|
37
|
-
|
|
38
|
-
try:
|
|
39
|
-
if content is not None:
|
|
40
|
-
# --- TEXT SNIPPET MODE (JSON) ---
|
|
41
|
-
payload = {
|
|
42
|
-
"content": content,
|
|
43
|
-
"filename": filename or "snippet.txt"
|
|
44
|
-
}
|
|
45
|
-
resp = httpx.post(
|
|
46
|
-
url,
|
|
47
|
-
json=payload,
|
|
48
|
-
headers=self.client._get_headers(headers),
|
|
49
|
-
timeout=None
|
|
50
|
-
)
|
|
51
|
-
else:
|
|
52
|
-
# --- FILE MODE (Multipart) ---
|
|
53
|
-
if not file_path or not os.path.exists(file_path):
|
|
54
|
-
raise UploadFailed(f"File not found: {file_path}")
|
|
55
|
-
|
|
56
|
-
file_size = os.path.getsize(file_path)
|
|
57
|
-
|
|
58
|
-
# Setup custom stream to track progress
|
|
59
|
-
def file_generator():
|
|
60
|
-
with open(file_path, "rb") as f:
|
|
61
|
-
chunk_size = 1024 * 1024 # 1MB chunks
|
|
62
|
-
bytes_read = 0
|
|
63
|
-
while chunk := f.read(chunk_size):
|
|
64
|
-
bytes_read += len(chunk)
|
|
65
|
-
if progress_callback:
|
|
66
|
-
progress_callback(bytes_read, file_size)
|
|
67
|
-
yield chunk
|
|
68
|
-
|
|
69
|
-
files = {"file": (os.path.basename(file_path), file_generator())}
|
|
70
|
-
|
|
71
|
-
resp = httpx.post(
|
|
72
|
-
url,
|
|
73
|
-
files=files,
|
|
74
|
-
headers=self.client._get_headers(headers),
|
|
75
|
-
timeout=None
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
if resp.status_code == 429:
|
|
79
|
-
raise QuotaExceeded("Monthly upload quota reached.")
|
|
80
|
-
|
|
81
|
-
resp.raise_for_status()
|
|
82
|
-
d = resp.json()
|
|
83
|
-
|
|
84
|
-
return UploadResult(
|
|
85
|
-
status=d['status'],
|
|
86
|
-
file_code=d['file_code'],
|
|
87
|
-
embed_url=d['links']['embed'],
|
|
88
|
-
direct_url=d['links']['direct'],
|
|
89
|
-
input_type=d['input_type'],
|
|
90
|
-
dna=d.get('dna', {})
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
except httpx.HTTPStatusError as e:
|
|
94
|
-
raise UploadFailed(f"Server rejected upload ({e.response.status_code}): {e.response.text}")
|
|
95
|
-
except Exception as e:
|
|
96
|
-
raise UploadFailed(f"Unexpected error during upload: {e}")
|
|
97
|
-
|
|
98
|
-
def upload_directory(self, folder_path: str, max_workers: int = 3) -> List[UploadResult]:
|
|
99
|
-
"""Recursively uploads all files in a folder using a thread pool."""
|
|
100
|
-
# Convert string path to Path object
|
|
101
|
-
root = Path(folder_path)
|
|
102
|
-
if not root.is_dir():
|
|
103
|
-
raise UploadFailed(f"Source is not a directory: {folder_path}")
|
|
104
|
-
|
|
105
|
-
# Find all files recursively (excluding hidden files/folders)
|
|
106
|
-
paths = [p for p in root.glob("**/*") if p.is_file() and not p.name.startswith('.')]
|
|
107
|
-
results = []
|
|
108
|
-
|
|
109
|
-
# Using ThreadPoolExecutor for parallel I/O bound tasks
|
|
110
|
-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
111
|
-
# We wrap the self.upload calls into the executor
|
|
112
|
-
# Note: We don't pass a progress_callback here to avoid UI overlap
|
|
113
|
-
# (The CLI 'bulk' command will handle the UI instead)
|
|
114
|
-
futures = [executor.submit(self.upload, file_path=str(p)) for p in paths]
|
|
115
|
-
|
|
116
|
-
for f in futures:
|
|
117
|
-
try:
|
|
118
|
-
results.append(f.result())
|
|
119
|
-
except Exception as e:
|
|
120
|
-
# Log error but continue with other files
|
|
121
|
-
print(f"Error uploading a file in batch: {e}")
|
|
122
|
-
|
|
123
|
-
return results
|
akashcli-3.0.0/akashcli/utils.py
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import uuid
|
|
2
|
-
import hashlib
|
|
3
|
-
import platform
|
|
4
|
-
|
|
5
|
-
def get_device_id() -> str:
|
|
6
|
-
"""Generates a unique hardware fingerprint."""
|
|
7
|
-
# Combine node (MAC address) and system info for a stable ID
|
|
8
|
-
raw_id = f"{uuid.getnode()}-{platform.node()}-{platform.processor()}"
|
|
9
|
-
return hashlib.sha256(raw_id.encode()).hexdigest()
|
|
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
|