aniate 1.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.
- aniate/__init__.py +10 -0
- aniate/auth.py +96 -0
- aniate/brew.py +73 -0
- aniate/brews/__init__.py +12 -0
- aniate/brews/document_storage/__init__.py +6 -0
- aniate/brews/document_storage/fetch.py +143 -0
- aniate/brews/document_storage/files.py +65 -0
- aniate/brews/document_storage/save.py +112 -0
- aniate/brews/maker.py +255 -0
- aniate/brews/secrets/__init__.py +4 -0
- aniate/brews/secrets/secrets.py +131 -0
- aniate/brews/tools/__init__.py +9 -0
- aniate/brews/tools/fix.py +186 -0
- aniate/brews/tools/review.py +141 -0
- aniate/brews/tools/shell.py +143 -0
- aniate/brews/tools/what.py +126 -0
- aniate/cli.py +146 -0
- aniate/commands/__init__.py +12 -0
- aniate/commands/chat.py +95 -0
- aniate/commands/core.py +53 -0
- aniate/commands/net.py +83 -0
- aniate/config.py +28 -0
- aniate/custom/__init__.py +5 -0
- aniate/custom/hello.py +9 -0
- aniate/custom/timer.py +41 -0
- aniate/engine.py +126 -0
- aniate/game/__init__.py +3 -0
- aniate/game/guess.py +48 -0
- aniate/utils.py +33 -0
- aniate-1.0.0.dist-info/METADATA +157 -0
- aniate-1.0.0.dist-info/RECORD +33 -0
- aniate-1.0.0.dist-info/WHEEL +4 -0
- aniate-1.0.0.dist-info/entry_points.txt +2 -0
aniate/__init__.py
ADDED
aniate/auth.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import getpass
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.prompt import Prompt
|
|
7
|
+
from supabase import create_client, Client
|
|
8
|
+
from aniate.config import SUPABASE_URL, SUPABASE_KEY, SESSION_FILE, CONFIG_DIR
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
12
|
+
|
|
13
|
+
def get_session():
|
|
14
|
+
"""Load session from file."""
|
|
15
|
+
if not SESSION_FILE.exists():
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
with open(SESSION_FILE, 'r') as f:
|
|
19
|
+
return json.load(f)
|
|
20
|
+
except:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
def save_session(data):
|
|
24
|
+
"""Save session to file."""
|
|
25
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
with open(SESSION_FILE, 'w') as f:
|
|
27
|
+
json.dump(data, f)
|
|
28
|
+
|
|
29
|
+
def signup():
|
|
30
|
+
"""Create a new account."""
|
|
31
|
+
session = get_session()
|
|
32
|
+
if session:
|
|
33
|
+
console.print("[yellow]Already logged in. Run: ant logout[/yellow]")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
console.print("\n[bold white]ANIATE[/bold white] [dim]Create Account[/dim]\n")
|
|
37
|
+
email = Prompt.ask("[white]Email[/white]")
|
|
38
|
+
while True:
|
|
39
|
+
password = getpass.getpass("Password (8+ chars, 1 number): ")
|
|
40
|
+
if len(password) < 8 or not re.search(r"\d", password):
|
|
41
|
+
console.print("[red]Password too weak. Try again.[/red]")
|
|
42
|
+
continue
|
|
43
|
+
break
|
|
44
|
+
with console.status("[dim]Creating account...[/dim]", spinner="dots"):
|
|
45
|
+
try:
|
|
46
|
+
res = supabase.auth.sign_up({"email": email, "password": password})
|
|
47
|
+
if res.user:
|
|
48
|
+
console.print(f"\n[green]Account created.[/green] {email}")
|
|
49
|
+
console.print("[dim]Check your email to verify.\n[/dim]")
|
|
50
|
+
else:
|
|
51
|
+
console.print("[yellow]Account created. Check email for verification.[/yellow]")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
console.print(f"[red]Failed:[/red] {str(e)}")
|
|
54
|
+
|
|
55
|
+
def login():
|
|
56
|
+
"""Login via Email/Password."""
|
|
57
|
+
session = get_session()
|
|
58
|
+
if session:
|
|
59
|
+
console.print(f"[dim]Already authenticated as[/dim] {session['email']}")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
console.print("\n[bold white]ANIATE[/bold white] [dim]Login[/dim]\n")
|
|
63
|
+
email = Prompt.ask("[white]Email[/white]")
|
|
64
|
+
password = getpass.getpass("Password: ")
|
|
65
|
+
|
|
66
|
+
with console.status("[dim]Authenticating...[/dim]", spinner="dots"):
|
|
67
|
+
try:
|
|
68
|
+
res = supabase.auth.sign_in_with_password({"email": email, "password": password})
|
|
69
|
+
save_session({
|
|
70
|
+
"access_token": res.session.access_token,
|
|
71
|
+
"refresh_token": res.session.refresh_token,
|
|
72
|
+
"user_id": res.user.id,
|
|
73
|
+
"email": res.user.email
|
|
74
|
+
})
|
|
75
|
+
console.print(f"\n[green]Authenticated.[/green] {res.user.email}\n")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
console.print(f"[red]Authentication failed.[/red] Check credentials.")
|
|
78
|
+
|
|
79
|
+
def logout():
|
|
80
|
+
"""Logout and clear session."""
|
|
81
|
+
if not SESSION_FILE.exists():
|
|
82
|
+
console.print("[dim]No active session.[/dim]")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
os.remove(SESSION_FILE)
|
|
86
|
+
console.print("[dim]Session cleared.[/dim]")
|
|
87
|
+
|
|
88
|
+
def whoami():
|
|
89
|
+
"""Show current logged in user."""
|
|
90
|
+
session = get_session()
|
|
91
|
+
if not session:
|
|
92
|
+
console.print("[dim]No active session. Run: ant login[/dim]")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
console.print(f"\n[white]{session['email']}[/white]")
|
|
96
|
+
console.print(f"[dim]{session['user_id']}[/dim]\n")
|
aniate/brew.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brew System - Create and manage custom commands
|
|
3
|
+
"""
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
def brew_cmd(name: str):
|
|
10
|
+
"""Brew a custom command template."""
|
|
11
|
+
console.print(f"\n[bold cyan]🍺 Brewing Command: {name}[/bold cyan]\n")
|
|
12
|
+
|
|
13
|
+
commands_dir = Path(__file__).parent / "commands"
|
|
14
|
+
cmd_file = commands_dir / f"{name}.py"
|
|
15
|
+
|
|
16
|
+
if cmd_file.exists():
|
|
17
|
+
console.print(f"[yellow]Command '{name}' already exists.[/yellow]")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
# Template for custom commands
|
|
21
|
+
template = f'''"""
|
|
22
|
+
{name.title()} Command - Custom brewed command
|
|
23
|
+
"""
|
|
24
|
+
import requests
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
import sys
|
|
27
|
+
import os
|
|
28
|
+
# Package imports
|
|
29
|
+
|
|
30
|
+
from aniate.config import SERVER_URL
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
def {name}_execute(arg: str):
|
|
35
|
+
"""Execute the {name} command."""
|
|
36
|
+
console.print(f"[cyan]{name.title()} command executing:[/cyan] {{arg}}")
|
|
37
|
+
|
|
38
|
+
# TODO: Implement your command logic here
|
|
39
|
+
# You can use the LLM via SERVER_URL
|
|
40
|
+
# You can access other APIs
|
|
41
|
+
# You can process files, data, etc.
|
|
42
|
+
|
|
43
|
+
console.print(f"[green]✓ {name.title()} completed[/green]")
|
|
44
|
+
'''
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
cmd_file.write_text(template)
|
|
48
|
+
console.print(f"[green]✓ Command '{name}' brewed successfully[/green]")
|
|
49
|
+
console.print(f"[dim]Edit: {cmd_file}[/dim]")
|
|
50
|
+
console.print(f"[dim]Use: ant {name} \"your input\"[/dim]\n")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
console.print(f"[red]Brew failed:[/red] {e}")
|
|
53
|
+
|
|
54
|
+
def delete_cmd(name: str):
|
|
55
|
+
"""Delete a custom brewed command."""
|
|
56
|
+
commands_dir = Path(__file__).parent / "commands"
|
|
57
|
+
cmd_file = commands_dir / f"{name}.py"
|
|
58
|
+
|
|
59
|
+
if not cmd_file.exists():
|
|
60
|
+
console.print(f"[yellow]Command '{name}' not found.[/yellow]")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Prevent deleting core commands
|
|
64
|
+
core_commands = ['chat', 'net']
|
|
65
|
+
if name in core_commands:
|
|
66
|
+
console.print(f"[red]Cannot delete core command '{name}'[/red]")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
cmd_file.unlink()
|
|
71
|
+
console.print(f"[bold red]✓ Command '{name}' deleted[/bold red]")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
console.print(f"[red]Delete failed:[/red] {e}")
|
aniate/brews/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global Brews - Cloud-enabled commands accessible from any terminal
|
|
3
|
+
"""
|
|
4
|
+
from .document_storage import save_file, fetch_file, list_files
|
|
5
|
+
from .secrets import add_secret, get_secret, list_secrets, delete_secret
|
|
6
|
+
from .maker import brew_make
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'save_file', 'fetch_file', 'list_files',
|
|
10
|
+
'add_secret', 'get_secret', 'list_secrets', 'delete_secret',
|
|
11
|
+
'brew_make'
|
|
12
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fetch Command - Download files from cloud storage
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
10
|
+
from rich.prompt import Prompt
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from supabase import create_client
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
# Package imports
|
|
16
|
+
|
|
17
|
+
from aniate.config import SUPABASE_URL, SUPABASE_KEY
|
|
18
|
+
from aniate.auth import get_session
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
def fetch_file(filename: Optional[str] = typer.Argument(None, help="File to download")):
|
|
23
|
+
"""Download a file from cloud storage."""
|
|
24
|
+
session = get_session()
|
|
25
|
+
if not session:
|
|
26
|
+
console.print("[red]Login required. Run: ant login[/red]")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
# Create Supabase client
|
|
30
|
+
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
31
|
+
supabase.auth.set_session(session['access_token'], session.get('refresh_token', ''))
|
|
32
|
+
|
|
33
|
+
user_id = session['user_id']
|
|
34
|
+
|
|
35
|
+
# If no filename provided, show interactive list
|
|
36
|
+
if not filename:
|
|
37
|
+
filename = _select_file_interactive(supabase, user_id)
|
|
38
|
+
if not filename:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Get file metadata
|
|
42
|
+
try:
|
|
43
|
+
result = supabase.table("user_files").select("*").eq("user_id", user_id).eq("filename", filename).execute()
|
|
44
|
+
|
|
45
|
+
if not result.data:
|
|
46
|
+
console.print(f"[red]File not found in cloud:[/red] {filename}")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
file_meta = result.data[0]
|
|
50
|
+
storage_path = file_meta['storage_path']
|
|
51
|
+
file_size = file_meta['file_size']
|
|
52
|
+
|
|
53
|
+
# Always save to Desktop
|
|
54
|
+
desktop = Path.home() / "Desktop"
|
|
55
|
+
dest_path = desktop / filename
|
|
56
|
+
|
|
57
|
+
# Check if file exists locally
|
|
58
|
+
if dest_path.exists():
|
|
59
|
+
overwrite = Prompt.ask(f"[yellow]File exists. Overwrite?[/yellow]", choices=["y", "n"], default="n")
|
|
60
|
+
if overwrite == 'n':
|
|
61
|
+
console.print("[dim]Download cancelled[/dim]")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Download file
|
|
65
|
+
with Progress(
|
|
66
|
+
SpinnerColumn(),
|
|
67
|
+
TextColumn("[cyan]{task.description}"),
|
|
68
|
+
BarColumn(),
|
|
69
|
+
TaskProgressColumn(),
|
|
70
|
+
console=console
|
|
71
|
+
) as progress:
|
|
72
|
+
task = progress.add_task(f"Downloading {filename}...", total=100)
|
|
73
|
+
|
|
74
|
+
# Download from Supabase Storage
|
|
75
|
+
file_data = supabase.storage.from_('user-files').download(storage_path)
|
|
76
|
+
|
|
77
|
+
progress.update(task, advance=70)
|
|
78
|
+
|
|
79
|
+
# Write to local file
|
|
80
|
+
with open(dest_path, 'wb') as f:
|
|
81
|
+
f.write(file_data)
|
|
82
|
+
|
|
83
|
+
progress.update(task, advance=30)
|
|
84
|
+
|
|
85
|
+
# Success message
|
|
86
|
+
size_str = _format_size(file_size)
|
|
87
|
+
console.print(f"[bold green]✓ Downloaded {filename}[/bold green] [dim]({size_str})[/dim]")
|
|
88
|
+
console.print(f"[dim]Saved to: {dest_path}[/dim]")
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
console.print(f"[red]Download failed:[/red] {e}")
|
|
92
|
+
|
|
93
|
+
def _select_file_interactive(supabase, user_id):
|
|
94
|
+
"""Show interactive file selector."""
|
|
95
|
+
try:
|
|
96
|
+
result = supabase.table("user_files").select("*").eq("user_id", user_id).order("uploaded_at", desc=True).execute()
|
|
97
|
+
|
|
98
|
+
if not result.data:
|
|
99
|
+
console.print("[dim]No files in cloud storage[/dim]")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Show table
|
|
103
|
+
table = Table(title="Cloud Files")
|
|
104
|
+
table.add_column("#", style="cyan")
|
|
105
|
+
table.add_column("Filename", style="white")
|
|
106
|
+
table.add_column("Size", style="dim")
|
|
107
|
+
table.add_column("Date", style="dim")
|
|
108
|
+
|
|
109
|
+
for idx, file in enumerate(result.data, 1):
|
|
110
|
+
size = _format_size(file['file_size'])
|
|
111
|
+
date = file['uploaded_at'][:10] # Just the date
|
|
112
|
+
table.add_row(str(idx), file['filename'], size, date)
|
|
113
|
+
|
|
114
|
+
console.print(table)
|
|
115
|
+
|
|
116
|
+
# Prompt for selection
|
|
117
|
+
choice = Prompt.ask("\n[cyan]Select file number (or 'q' to quit)[/cyan]")
|
|
118
|
+
|
|
119
|
+
if choice.lower() == 'q':
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
idx = int(choice) - 1
|
|
124
|
+
if 0 <= idx < len(result.data):
|
|
125
|
+
return result.data[idx]['filename']
|
|
126
|
+
else:
|
|
127
|
+
console.print("[red]Invalid selection[/red]")
|
|
128
|
+
return None
|
|
129
|
+
except ValueError:
|
|
130
|
+
console.print("[red]Invalid input[/red]")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
console.print(f"[red]Error listing files:[/red] {e}")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def _format_size(bytes_size):
|
|
138
|
+
"""Format bytes to human-readable size."""
|
|
139
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
140
|
+
if bytes_size < 1024.0:
|
|
141
|
+
return f"{bytes_size:.1f} {unit}"
|
|
142
|
+
bytes_size /= 1024.0
|
|
143
|
+
return f"{bytes_size:.1f} TB"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Files Command - List all files in cloud storage
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from supabase import create_client
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
# Package imports
|
|
11
|
+
|
|
12
|
+
from aniate.config import SUPABASE_URL, SUPABASE_KEY
|
|
13
|
+
from aniate.auth import get_session
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
def list_files():
|
|
18
|
+
"""List all files in cloud storage."""
|
|
19
|
+
session = get_session()
|
|
20
|
+
if not session:
|
|
21
|
+
console.print("[red]Login required. Run: ant login[/red]")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
# Create Supabase client
|
|
25
|
+
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
26
|
+
supabase.auth.set_session(session['access_token'], session.get('refresh_token', ''))
|
|
27
|
+
|
|
28
|
+
user_id = session['user_id']
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
result = supabase.table("user_files").select("*").eq("user_id", user_id).order("uploaded_at", desc=True).execute()
|
|
32
|
+
|
|
33
|
+
if not result.data:
|
|
34
|
+
console.print("[dim]No files in cloud storage[/dim]")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
# Calculate totals
|
|
38
|
+
total_size = sum(f['file_size'] for f in result.data)
|
|
39
|
+
total_count = len(result.data)
|
|
40
|
+
|
|
41
|
+
# Create table
|
|
42
|
+
table = Table(title=f"Cloud Files ({total_count} files, {_format_size(total_size)} total)")
|
|
43
|
+
table.add_column("Filename", style="cyan")
|
|
44
|
+
table.add_column("Size", style="white")
|
|
45
|
+
table.add_column("Type", style="dim")
|
|
46
|
+
table.add_column("Uploaded", style="dim")
|
|
47
|
+
|
|
48
|
+
for file in result.data:
|
|
49
|
+
size = _format_size(file['file_size'])
|
|
50
|
+
mime = file['mime_type'].split('/')[-1] if file['mime_type'] else 'unknown'
|
|
51
|
+
date = file['uploaded_at'][:10] # Just the date
|
|
52
|
+
table.add_row(file['filename'], size, mime, date)
|
|
53
|
+
|
|
54
|
+
console.print(table)
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
console.print(f"[red]Error listing files:[/red] {e}")
|
|
58
|
+
|
|
59
|
+
def _format_size(bytes_size):
|
|
60
|
+
"""Format bytes to human-readable size."""
|
|
61
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
62
|
+
if bytes_size < 1024.0:
|
|
63
|
+
return f"{bytes_size:.1f} {unit}"
|
|
64
|
+
bytes_size /= 1024.0
|
|
65
|
+
return f"{bytes_size:.1f} TB"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Save Command - Upload files to cloud storage
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
10
|
+
from supabase import create_client
|
|
11
|
+
import mimetypes
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
# Package imports
|
|
15
|
+
|
|
16
|
+
from aniate.config import SUPABASE_URL, SUPABASE_KEY
|
|
17
|
+
from aniate.auth import get_session
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
def save_file(filepath: str = typer.Argument(..., help="File to upload"), cloud_name: Optional[str] = typer.Argument(None, help="Custom name in cloud")):
|
|
22
|
+
"""Upload a file to cloud storage."""
|
|
23
|
+
session = get_session()
|
|
24
|
+
if not session:
|
|
25
|
+
console.print("[red]Login required. Run: ant login[/red]")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
# Resolve file path
|
|
29
|
+
file_path = Path(filepath).expanduser().resolve()
|
|
30
|
+
|
|
31
|
+
if not file_path.exists():
|
|
32
|
+
console.print(f"[red]File not found:[/red] {filepath}")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if not file_path.is_file():
|
|
36
|
+
console.print(f"[red]Not a file:[/red] {filepath}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Get file info
|
|
40
|
+
file_size = file_path.stat().st_size
|
|
41
|
+
filename = cloud_name if cloud_name else file_path.name
|
|
42
|
+
mime_type = mimetypes.guess_type(str(file_path))[0] or 'application/octet-stream'
|
|
43
|
+
|
|
44
|
+
# Size warning for large files
|
|
45
|
+
if file_size > 10 * 1024 * 1024: # 10MB
|
|
46
|
+
size_mb = file_size / (1024 * 1024)
|
|
47
|
+
console.print(f"[yellow]Warning: Large file ({size_mb:.1f} MB)[/yellow]")
|
|
48
|
+
confirm = console.input("Continue? (y/n): ")
|
|
49
|
+
if confirm.lower() != 'y':
|
|
50
|
+
console.print("[dim]Upload cancelled[/dim]")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Create Supabase client
|
|
54
|
+
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
55
|
+
supabase.auth.set_session(session['access_token'], session.get('refresh_token', ''))
|
|
56
|
+
|
|
57
|
+
user_id = session['user_id']
|
|
58
|
+
storage_path = f"{user_id}/{filename}"
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
with Progress(
|
|
62
|
+
SpinnerColumn(),
|
|
63
|
+
TextColumn("[cyan]{task.description}"),
|
|
64
|
+
BarColumn(),
|
|
65
|
+
TaskProgressColumn(),
|
|
66
|
+
console=console
|
|
67
|
+
) as progress:
|
|
68
|
+
task = progress.add_task(f"Uploading {filename}...", total=100)
|
|
69
|
+
|
|
70
|
+
# Read file
|
|
71
|
+
with open(file_path, 'rb') as f:
|
|
72
|
+
file_data = f.read()
|
|
73
|
+
|
|
74
|
+
progress.update(task, advance=30)
|
|
75
|
+
|
|
76
|
+
# Upload to Supabase Storage
|
|
77
|
+
supabase.storage.from_('user-files').upload(
|
|
78
|
+
storage_path,
|
|
79
|
+
file_data,
|
|
80
|
+
file_options={"content-type": mime_type, "upsert": "true"}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
progress.update(task, advance=40)
|
|
84
|
+
|
|
85
|
+
# Save metadata to database
|
|
86
|
+
metadata = {
|
|
87
|
+
"user_id": user_id,
|
|
88
|
+
"filename": filename,
|
|
89
|
+
"storage_path": storage_path,
|
|
90
|
+
"file_size": file_size,
|
|
91
|
+
"mime_type": mime_type
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Upsert (insert or update)
|
|
95
|
+
supabase.table("user_files").upsert(metadata, on_conflict="user_id,filename").execute()
|
|
96
|
+
|
|
97
|
+
progress.update(task, advance=30)
|
|
98
|
+
|
|
99
|
+
# Success message
|
|
100
|
+
size_str = _format_size(file_size)
|
|
101
|
+
console.print(f"[bold green]✓ Uploaded {filename}[/bold green] [dim]({size_str})[/dim]")
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
console.print(f"[red]Upload failed:[/red] {e}")
|
|
105
|
+
|
|
106
|
+
def _format_size(bytes_size):
|
|
107
|
+
"""Format bytes to human-readable size."""
|
|
108
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
109
|
+
if bytes_size < 1024.0:
|
|
110
|
+
return f"{bytes_size:.1f} {unit}"
|
|
111
|
+
bytes_size /= 1024.0
|
|
112
|
+
return f"{bytes_size:.1f} TB"
|