mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
QuotaProvider grows a read half: CheckRead(org, bytes) and RecordEgress(org, bytes). CheckRead is enforced on /s/* only — a public share link is the sole unauthenticated door to stored bytes, so it is the only egress a plan can cap. The sync proxy and viewer merely RecordEgress: refusing a device mid-cycle surfaces as ErrForbidden, which the syncer reads as "access is gone — pause and touch nothing", and sync must never break over a bill. UnlimitedQuota stays the OSS default. countingWriter bills what actually reached the client rather than a size claimed before the write. bdrive init warns past 1 GiB or 20k files and says how to narrow scope; syncer.Measure sizes that through the real Filter and the one walkFolder predicate. starterIgnore gains video/archive/disk-image globs and Library/ — every version is kept forever, so a big binary committed once is paid for forever on every device. deploy: a Nearline-at-30-days lifecycle rule and the arithmetic for why it stops there. Coldline and Archive only pay off below roughly one read per month, and a first sync pulls every historical blob rather than just the current tree, so blob read rate tracks device onboarding. docs/launch-plan.md said Cloud was waitlist-only and framed Product Hunt as an OSS launch whose goal was not signups or revenue; both are stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
148 lines
6.7 KiB
Bash
Executable File
148 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Deploy the BearDrive hub to Google Cloud Run (single instance) with Cloud SQL
|
|
# Postgres for metadata and a GCS bucket for blobs/journals.
|
|
#
|
|
# The hub is single-process for now, so max-instances=1 (in-memory caches
|
|
# assume one writer). Run from the repo root: bash example/deploy/gcp-cloudrun.sh
|
|
set -euo pipefail
|
|
|
|
# gcloud needs Python >= 3.10 (3.9 crashes `gcloud builds`); point it at a
|
|
# newer interpreter if your system default is old:
|
|
# export CLOUDSDK_PYTHON=$(command -v python3.11 || command -v python3.12)
|
|
|
|
# ---- fill these in -----------------------------------------------------------
|
|
PROJECT_ID="${PROJECT_ID:?set PROJECT_ID}" # dedicated GCP project id
|
|
BILLING_ACCOUNT="${BILLING_ACCOUNT:-}" # e.g. 0X0X0X-0X0X0X-0X0X0X (only needed if creating the project)
|
|
REGION="${REGION:-us-central1}"
|
|
ADMIN_EMAIL="${ADMIN_EMAIL:?set ADMIN_EMAIL}" # first hub admin
|
|
ADMIN_DOMAIN="${ADMIN_DOMAIN:?set ADMIN_DOMAIN}" # signup limited to this email domain for bootstrap, e.g. runbear.io
|
|
BRAND="${BRAND:-BearDrive}"
|
|
# ------------------------------------------------------------------------------
|
|
BUCKET="${BUCKET:-${PROJECT_ID}-bdrive}"
|
|
SQL_INSTANCE="${SQL_INSTANCE:-bdrive-pg}"
|
|
SQL_TIER="${SQL_TIER:-db-f1-micro}"
|
|
DB_NAME="bdrive"; DB_USER="bdrive"
|
|
SERVICE="${SERVICE:-bdrive}"
|
|
RUN_SA="bdrive-run@${PROJECT_ID}.iam.gserviceaccount.com"
|
|
CONN_NAME="${PROJECT_ID}:${REGION}:${SQL_INSTANCE}"
|
|
|
|
echo "== project =="
|
|
gcloud projects describe "$PROJECT_ID" >/dev/null 2>&1 || {
|
|
echo "creating project $PROJECT_ID"; gcloud projects create "$PROJECT_ID"
|
|
[ -n "$BILLING_ACCOUNT" ] && gcloud billing projects link "$PROJECT_ID" --billing-account "$BILLING_ACCOUNT"
|
|
}
|
|
gcloud config set project "$PROJECT_ID"
|
|
|
|
echo "== enable APIs =="
|
|
gcloud services enable run.googleapis.com sqladmin.googleapis.com storage.googleapis.com \
|
|
secretmanager.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com
|
|
|
|
echo "== GCS bucket for blobs/journals =="
|
|
gcloud storage buckets describe "gs://$BUCKET" >/dev/null 2>&1 || \
|
|
gcloud storage buckets create "gs://$BUCKET" --location "$REGION" --uniform-bucket-level-access
|
|
|
|
# Age old objects down to Nearline. Every version of every file is retained
|
|
# forever, so storage grows monotonically against flat per-seat revenue; at
|
|
# 30 days Nearline halves the per-GB cost ($0.020 → $0.010 in us-central1).
|
|
#
|
|
# Nearline and NOT Coldline/Archive, deliberately — see deploy/README.md
|
|
# "Storage tiering". Colder tiers only pay off when an object is read less
|
|
# than roughly once a month (Coldline) or once a year (Archive), and today a
|
|
# BearDrive device that syncs a project for the first time downloads EVERY
|
|
# historical blob, not just the current file tree. So blob reads scale with
|
|
# how often anyone adds a device, which is nowhere near cold enough for
|
|
# Archive to be anything but a bill increase. Revisit when a first sync
|
|
# fetches only current-state blobs.
|
|
#
|
|
# Applied bucket-wide rather than to blobs/ only: journal objects sit under
|
|
# the same per-project prefixes and a GCS lifecycle prefix cannot express
|
|
# "*/blobs/". That is safe here because a peer journal is re-fetched only
|
|
# when the listing shows it GREW (internal/syncer pull), and one that grew
|
|
# was just rewritten and is Standard again.
|
|
if [ "${LIFECYCLE:-1}" = "1" ]; then
|
|
echo "== GCS lifecycle: Nearline at 30 days (LIFECYCLE=0 to skip) =="
|
|
TMP_LIFECYCLE="$(mktemp)"
|
|
cat >"$TMP_LIFECYCLE" <<'JSON'
|
|
{
|
|
"lifecycle": {
|
|
"rule": [
|
|
{
|
|
"action": { "type": "SetStorageClass", "storageClass": "NEARLINE" },
|
|
"condition": { "age": 30, "matchesStorageClass": ["STANDARD"] }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
JSON
|
|
gcloud storage buckets update "gs://$BUCKET" --lifecycle-file="$TMP_LIFECYCLE"
|
|
rm -f "$TMP_LIFECYCLE"
|
|
fi
|
|
|
|
echo "== Cloud SQL Postgres (this takes several minutes) =="
|
|
gcloud sql instances describe "$SQL_INSTANCE" >/dev/null 2>&1 || \
|
|
gcloud sql instances create "$SQL_INSTANCE" --database-version POSTGRES_16 \
|
|
--edition ENTERPRISE --tier "$SQL_TIER" --region "$REGION" \
|
|
--storage-size 10 --storage-auto-increase
|
|
gcloud sql databases describe "$DB_NAME" --instance "$SQL_INSTANCE" >/dev/null 2>&1 || \
|
|
gcloud sql databases create "$DB_NAME" --instance "$SQL_INSTANCE"
|
|
DB_PASS="$(gcloud secrets versions access latest --secret bdrive-db-pass 2>/dev/null || true)"
|
|
if [ -z "$DB_PASS" ]; then
|
|
DB_PASS="$(openssl rand -base64 24 | tr -d '/+=')"
|
|
printf '%s' "$DB_PASS" | gcloud secrets create bdrive-db-pass --data-file=- 2>/dev/null || \
|
|
printf '%s' "$DB_PASS" | gcloud secrets versions add bdrive-db-pass --data-file=-
|
|
fi
|
|
gcloud sql users create "$DB_USER" --instance "$SQL_INSTANCE" --password "$DB_PASS" 2>/dev/null || \
|
|
gcloud sql users set-password "$DB_USER" --instance "$SQL_INSTANCE" --password "$DB_PASS"
|
|
|
|
echo "== runtime service account + IAM =="
|
|
gcloud iam service-accounts describe "$RUN_SA" >/dev/null 2>&1 || \
|
|
gcloud iam service-accounts create bdrive-run --display-name "BearDrive Cloud Run"
|
|
gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" \
|
|
--member "serviceAccount:$RUN_SA" --role roles/storage.objectAdmin
|
|
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
|
|
--member "serviceAccount:$RUN_SA" --role roles/cloudsql.client >/dev/null
|
|
|
|
echo "== hub config secret (contains the DB DSN) =="
|
|
# Bootstrap posture: domain-gated self-signup so the first admin can create
|
|
# their account and become owner; tighten to invite-only afterwards.
|
|
CONFIG="$(cat <<JSON
|
|
{
|
|
"remote": "gs://$BUCKET/hub",
|
|
"addr": ":8080",
|
|
"upload": true,
|
|
"database": {
|
|
"driver": "postgres",
|
|
"dsn": "postgres://$DB_USER:$DB_PASS@/$DB_NAME?host=/cloudsql/$CONN_NAME&sslmode=disable"
|
|
},
|
|
"auth": {
|
|
"allow_signup": true,
|
|
"allowed_domains": ["$ADMIN_DOMAIN"],
|
|
"admins": ["$ADMIN_EMAIL"],
|
|
"brand": "$BRAND"
|
|
}
|
|
}
|
|
JSON
|
|
)"
|
|
printf '%s' "$CONFIG" | gcloud secrets create bdrive-config --data-file=- 2>/dev/null || \
|
|
printf '%s' "$CONFIG" | gcloud secrets versions add bdrive-config --data-file=-
|
|
gcloud secrets add-iam-policy-binding bdrive-config \
|
|
--member "serviceAccount:$RUN_SA" --role roles/secretmanager.secretAccessor >/dev/null
|
|
|
|
echo "== build + deploy to Cloud Run (single instance) =="
|
|
gcloud run deploy "$SERVICE" \
|
|
--source . \
|
|
--region "$REGION" \
|
|
--service-account "$RUN_SA" \
|
|
--add-cloudsql-instances "$CONN_NAME" \
|
|
--update-secrets "/config/config.json=bdrive-config:latest" \
|
|
--args "web,-c,/config/config.json" \
|
|
--min-instances 1 --max-instances 1 \
|
|
--cpu 1 --memory 512Mi \
|
|
--allow-unauthenticated
|
|
|
|
echo
|
|
echo "Deployed. URL:"
|
|
gcloud run services describe "$SERVICE" --region "$REGION" --format 'value(status.url)'
|
|
echo "Next: open the URL, Sign up as $ADMIN_EMAIL (domain-gated), then tighten"
|
|
echo "auth to invite-only by editing the bdrive-config secret + redeploying."
|