televault 0.1.0__py3-none-any.whl → 2.0.0__py3-none-any.whl

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.
televault/cli.py CHANGED
@@ -3,17 +3,14 @@
3
3
  import asyncio
4
4
  import sys
5
5
  from pathlib import Path
6
- from typing import Optional
7
6
 
8
7
  import click
9
8
  from rich.console import Console
9
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeRemainingColumn
10
10
  from rich.table import Table
11
- from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn
12
- from rich import print as rprint
13
11
 
14
- from .core import TeleVault, UploadProgress, DownloadProgress
15
12
  from .config import Config, get_config_dir
16
-
13
+ from .core import DownloadProgress, TeleVault, UploadProgress
17
14
 
18
15
  console = Console()
19
16
 
@@ -32,6 +29,22 @@ def run_async(coro):
32
29
  return asyncio.get_event_loop().run_until_complete(coro)
33
30
 
34
31
 
32
+ async def check_auth(vault: TeleVault) -> bool:
33
+ """Check if user is authenticated. Returns True if authenticated, False otherwise."""
34
+ if not await vault.is_authenticated():
35
+ console.print("[red]Not logged in. Run 'televault login' first.[/red]")
36
+ return False
37
+ return True
38
+
39
+
40
+ async def check_channel(vault: TeleVault) -> bool:
41
+ """Check if channel is set. Returns True if set, False otherwise."""
42
+ if vault.config.channel_id is None:
43
+ console.print("[red]No storage channel configured. Run 'televault setup' first.[/red]")
44
+ return False
45
+ return True
46
+
47
+
35
48
  @click.group(invoke_without_command=True)
36
49
  @click.option("-h", "--help", is_flag=True, help="Show this message and exit.")
37
50
  @click.pass_context
@@ -43,27 +56,28 @@ def main(ctx, help):
43
56
 
44
57
  @main.command()
45
58
  @click.option("--phone", "-p", help="Phone number for login")
46
- def login(phone: Optional[str]):
59
+ def login(phone: str | None):
47
60
  """Login to Telegram."""
61
+
48
62
  async def _login():
49
63
  vault = TeleVault()
50
64
  await vault.connect(skip_channel=True) # Don't try to access channel yet
51
-
65
+
52
66
  console.print("[bold blue]TeleVault Login[/bold blue]")
53
67
  console.print("You'll receive a code on Telegram.\n")
54
-
55
- session = await vault.login(phone)
56
-
68
+
69
+ await vault.login(phone)
70
+
57
71
  console.print("\n[bold green]✓ Login successful![/bold green]")
58
72
  console.print(f"Session saved to: {get_config_dir() / 'telegram.json'}")
59
-
73
+
60
74
  # Now set up channel if configured
61
75
  if vault.config.channel_id:
62
76
  await vault.telegram.set_channel(vault.config.channel_id)
63
77
  console.print(f"Channel configured: {vault.config.channel_id}")
64
-
78
+
65
79
  await vault.disconnect()
66
-
80
+
67
81
  run_async(_login())
68
82
 
69
83
 
@@ -72,7 +86,7 @@ def logout():
72
86
  """Logout and clear session."""
73
87
  config_dir = get_config_dir()
74
88
  telegram_config = config_dir / "telegram.json"
75
-
89
+
76
90
  if telegram_config.exists():
77
91
  telegram_config.unlink()
78
92
  console.print("[green]✓ Logged out successfully[/green]")
@@ -82,22 +96,69 @@ def logout():
82
96
 
83
97
  @main.command()
84
98
  @click.option("--channel-id", "-c", type=int, help="Existing channel ID to use")
85
- def setup(channel_id: Optional[int]):
99
+ @click.option("--auto-create", is_flag=True, help="Auto-create a new channel without prompting")
100
+ def setup(channel_id: int | None, auto_create: bool):
86
101
  """Set up storage channel."""
102
+
87
103
  async def _setup():
88
104
  vault = TeleVault()
89
105
  await vault.connect()
90
-
106
+
107
+ # Check authentication first
108
+ if not await check_auth(vault):
109
+ await vault.disconnect()
110
+ return
111
+
112
+ # If channel_id is provided via CLI, use it
91
113
  if channel_id:
92
114
  cid = await vault.setup_channel(channel_id)
93
- console.print(f"[green]✓ Using channel: {cid}[/green]")
94
- else:
115
+ console.print(f"[green]✓ Using existing channel: {cid}[/green]")
116
+ console.print("[dim]Note: Make sure the bot is a member of this channel.[/dim]")
117
+ elif auto_create:
118
+ # Auto-create without prompting
95
119
  console.print("[bold]Creating new storage channel...[/bold]")
96
120
  cid = await vault.setup_channel()
97
- console.print(f"[green]✓ Created channel: {cid}[/green]")
98
-
121
+ console.print(f"[green]✓ Created new channel: {cid}[/green]")
122
+ else:
123
+ # Interactive mode - ask user what they want to do
124
+ console.print("[bold blue]TeleVault Storage Channel Setup[/bold blue]\n")
125
+ console.print("How would you like to set up your storage?")
126
+ console.print(" 1. Create a new private channel (recommended)")
127
+ console.print(" 2. Use an existing channel by ID")
128
+ console.print("")
129
+
130
+ choice = input("Enter your choice (1 or 2): ").strip()
131
+
132
+ if choice == "1":
133
+ console.print("\n[bold]Creating new storage channel...[/bold]")
134
+ cid = await vault.setup_channel()
135
+ console.print(f"[green]✓ Created new channel: {cid}[/green]")
136
+ elif choice == "2":
137
+ console.print("\n[bold]Using existing channel[/bold]")
138
+ console.print(
139
+ "[dim]Note: The channel ID should start with -100 (e.g., -1001234567890)[/dim]"
140
+ )
141
+
142
+ try:
143
+ existing_id = input("Enter channel ID: ").strip()
144
+ existing_id_int = int(existing_id)
145
+ cid = await vault.setup_channel(existing_id_int)
146
+ console.print(f"[green]✓ Using existing channel: {cid}[/green]")
147
+ except ValueError:
148
+ console.print("[red]✗ Invalid channel ID. Please provide a valid number.[/red]")
149
+ await vault.disconnect()
150
+ return
151
+ except Exception as e:
152
+ console.print(f"[red]✗ Error setting up channel: {e}[/red]")
153
+ await vault.disconnect()
154
+ return
155
+ else:
156
+ console.print("[red]✗ Invalid choice. Please enter 1 or 2.[/red]")
157
+ await vault.disconnect()
158
+ return
159
+
99
160
  await vault.disconnect()
100
-
161
+
101
162
  run_async(_setup())
102
163
 
103
164
 
@@ -107,43 +168,56 @@ def setup(channel_id: Optional[int]):
107
168
  @click.option("--no-compress", is_flag=True, help="Disable compression")
108
169
  @click.option("--no-encrypt", is_flag=True, help="Disable encryption")
109
170
  @click.option("--recursive", "-r", is_flag=True, help="Upload directory recursively")
110
- def push(file_path: str, password: Optional[str], no_compress: bool, no_encrypt: bool, recursive: bool):
171
+ def push(
172
+ file_path: str, password: str | None, no_compress: bool, no_encrypt: bool, recursive: bool
173
+ ):
111
174
  """Upload a file or directory to TeleVault."""
175
+
112
176
  async def _push():
113
177
  config = Config.load_or_create()
114
-
178
+
115
179
  if no_compress:
116
180
  config.compression = False
117
181
  if no_encrypt:
118
182
  config.encryption = False
119
-
183
+
120
184
  if config.encryption and not password:
121
185
  console.print("[yellow]Warning: Encryption enabled but no password provided.[/yellow]")
122
186
  console.print("Set password with --password or TELEVAULT_PASSWORD env var.")
123
187
  console.print("Use --no-encrypt to disable encryption.\n")
124
-
188
+
125
189
  vault = TeleVault(config=config, password=password)
126
190
  await vault.connect()
127
-
191
+
192
+ if not await check_auth(vault):
193
+ await vault.disconnect()
194
+ return
195
+
196
+ if not await check_channel(vault):
197
+ await vault.disconnect()
198
+ return
199
+
128
200
  file_path_obj = Path(file_path)
129
-
201
+
130
202
  # Handle directory upload
131
203
  if file_path_obj.is_dir():
132
204
  if not recursive:
133
- console.print(f"[red]'{file_path}' is a directory. Use --recursive (-r) to upload.[/red]")
205
+ console.print(
206
+ f"[red]'{file_path}' is a directory. Use --recursive (-r) to upload.[/red]"
207
+ )
134
208
  await vault.disconnect()
135
209
  return
136
-
210
+
137
211
  files = list(file_path_obj.rglob("*"))
138
212
  files = [f for f in files if f.is_file()]
139
-
213
+
140
214
  if not files:
141
215
  console.print("[yellow]No files found in directory.[/yellow]")
142
216
  await vault.disconnect()
143
217
  return
144
-
218
+
145
219
  console.print(f"[bold]Uploading {len(files)} files from {file_path_obj.name}/[/bold]\n")
146
-
220
+
147
221
  for i, f in enumerate(files, 1):
148
222
  rel_path = f.relative_to(file_path_obj)
149
223
  console.print(f"[{i}/{len(files)}] {rel_path}...", end=" ")
@@ -152,12 +226,12 @@ def push(file_path: str, password: Optional[str], no_compress: bool, no_encrypt:
152
226
  console.print(f"[green]✓[/green] ({format_size(metadata.size)})")
153
227
  except Exception as e:
154
228
  console.print(f"[red]✗ {e}[/red]")
155
-
229
+
156
230
  console.print(f"\n[bold green]✓ Uploaded {len(files)} files[/bold green]")
157
231
  else:
158
232
  # Single file upload with progress
159
233
  file_size = file_path_obj.stat().st_size
160
-
234
+
161
235
  with Progress(
162
236
  SpinnerColumn(),
163
237
  TextColumn("[progress.description]{task.description}"),
@@ -169,26 +243,24 @@ def push(file_path: str, password: Optional[str], no_compress: bool, no_encrypt:
169
243
  refresh_per_second=10,
170
244
  ) as progress:
171
245
  task = progress.add_task(
172
- f"Uploading {file_path_obj.name}",
173
- total=100,
174
- size=format_size(file_size)
246
+ f"Uploading {file_path_obj.name}", total=100, size=format_size(file_size)
175
247
  )
176
-
248
+
177
249
  def on_progress(p: UploadProgress):
178
250
  progress.update(task, completed=p.percent)
179
-
251
+
180
252
  metadata = await vault.upload(file_path, progress_callback=on_progress)
181
253
  progress.update(task, completed=100) # Ensure 100% at end
182
-
183
- console.print(f"\n[bold green]✓ Uploaded successfully![/bold green]")
254
+
255
+ console.print("\n[bold green]✓ Uploaded successfully![/bold green]")
184
256
  console.print(f" File ID: {metadata.id}")
185
257
  console.print(f" Size: {format_size(metadata.size)}")
186
258
  console.print(f" Chunks: {metadata.chunk_count}")
187
259
  console.print(f" Encrypted: {'Yes' if metadata.encrypted else 'No'}")
188
260
  console.print(f" Compressed: {'Yes' if metadata.compressed else 'No'}")
189
-
261
+
190
262
  await vault.disconnect()
191
-
263
+
192
264
  run_async(_push())
193
265
 
194
266
 
@@ -196,12 +268,21 @@ def push(file_path: str, password: Optional[str], no_compress: bool, no_encrypt:
196
268
  @click.argument("file_id_or_name")
197
269
  @click.option("--output", "-o", type=click.Path(), help="Output path")
198
270
  @click.option("--password", "-p", help="Decryption password", envvar="TELEVAULT_PASSWORD")
199
- def pull(file_id_or_name: str, output: Optional[str], password: Optional[str]):
271
+ def pull(file_id_or_name: str, output: str | None, password: str | None):
200
272
  """Download a file from TeleVault."""
273
+
201
274
  async def _pull():
202
275
  vault = TeleVault(password=password)
203
276
  await vault.connect()
204
-
277
+
278
+ if not await check_auth(vault):
279
+ await vault.disconnect()
280
+ return
281
+
282
+ if not await check_channel(vault):
283
+ await vault.disconnect()
284
+ return
285
+
205
286
  with Progress(
206
287
  SpinnerColumn(),
207
288
  TextColumn("[progress.description]{task.description}"),
@@ -212,10 +293,10 @@ def pull(file_id_or_name: str, output: Optional[str], password: Optional[str]):
212
293
  refresh_per_second=10,
213
294
  ) as progress:
214
295
  task = progress.add_task(f"Downloading {file_id_or_name}", total=100)
215
-
296
+
216
297
  def on_progress(p: DownloadProgress):
217
298
  progress.update(task, completed=p.percent)
218
-
299
+
219
300
  try:
220
301
  output_path = await vault.download(
221
302
  file_id_or_name,
@@ -231,11 +312,11 @@ def pull(file_id_or_name: str, output: Optional[str], password: Optional[str]):
231
312
  console.print(f"[red]✗ Error: {e}[/red]")
232
313
  await vault.disconnect()
233
314
  sys.exit(1)
234
-
315
+
235
316
  console.print(f"\n[bold green]✓ Downloaded to: {output_path}[/bold green]")
236
-
317
+
237
318
  await vault.disconnect()
238
-
319
+
239
320
  run_async(_pull())
240
321
 
241
322
 
@@ -244,12 +325,21 @@ def pull(file_id_or_name: str, output: Optional[str], password: Optional[str]):
244
325
  @click.option("--sort", type=click.Choice(["name", "size", "date"]), default="name")
245
326
  def list_files(as_json: bool, sort: str):
246
327
  """List all files in the vault."""
328
+
247
329
  async def _list():
248
330
  vault = TeleVault()
249
331
  await vault.connect()
250
-
332
+
333
+ if not await check_auth(vault):
334
+ await vault.disconnect()
335
+ return
336
+
337
+ if not await check_channel(vault):
338
+ await vault.disconnect()
339
+ return
340
+
251
341
  files = await vault.list_files()
252
-
342
+
253
343
  # Sort
254
344
  if sort == "name":
255
345
  files.sort(key=lambda f: f.name.lower())
@@ -257,9 +347,10 @@ def list_files(as_json: bool, sort: str):
257
347
  files.sort(key=lambda f: f.size, reverse=True)
258
348
  elif sort == "date":
259
349
  files.sort(key=lambda f: f.created_at, reverse=True)
260
-
350
+
261
351
  if as_json:
262
352
  import json
353
+
263
354
  output = [{"id": f.id, "name": f.name, "size": f.size} for f in files]
264
355
  click.echo(json.dumps(output, indent=2))
265
356
  else:
@@ -272,7 +363,7 @@ def list_files(as_json: bool, sort: str):
272
363
  table.add_column("Size", justify="right")
273
364
  table.add_column("Chunks", justify="right")
274
365
  table.add_column("Encrypted")
275
-
366
+
276
367
  for f in files:
277
368
  table.add_row(
278
369
  f.id[:8],
@@ -281,12 +372,13 @@ def list_files(as_json: bool, sort: str):
281
372
  str(f.chunk_count),
282
373
  "🔒" if f.encrypted else "📄",
283
374
  )
284
-
375
+
285
376
  console.print(table)
286
- console.print(f"\n[dim]{len(files)} file(s), {format_size(sum(f.size for f in files))} total[/dim]")
287
-
377
+ total_size = format_size(sum(f.size for f in files))
378
+ console.print(f"\n[dim]{len(files)} file(s), {total_size} total[/dim]")
379
+
288
380
  await vault.disconnect()
289
-
381
+
290
382
  run_async(_list())
291
383
 
292
384
 
@@ -294,20 +386,29 @@ def list_files(as_json: bool, sort: str):
294
386
  @click.argument("query")
295
387
  def search(query: str):
296
388
  """Search files by name."""
389
+
297
390
  async def _search():
298
391
  vault = TeleVault()
299
392
  await vault.connect()
300
-
393
+
394
+ if not await check_auth(vault):
395
+ await vault.disconnect()
396
+ return
397
+
398
+ if not await check_channel(vault):
399
+ await vault.disconnect()
400
+ return
401
+
301
402
  files = await vault.search(query)
302
-
403
+
303
404
  if not files:
304
405
  console.print(f"[dim]No files matching '{query}'[/dim]")
305
406
  else:
306
407
  for f in files:
307
408
  console.print(f"[cyan]{f.id[:8]}[/cyan] {f.name} ({format_size(f.size)})")
308
-
409
+
309
410
  await vault.disconnect()
310
-
411
+
311
412
  run_async(_search())
312
413
 
313
414
 
@@ -315,10 +416,19 @@ def search(query: str):
315
416
  @click.argument("file_id_or_name")
316
417
  def info(file_id_or_name: str):
317
418
  """Show detailed file information."""
419
+
318
420
  async def _info():
319
421
  vault = TeleVault()
320
422
  await vault.connect()
321
-
423
+
424
+ if not await check_auth(vault):
425
+ await vault.disconnect()
426
+ return
427
+
428
+ if not await check_channel(vault):
429
+ await vault.disconnect()
430
+ return
431
+
322
432
  try:
323
433
  # Find file
324
434
  files = await vault.search(file_id_or_name)
@@ -330,14 +440,14 @@ def info(file_id_or_name: str):
330
440
  metadata = await vault.telegram.get_metadata(msg_id)
331
441
  files = [metadata]
332
442
  break
333
-
443
+
334
444
  if not files:
335
445
  console.print(f"[red]File not found: {file_id_or_name}[/red]")
336
446
  await vault.disconnect()
337
447
  return
338
-
448
+
339
449
  f = files[0]
340
-
450
+
341
451
  console.print(f"[bold]{f.name}[/bold]\n")
342
452
  console.print(f" ID: {f.id}")
343
453
  console.print(f" Size: {format_size(f.size)}")
@@ -349,20 +459,21 @@ def info(file_id_or_name: str):
349
459
  console.print(f" Comp. ratio: {f.compression_ratio:.1%}")
350
460
  if f.mime_type:
351
461
  console.print(f" MIME type: {f.mime_type}")
352
-
462
+
353
463
  from datetime import datetime
464
+
354
465
  created = datetime.fromtimestamp(f.created_at)
355
466
  console.print(f" Created: {created.strftime('%Y-%m-%d %H:%M')}")
356
-
467
+
357
468
  if f.chunks:
358
469
  stored = sum(c.size for c in f.chunks)
359
470
  console.print(f" Stored size: {format_size(stored)}")
360
-
471
+
361
472
  except Exception as e:
362
473
  console.print(f"[red]Error: {e}[/red]")
363
-
474
+
364
475
  await vault.disconnect()
365
-
476
+
366
477
  run_async(_info())
367
478
 
368
479
 
@@ -371,38 +482,55 @@ def info(file_id_or_name: str):
371
482
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
372
483
  def rm(file_id_or_name: str, yes: bool):
373
484
  """Delete a file from the vault."""
485
+
374
486
  async def _rm():
375
487
  vault = TeleVault()
376
488
  await vault.connect()
377
-
378
- if not yes:
379
- if not click.confirm(f"Delete '{file_id_or_name}'?"):
380
- console.print("[dim]Cancelled[/dim]")
381
- await vault.disconnect()
382
- return
383
-
489
+
490
+ if not await check_auth(vault):
491
+ await vault.disconnect()
492
+ return
493
+
494
+ if not await check_channel(vault):
495
+ await vault.disconnect()
496
+ return
497
+
498
+ if not yes and not click.confirm(f"Delete '{file_id_or_name}'?"):
499
+ console.print("[dim]Cancelled[/dim]")
500
+ await vault.disconnect()
501
+ return
502
+
384
503
  deleted = await vault.delete(file_id_or_name)
385
-
504
+
386
505
  if deleted:
387
506
  console.print(f"[green]✓ Deleted: {file_id_or_name}[/green]")
388
507
  else:
389
508
  console.print(f"[red]✗ File not found: {file_id_or_name}[/red]")
390
-
509
+
391
510
  await vault.disconnect()
392
-
511
+
393
512
  run_async(_rm())
394
513
 
395
514
 
396
515
  @main.command()
397
516
  def status():
398
517
  """Show vault status."""
518
+
399
519
  async def _status():
400
520
  vault = TeleVault()
401
521
  await vault.connect()
402
-
522
+
523
+ if not await check_auth(vault):
524
+ await vault.disconnect()
525
+ return
526
+
527
+ if not await check_channel(vault):
528
+ await vault.disconnect()
529
+ return
530
+
403
531
  try:
404
532
  status = await vault.get_status()
405
-
533
+
406
534
  console.print("[bold]TeleVault Status[/bold]\n")
407
535
  console.print(f" Channel: {status['channel_id']}")
408
536
  console.print(f" Files: {status['file_count']}")
@@ -412,34 +540,53 @@ def status():
412
540
  except Exception as e:
413
541
  console.print(f"[red]Error: {e}[/red]")
414
542
  console.print("\n[dim]Have you run 'televault login' and 'televault setup'?[/dim]")
415
-
543
+
416
544
  await vault.disconnect()
417
-
545
+
418
546
  run_async(_status())
419
547
 
420
548
 
421
549
  @main.command()
422
550
  def whoami():
423
551
  """Show current Telegram account."""
552
+
424
553
  async def _whoami():
425
554
  vault = TeleVault()
426
555
  await vault.connect()
427
-
556
+
557
+ if not await vault.telegram._client.is_user_authorized():
558
+ console.print("[red]Not logged in. Run 'televault login' first.[/red]")
559
+ await vault.disconnect()
560
+ return
561
+
428
562
  me = await vault.telegram._client.get_me()
429
-
563
+
564
+ if me is None:
565
+ console.print("[red]Not logged in. Run 'televault login' first.[/red]")
566
+ await vault.disconnect()
567
+ return
568
+
430
569
  console.print(f"[bold]{me.first_name}[/bold]", end="")
431
570
  if me.last_name:
432
571
  console.print(f" {me.last_name}", end="")
433
572
  console.print()
434
-
573
+
435
574
  if me.username:
436
575
  console.print(f" @{me.username}")
437
576
  console.print(f" ID: {me.id}")
438
-
577
+
439
578
  await vault.disconnect()
440
-
579
+
441
580
  run_async(_whoami())
442
581
 
443
582
 
583
+ @main.command()
584
+ def tui():
585
+ """Launch the interactive TUI."""
586
+ from .tui import run_tui
587
+
588
+ run_tui()
589
+
590
+
444
591
  if __name__ == "__main__":
445
592
  main()