feat: upload any validated file to the asset library

Activos now supports multi-file/folder upload to private Storage
(PDF, Excel, CSV, images, ZIP, etc.) with per-tenant folders.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-22 15:26:33 +02:00
parent 8182402d96
commit ce9498e63c
9 changed files with 931 additions and 63 deletions

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Apply sql/asset_files.sql + ensure storage bucket via Dokploy / API."""
from __future__ import annotations
import base64
import json
import os
import re
import sys
import urllib.request
from pathlib import Path
from playwright.sync_api import sync_playwright
ROOT = Path("/Users/4ventures/4ventures-client-portal")
BPS = Path("/Users/4ventures/Projects/bps-facturacion")
SQL = ROOT / "sql" / "asset_files.sql"
BASE = "https://dokploy.4ventures.es"
def env(key: str) -> str:
v = os.environ.get(key, "").strip()
if v:
return v
p = BPS / ".env"
if not p.is_file():
return ""
for line in p.read_text().splitlines():
if line.startswith(f"{key}="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
return ""
def main() -> int:
email = env("DOKPLOY_EMAIL") or "marketing@4ventures.es"
password = env("DOKPLOY_PASSWORD")
b64 = base64.b64encode(SQL.read_text(encoding="utf-8").encode()).decode()
chunks = [b64[i : i + 180] for i in range(0, len(b64), 180)]
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1400, "height": 1000})
page.goto(BASE + "/", wait_until="domcontentloaded", timeout=90000)
page.wait_for_timeout(800)
if "/dashboard" not in page.url:
page.locator('input[type="email"], input[name="email"]').first.fill(email)
page.locator('input[type="password"]').first.fill(password)
page.locator('button[type="submit"]').first.click()
page.wait_for_url(re.compile(r"/dashboard"), timeout=90000)
print("login ok", flush=True)
page.goto(f"{BASE}/dashboard/docker", wait_until="domcontentloaded", timeout=120000)
page.wait_for_timeout(800)
page.locator("input[placeholder*='ilter' i], input[placeholder*='earch' i]").first.fill(
"supabase-db"
)
page.wait_for_timeout(1000)
page.keyboard.press("Escape")
page.wait_for_timeout(200)
page.get_by_role("button", name=re.compile(r"Open menu", re.I)).first.click()
page.wait_for_timeout(400)
page.get_by_text("Terminal", exact=True).click()
page.wait_for_timeout(1000)
if page.locator("button", has_text="Bash").count():
page.locator("button", has_text="Bash").first.click()
page.wait_for_timeout(2800)
if page.locator("textarea.xterm-helper-textarea").count():
page.locator("textarea.xterm-helper-textarea").first.click()
def run(cmd: str, wait: int = 800) -> None:
page.keyboard.type(cmd, delay=1)
page.keyboard.press("Enter")
page.wait_for_timeout(wait)
run("rm -f /tmp/af.b64 /tmp/af.sql; echo READY", 1200)
for i, ch in enumerate(chunks):
op = ">" if i == 0 else ">>"
run(f"printf '%s' '{ch}' {op} /tmp/af.b64", 400)
run(
"base64 -d /tmp/af.b64 > /tmp/af.sql && "
"psql -U postgres -d postgres -v ON_ERROR_STOP=1 -f /tmp/af.sql; echo EXIT:$?",
9000,
)
text = page.evaluate(
"() => (document.querySelector('.xterm-rows')||{}).innerText||''"
)
browser.close()
ok = "EXIT:0" in text
print("SQL_OK", ok, flush=True)
# Create bucket via service role from .env.local
vals = {}
for ln in (ROOT / ".env.local").read_text().splitlines():
if "=" in ln and not ln.strip().startswith("#"):
k, v = ln.split("=", 1)
vals[k.strip()] = v.strip().strip('"').strip("'")
url = vals["NEXT_PUBLIC_SUPABASE_URL"].rstrip("/")
key = vals["SUPABASE_SERVICE_ROLE_KEY"]
req = urllib.request.Request(
url + "/storage/v1/bucket",
data=json.dumps(
{
"id": "tenant-assets",
"name": "tenant-assets",
"public": False,
"file_size_limit": 83886080,
}
).encode(),
headers={
"apikey": key,
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=20) as r:
print("BUCKET", r.status, flush=True)
except urllib.error.HTTPError as e:
body = e.read().decode()[:200]
print("BUCKET", e.code, body, flush=True)
if e.code not in (409, 400) and "exist" not in body.lower():
return 5 if not ok else 0
return 0 if ok else 6
if __name__ == "__main__":
raise SystemExit(main())