diff --git a/db.py b/db.py index a6d51d5..1a0bc43 100644 --- a/db.py +++ b/db.py @@ -118,17 +118,18 @@ CREATE TABLE IF NOT EXISTS revoked_tokens ( ); -- User storage providers (IPFS pinning services, local storage, etc.) +-- Users can have multiple configs of the same provider type CREATE TABLE IF NOT EXISTS user_storage ( id SERIAL PRIMARY KEY, username VARCHAR(255) NOT NULL REFERENCES users(username), - provider_type VARCHAR(50) NOT NULL, -- 'pinata', 'web3storage', 'filebase', 'local' + provider_type VARCHAR(50) NOT NULL, -- 'pinata', 'web3storage', 'nftstorage', 'infura', 'filebase', 'storj', 'local' provider_name VARCHAR(255), -- User-friendly name + description TEXT, -- User description to distinguish configs config JSONB NOT NULL DEFAULT '{}', -- API keys, endpoints, paths capacity_gb INTEGER NOT NULL, -- Total capacity user is contributing is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(username, provider_type, provider_name) + updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Track what's stored where @@ -818,20 +819,33 @@ async def get_user_storage(username: str) -> list[dict]: """Get all storage providers for a user.""" async with get_connection() as conn: rows = await conn.fetch( - """SELECT id, username, provider_type, provider_name, config, + """SELECT id, username, provider_type, provider_name, description, config, capacity_gb, is_active, created_at, updated_at FROM user_storage WHERE username = $1 - ORDER BY created_at""", + ORDER BY provider_type, created_at""", username ) return [dict(row) for row in rows] +async def get_user_storage_by_type(username: str, provider_type: str) -> list[dict]: + """Get storage providers of a specific type for a user.""" + async with get_connection() as conn: + rows = await conn.fetch( + """SELECT id, username, provider_type, provider_name, description, config, + capacity_gb, is_active, created_at, updated_at + FROM user_storage WHERE username = $1 AND provider_type = $2 + ORDER BY created_at""", + username, provider_type + ) + return [dict(row) for row in rows] + + async def get_storage_by_id(storage_id: int) -> Optional[dict]: """Get a storage provider by ID.""" async with get_connection() as conn: row = await conn.fetchrow( - """SELECT id, username, provider_type, provider_name, config, + """SELECT id, username, provider_type, provider_name, description, config, capacity_gb, is_active, created_at, updated_at FROM user_storage WHERE id = $1""", storage_id @@ -844,16 +858,17 @@ async def add_user_storage( provider_type: str, provider_name: str, config: dict, - capacity_gb: int + capacity_gb: int, + description: Optional[str] = None ) -> Optional[int]: """Add a storage provider for a user. Returns storage ID.""" async with get_connection() as conn: try: row = await conn.fetchrow( - """INSERT INTO user_storage (username, provider_type, provider_name, config, capacity_gb) - VALUES ($1, $2, $3, $4, $5) + """INSERT INTO user_storage (username, provider_type, provider_name, description, config, capacity_gb) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id""", - username, provider_type, provider_name, json.dumps(config), capacity_gb + username, provider_type, provider_name, description, json.dumps(config), capacity_gb ) return row["id"] if row else None except Exception: @@ -862,6 +877,8 @@ async def add_user_storage( async def update_user_storage( storage_id: int, + provider_name: Optional[str] = None, + description: Optional[str] = None, config: Optional[dict] = None, capacity_gb: Optional[int] = None, is_active: Optional[bool] = None @@ -871,6 +888,14 @@ async def update_user_storage( params = [] param_num = 1 + if provider_name is not None: + updates.append(f"provider_name = ${param_num}") + params.append(provider_name) + param_num += 1 + if description is not None: + updates.append(f"description = ${param_num}") + params.append(description) + param_num += 1 if config is not None: updates.append(f"config = ${param_num}") params.append(json.dumps(config)) @@ -974,14 +999,15 @@ async def get_all_active_storage() -> list[dict]: """Get all active storage providers (for distributed pinning).""" async with get_connection() as conn: rows = await conn.fetch( - """SELECT us.*, + """SELECT us.id, us.username, us.provider_type, us.provider_name, us.description, + us.config, us.capacity_gb, us.is_active, us.created_at, us.updated_at, COALESCE(SUM(sp.size_bytes), 0) as used_bytes, COUNT(sp.id) as pin_count FROM user_storage us LEFT JOIN storage_pins sp ON us.id = sp.storage_id WHERE us.is_active = true GROUP BY us.id - ORDER BY us.created_at""" + ORDER BY us.provider_type, us.created_at""" ) return [dict(row) for row in rows] diff --git a/server.py b/server.py index bdb9faa..075ee52 100644 --- a/server.py +++ b/server.py @@ -3126,6 +3126,7 @@ async def add_storage_form( request: Request, provider_type: str = Form(...), provider_name: Optional[str] = Form(None), + description: Optional[str] = Form(None), capacity_gb: int = Form(5), api_key: Optional[str] = Form(None), secret_key: Optional[str] = Form(None), @@ -3190,13 +3191,14 @@ async def add_storage_form( return HTMLResponse(f'
Provider connection failed: {message}
') # Save to database - name = provider_name or f"{provider_type}-{username}" + name = provider_name or f"{provider_type}-{username}-{len(await db.get_user_storage_by_type(username, provider_type)) + 1}" storage_id = await db.add_user_storage( username=username, provider_type=provider_type, provider_name=name, config=config, - capacity_gb=capacity_gb + capacity_gb=capacity_gb, + description=description ) if not storage_id: @@ -3204,7 +3206,7 @@ async def add_storage_form( return HTMLResponse(f'''
Storage provider "{name}" added successfully!
- + ''') @@ -3326,8 +3328,122 @@ async def test_storage(storage_id: int, request: Request, user: User = Depends(g return {"success": success, "message": message} +STORAGE_PROVIDERS_INFO = { + "pinata": {"name": "Pinata", "desc": "1GB free, IPFS pinning", "color": "blue"}, + "web3storage": {"name": "web3.storage", "desc": "IPFS + Filecoin", "color": "green"}, + "nftstorage": {"name": "NFT.Storage", "desc": "Free for NFTs", "color": "pink"}, + "infura": {"name": "Infura IPFS", "desc": "5GB free", "color": "orange"}, + "filebase": {"name": "Filebase", "desc": "5GB free, S3+IPFS", "color": "cyan"}, + "storj": {"name": "Storj", "desc": "25GB free", "color": "indigo"}, + "local": {"name": "Local Storage", "desc": "Your own disk", "color": "purple"}, +} + + async def ui_storage_page(username: str, storages: list, request: Request) -> HTMLResponse: - """Render storage settings page.""" + """Render main storage settings page showing provider types.""" + # Count configs per type + type_counts = {} + for s in storages: + pt = s["provider_type"] + type_counts[pt] = type_counts.get(pt, 0) + 1 + + # Build provider type cards + cards = "" + for ptype, info in STORAGE_PROVIDERS_INFO.items(): + count = type_counts.get(ptype, 0) + count_badge = f'{count}' if count > 0 else "" + cards += f''' + +
+ {info["name"]} + {count_badge} +
+
{info["desc"]}
+
+ ''' + + # Total stats + total_capacity = sum(s["capacity_gb"] for s in storages) + total_used = sum(s.get("used_bytes", 0) for s in storages) + total_pins = sum(s.get("pin_count", 0) for s in storages) + + content = f''' +
+
+

Storage Providers

+
+ +
+

+ Attach your own storage to help power the network. 50% of your capacity is donated to store + shared content, making popular assets more resilient. +

+ +
+
+
{len(storages)}
+
Total Configs
+
+
+
{total_capacity} GB
+
Total Capacity
+
+
+
{total_used / (1024**3):.1f} GB
+
Used
+
+
+
{total_pins}
+
Total Pins
+
+
+ +

Select Provider Type

+
+ {cards} +
+
+
+ ''' + + return HTMLResponse(base_html("Storage", content, username)) + + +@app.get("/storage/type/{provider_type}") +async def storage_type_page(provider_type: str, request: Request, user: User = Depends(get_optional_user)): + """Page for managing storage configs of a specific type.""" + username = user.username if user else get_user_from_cookie(request) + if not username: + return RedirectResponse(url="/login", status_code=302) + + if provider_type not in STORAGE_PROVIDERS_INFO: + raise HTTPException(404, "Invalid provider type") + + storages = await db.get_user_storage_by_type(username, provider_type) + + # Add usage stats + for storage in storages: + usage = await db.get_storage_usage(storage["id"]) + storage["used_bytes"] = usage["used_bytes"] + storage["pin_count"] = usage["pin_count"] + # Mask sensitive config keys + if storage.get("config"): + config = storage["config"] if isinstance(storage["config"], dict) else json.loads(storage["config"]) + masked = {} + for k, v in config.items(): + if "key" in k.lower() or "token" in k.lower() or "secret" in k.lower(): + masked[k] = v[:4] + "..." + v[-4:] if len(str(v)) > 8 else "****" + else: + masked[k] = v + storage["config_display"] = masked + + info = STORAGE_PROVIDERS_INFO[provider_type] + return await ui_storage_type_page(username, provider_type, info, storages, request) + + +async def ui_storage_type_page(username: str, provider_type: str, info: dict, storages: list, request: Request) -> HTMLResponse: + """Render per-type storage management page.""" def format_bytes(b): if b > 1024**3: @@ -3338,26 +3454,29 @@ async def ui_storage_page(username: str, storages: list, request: Request) -> HT return f"{b / 1024:.1f} KB" return f"{b} bytes" + # Build storage rows storage_rows = "" for s in storages: status_class = "bg-green-600" if s["is_active"] else "bg-gray-600" status_text = "Active" if s["is_active"] else "Inactive" config_display = s.get("config_display", {}) config_html = ", ".join(f"{k}: {v}" for k, v in config_display.items() if k != "path") + desc = s.get("description") or "" + desc_html = f'
{desc}
' if desc else "" storage_rows += f''' -
+
-

{s["provider_name"] or s["provider_type"]}

- {s["provider_type"]} +

{s["provider_name"] or provider_type}

+ {desc_html}
{status_text} -
@@ -3368,7 +3487,7 @@ async def ui_storage_page(username: str, storages: list, request: Request) -> HT
Donated
-
{s["donated_gb"]} GB
+
{s["capacity_gb"] // 2} GB
Used
@@ -3385,183 +3504,106 @@ async def ui_storage_page(username: str, storages: list, request: Request) -> HT ''' if not storages: - storage_rows = '

No storage providers configured.

' + storage_rows = f'

No {info["name"]} configs yet. Add one below.

' + + # Build form fields based on provider type + form_fields = "" + if provider_type == "pinata": + form_fields = ''' +
+ + +
+
+ + +
+ ''' + elif provider_type in ("web3storage", "nftstorage"): + form_fields = ''' +
+ + +
+ ''' + elif provider_type == "infura": + form_fields = ''' +
+ + +
+
+ + +
+ ''' + elif provider_type in ("filebase", "storj"): + form_fields = ''' +
+ + +
+
+ + +
+
+ + +
+ ''' + elif provider_type == "local": + form_fields = ''' +
+ + +
+ ''' content = f'''
-
-

Storage Providers

+
+ ← Back +

{info["name"]} Storage

-

- Attach your own storage to help power the network. 50% of your capacity is donated to store - shared content, making popular assets more resilient. -

- -

Add Storage Provider

- -
- - - - - - - -
- - -

Your Storage Providers

-
- {storage_rows} +
+

Add New {info["name"]} Config

+
+ + + {form_fields} + +
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
- - ''' - return HTMLResponse(base_html("Storage", content, username)) + return HTMLResponse(base_html(f"{info['name']} Storage", content, username)) # ============ Client Download ============