CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 3s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 27s
CI and images / backend-lint-and-test (push) Successful in 40s
CI and images / integration (push) Failing after 19s
CI and images / sign-extension (push) Skipped
CI and images / build-web (push) Skipped
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped
CI and images / build-agent (push) Skipped
Settings → Maintenance → Storage gains "Off-site backup destination": the
address (with its port), bucket, region, access key and secret key of an S3
bucket such as Fabled Archives. The secret key is sealed with the credential
key, never returned by the API, and kept when the field is left blank. Test
destination lists the bucket with the fields as typed and writes nothing, and
names the cause when it fails ("nothing is listening there (check the port)",
"the secret key is wrong", "answered, but not as S3").
services/backup_s3 is a standard-library SigV4 client (put, get, paged list)
ported from Grimoire's; it is held to AWS's published signing examples and
exercised against a local HTTP server. Migration 0118 adds the columns; no
destination is set up until someone saves one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 lines
1.3 KiB
Python
44 lines
1.3 KiB
Python
"""import_settings: the off-site backup destination (an S3 bucket).
|
|
|
|
Address, bucket, access key and region are plain settings; the secret key is
|
|
stored sealed with the credential key. All empty means no destination is set
|
|
up, which is every existing install's state.
|
|
|
|
Revision ID: 0118
|
|
Revises: 0117
|
|
Create Date: 2026-11-06
|
|
|
|
"""
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision = "0118"
|
|
down_revision = "0117"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.add_column("import_settings", sa.Column("backup_s3_endpoint", sa.Text(), nullable=True))
|
|
op.add_column(
|
|
"import_settings", sa.Column("backup_s3_bucket", sa.String(255), nullable=True),
|
|
)
|
|
op.add_column("import_settings", sa.Column("backup_s3_access_key", sa.Text(), nullable=True))
|
|
op.add_column(
|
|
"import_settings", sa.Column("backup_s3_secret_sealed", sa.Text(), nullable=True),
|
|
)
|
|
op.add_column(
|
|
"import_settings",
|
|
sa.Column(
|
|
"backup_s3_region", sa.String(64), nullable=False, server_default="us-east-1",
|
|
),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
for column in (
|
|
"backup_s3_region", "backup_s3_secret_sealed", "backup_s3_access_key",
|
|
"backup_s3_bucket", "backup_s3_endpoint",
|
|
):
|
|
op.drop_column("import_settings", column)
|