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}
Test
- Remove
@@ -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 = '''
+
+ API Key *
+
+
+
+ Secret Key *
+
+
+ '''
+ elif provider_type in ("web3storage", "nftstorage"):
+ form_fields = '''
+
+ API Token *
+
+
+ '''
+ elif provider_type == "infura":
+ form_fields = '''
+
+ Project ID *
+
+
+
+ Project Secret *
+
+
+ '''
+ elif provider_type in ("filebase", "storj"):
+ form_fields = '''
+
+ Access Key *
+
+
+
+ Secret Key *
+
+
+
+ Bucket Name *
+
+
+ '''
+ elif provider_type == "local":
+ form_fields = '''
+
+ Storage Path *
+
+
+ '''
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
-
-
-
- Pinata
- 1GB free, IPFS
-
-
- web3.storage
- IPFS + Filecoin
-
-
- NFT.Storage
- Free for NFTs
-
-
- Infura IPFS
- 5GB free
-
-
- Filebase
- 5GB free, S3+IPFS
-
-
- Storj
- 25GB free
-
-
- Local Storage
- Your own disk
-
-
-
-
-
Your Storage Providers
-
- {storage_rows}
+
+
Add New {info["name"]} Config
+
+
-
-
'''
- return HTMLResponse(base_html("Storage", content, username))
+ return HTMLResponse(base_html(f"{info['name']} Storage", content, username))
# ============ Client Download ============