name: google-drive-to-supabase-migration
description: Migrate files from Google Drive shared folder to Supabase Cloud storage + database via Edge Functions (bypasses RLS)
triggers: ["migrate drive documents", "google drive to supabase", "RLS blocking insert"]
Google Drive → Supabase Cloud Migration
Migrate documents from Google Drive shared folders to DocCorretor (Supabase Cloud) storage and database.
Context
DocCorretor is a TanStack Start app on VPS with Supabase Cloud backend. Documents are stored as files on VPS (/var/www/documentos/uploads/) and referenced in documents table. The site is documentos.rochasalesseguros.com.br.
Key Challenge: RLS Blocking
Supabase Cloud has Row Level Security. Anon key cannot INSERT into documents, client_folders tables.
Two reliable options to bypass RLS:
1. Management API (PAT) — recommended. Works even when service_role key returns 401.
POST https://api.supabase.com/v1/projects/{ref}/database/query with Authorization: Bearer {PAT}
2. Edge Functions — requires working service_role key (currently broken — returns 401).
Migration Pattern
1. Discover files via Playwright
`python
from playwright.sync_api import sync_playwright
def get_file_list(page, folder_url: str) -> list:
page.goto(folder_url, timeout=30000)
page.wait_for_timeout(5000)
for _ in range(5):
page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
page.wait_for_timeout(1000)
return page.evaluate('''() => {
const results = [];
const seen = new Set();
document.querySelectorAll("tr[data-id]").forEach(tr => {
const id = tr.getAttribute("data-id");
const ariaLabel = tr.getAttribute("aria-label") || "";
if (ariaLabel.includes("Pasta compartilhada")) return;
if (id && !id.includes("folders") && id.length > 20 && !seen.has(id)) {
seen.add(id);
const nameSpan = tr.querySelector("span[title]");
const name = nameSpan ? nameSpan.getAttribute("title") : id;
if (name && name.length > 1) results.push({name, file_id: id});
}
});
return results;
}''')
`
2. Download files preserving real names
CRITICAL: Google Drive returns file content with filename in Content-Disposition header (e.g., attachment; filename="COMP RESIDENCIA.pdf"). The file ID from Drive UI is NOT the filename. Use curl -J -O to extract the real name:
`python
import subprocess, os, shutil
def download_file(file_id: str, original_name: str, temp_dir: str, timeout: int = 60) -> tuple:
"""Uses -J -O to extract filename from Content-Disposition header.
Returns (absolute_path, real_name) or ("", "") on failure."""
url = f"https://drive.google.com/uc?export=download&id={file_id}"
abs_temp_dir = os.path.abspath(temp_dir)
original_cwd = os.getcwd()
try:
os.chdir(abs_temp_dir)
subprocess.run(
['curl', '-s', '-L', '-J', '-O', '-m', str(timeout), url],
capture_output=True, timeout=timeout + 10
)
finally:
os.chdir(original_cwd)
# curl -J saves with the Content-Disposition name
for fname in os.listdir(abs_temp_dir):
fpath = os.path.join(abs_temp_dir, fname)
if os.path.isfile(fpath) and os.path.getsize(fpath) > 5000:
return (fpath, fname)
return ("", "")
`
Do NOT use -o with -J — they conflict. Use cwd= in subprocess or os.chdir().
3. Download files + Bypass RLS with Management API
Download from Google Drive (use gdown locally — VPS has no pip):
`bash
/root/.hermes/hermes-agent/venv/bin/gdown {folder_id} -O /tmp/dest/ --folder
`
Management API INSERT (bypasses RLS even when service_role key is broken):
`python
import urllib.request, json
PAT = "sbp_REDACTED"
REF = "dauftiqcvgaydddoxqhh"
def insert_documents(documents: list) -> dict:
"""Insert documents via Supabase Management API. Batches of ~100."""
values = ", ".join([
f"('{d['id']}', '{d['client_id']}', '{d['folder_id']}', "
f"'{d['title'].replace(\"'\", \"''\")}', "
f"'{d['original_name'].replace(\"'\", \"''\")}', "
f"'{d['stored_name'].replace(\"'\", \"''\")}', "
f"'{d['storage_path'].replace(\"'\", \"''\")}', "
f"'{d['mime_type']}', {d['size_bytes']}, "
f"'{d['category']}', '{d['uploaded_by']}')"
for d in documents
])
sql = f"""
INSERT INTO public.documents
(id, client_id, folder_id, title, original_name, stored_name, storage_path,
mime_type, size_bytes, category, uploaded_by)
VALUES {values}
"""
req = urllib.request.Request(
f"https://api.supabase.com/v1/projects/{REF}/database/query",
data=json.dumps({"query": sql}).encode(),
headers={"Authorization": f"Bearer {PAT}", "Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read())
`
Notes:
- - No
$1placeholders — use literal values in SQL - - Escape single quotes as
'' - - Skip
.partfiles (incomplete gdown downloads — 0 bytes) - -
stored_namein DB must match actual filename on disk exactly - -
gdown --folderdownloads entire Drive folder preserving real filenames - - Management API (PAT) INSERT bypasses RLS (works when service_role key returns 401)
- -
chown ubuntu:ubuntu+chmod 644— nginx serves files correctly (ubuntu, not www-data) - -
stored_namein DB must match actual on-disk filename exactly - - Skip
.partfiles (incomplete gdown downloads) - - Ref:
dauftiqcvgaydddoxqhh - - Vinicius profile ID (uploaded_by):
5a4ebb41-9e54-47b3-86eb-1c18bc01ad5a - - Edge Function:
migrate-drive
4. Three-table seeding order
clients → client_folders → documents. FK dependencies: client_folders.client_id → clients.id, documents.client_id → clients.id, documents.folder_id → client_folders.id.
CRITICAL: folder_id in documents must reference client_folders.id, NOT clients.id. These are different UUIDs.
5. Storage path and permissions
Files go to /var/www/documentos/uploads/{uploaded_by}/{client_id}/{stored_name}.
CRITICAL: nginx runs as user ubuntu, NOT www-data.
After copying via SCP, always run:
`bash
sshpass -p 'Marcia19671951@' ssh root@31.97.243.106 \
"chown ubuntu:ubuntu /var/www/documentos/uploads/{user_id}/{client_id}/* && \
chmod 644 /var/www/documentos/uploads/{user_id}/{client_id}/*"
`
Do NOT use chmod -R o+r — that's for www-data. root:root 600 causes 403.
6. Debugging: Check Edge Function directly
`python
import urllib.request, json
EDGE_URL = f'https://dauftiqcvgaydddoxqhh.supabase.co/functions/v1/migrate-drive'
ANON_KEY = '...'
payload = json.dumps({'action': 'status'}).encode()
req = urllib.request.Request(EDGE_URL, data=payload, headers={
'Authorization': f'Bearer {ANON_KEY}', 'apikey': ANON_KEY, 'Content-Type': 'application/json'
}, method='POST')
with urllib.request.urlopen(req, timeout=30) as resp:
print(json.loads(resp.read()))
`