diff --git a/.agents/skills/pgpm/references/ci-cd.md b/.agents/skills/pgpm/references/ci-cd.md index c4860c8ff0..ac65b76c8f 100644 --- a/.agents/skills/pgpm/references/ci-cd.md +++ b/.agents/skills/pgpm/references/ci-cd.md @@ -75,13 +75,13 @@ env: PGPASSWORD: password ``` -For MinIO/S3 testing (uploads, storage): +For RustFS/S3 testing (uploads, storage): ```yaml env: - MINIO_ENDPOINT: http://localhost:9000 - AWS_ACCESS_KEY: minioadmin - AWS_SECRET_KEY: minioadmin + OBJECT_STORE_ENDPOINT: http://localhost:9000 + AWS_ACCESS_KEY: constructive + AWS_SECRET_KEY: constructive-dev-secret AWS_REGION: us-east-1 BUCKET_NAME: test-bucket ``` @@ -320,22 +320,22 @@ steps: fi ``` -## MinIO Service Container +## RustFS Service Container For testing uploads and S3-compatible storage: ```yaml services: - minio_cdn: - image: minio/minio:edge-cicd + rustfs_cdn: + image: rustfs/rustfs:1.0.0-rc.5 env: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin + RUSTFS_ACCESS_KEY: constructive + RUSTFS_SECRET_KEY: constructive-dev-secret + RUSTFS_ADDRESS: ":9000" ports: - 9000:9000 - - 9001:9001 options: >- - --health-cmd "curl -f http://localhost:9000/minio/health/live || exit 1" + --health-cmd "curl -f http://localhost:9000/health || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5 diff --git a/.agents/skills/pgpm/references/environment-configuration.md b/.agents/skills/pgpm/references/environment-configuration.md index 2eee68fa37..9aa0541096 100644 --- a/.agents/skills/pgpm/references/environment-configuration.md +++ b/.agents/skills/pgpm/references/environment-configuration.md @@ -128,12 +128,12 @@ const deployOptions = getDeploymentEnvOptions(); | Variable | Description | |----------|-------------| -| `BUCKET_PROVIDER` | Storage provider (s3, minio, rustfs, gcs) — defaults to minio | +| `BUCKET_PROVIDER` | Storage provider (s3, minio, rustfs, gcs) — `minio` is path-style S3-compatible storage (RustFS, MinIO) | | `BUCKET_NAME` | Bucket name | | `AWS_REGION` | AWS region | | `AWS_ACCESS_KEY_ID` | AWS access key | | `AWS_SECRET_ACCESS_KEY` | AWS secret key | -| `MINIO_ENDPOINT` | S3-compatible endpoint URL (MinIO or RustFS; both listen on 9000) | +| `OBJECT_STORE_ENDPOINT` | S3-compatible endpoint URL (RustFS or MinIO; both listen on 9000) | ### Jobs Configuration diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 5a4b52fdf1..54a848effd 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -37,7 +37,7 @@ concurrency: # Service tiers (avoids spinning up unneeded containers): # unit-tests → no services (pure JS/TS) # pg-tests → PostgreSQL only -# integration-tests → PostgreSQL + MinIO +# integration-tests → PostgreSQL + RustFS # ai-tests → PostgreSQL + Ollama # --------------------------------------------------------------------------- @@ -333,7 +333,7 @@ jobs: done # ========================================================================= - # TIER 3 – Integration tests (PostgreSQL + MinIO) + # TIER 3 – Integration tests (PostgreSQL + RustFS) # ========================================================================= integration-tests: needs: build @@ -356,8 +356,8 @@ jobs: PGUSER: postgres PGPASSWORD: password CDN_ENDPOINT: http://localhost:9000 - AWS_ACCESS_KEY: minioadmin - AWS_SECRET_KEY: minioadmin + AWS_ACCESS_KEY: constructive + AWS_SECRET_KEY: constructive-dev-secret AWS_REGION: us-east-1 # uploads/s3-streamer reads BUCKET_NAME; harmless for the others. BUCKET_NAME: test-bucket @@ -379,16 +379,16 @@ jobs: ports: - 5432:5432 - minio_cdn: - image: minio/minio:edge-cicd + rustfs_cdn: + image: rustfs/rustfs:1.0.0-rc.5 env: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin + RUSTFS_ACCESS_KEY: constructive + RUSTFS_SECRET_KEY: constructive-dev-secret + RUSTFS_ADDRESS: ":9000" ports: - 9000:9000 - - 9001:9001 options: >- - --health-cmd "curl -f http://localhost:9000/minio/health/live || exit 1" + --health-cmd "curl -f http://localhost:9000/health || exit 1" --health-interval 3s --health-timeout 5s --health-retries 10 diff --git a/CLAUDE.md b/CLAUDE.md index 3aac6ee6b8..e8b5c156d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,7 +69,7 @@ React Query Hooks or Prisma-like ORM Client | `graphile/` | PostGraphile plugins - filters, i18n, meta-schema, PostGIS, search, uploads, settings | | `postgres/` | PostgreSQL utilities - introspection, testing (pgsql-test), seeding, AST, query context | | `packages/` | Shared utilities - CLI (`cnc`), ORM base, query builder, server utils, client | -| `uploads/` | File streaming - S3/MinIO, ETags, content-type detection, UUID hashing | +| `uploads/` | File streaming - RustFS (or any S3-compatible store), ETags, content-type detection, UUID hashing | ### Key Packages & CLIs @@ -115,7 +115,7 @@ Tests require PostgreSQL. Standard PG env vars: - `PGHOST` (default: localhost), `PGPORT` (default: 5432) - `PGUSER` (default: postgres), `PGPASSWORD` (default: password) -For S3/MinIO tests: `MINIO_ENDPOINT`, `AWS_ACCESS_KEY`, `AWS_SECRET_KEY`, `AWS_REGION` +For S3/RustFS tests: `OBJECT_STORE_ENDPOINT`, `AWS_ACCESS_KEY`, `AWS_SECRET_KEY`, `AWS_REGION` ## Build System diff --git a/docker-compose.yml b/docker-compose.yml index 19dcf6c2d8..d11e25389d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,34 +12,12 @@ services: networks: - constructive-net - minio: - container_name: minio - image: minio/minio - environment: - - MINIO_ROOT_USER=minioadmin - - MINIO_ROOT_PASSWORD=minioadmin - ports: - - "9000:9000" - - "9001:9001" - expose: - - "9000" - - "9001" - command: server /data --console-address ":9001" - networks: - - constructive-net - - # RustFS is an alternative to the `minio` service above: same S3 API on 9000 - # and console on 9001, so the two cannot run together. Behind a profile: - # `docker compose up postgres` keeps MinIO, `docker compose --profile rustfs - # up postgres rustfs` swaps it in. Pair it with BUCKET_PROVIDER=rustfs. rustfs: container_name: rustfs - image: rustfs/rustfs:latest - profiles: - - rustfs + image: rustfs/rustfs:1.0.0-rc.5 environment: - - RUSTFS_ACCESS_KEY=minioadmin - - RUSTFS_SECRET_KEY=minioadmin + - RUSTFS_ACCESS_KEY=constructive + - RUSTFS_SECRET_KEY=constructive-dev-secret - RUSTFS_ADDRESS=:9000 - RUSTFS_CONSOLE_ADDRESS=:9001 - RUSTFS_CONSOLE_ENABLE=true @@ -49,7 +27,6 @@ services: expose: - "9000" - "9001" - command: /data networks: - constructive-net diff --git a/graphile/graphile-presigned-url-plugin/__tests__/s3-signer.integration.test.ts b/graphile/graphile-presigned-url-plugin/__tests__/s3-signer.integration.test.ts index 2349275ba8..ce95f375ed 100644 --- a/graphile/graphile-presigned-url-plugin/__tests__/s3-signer.integration.test.ts +++ b/graphile/graphile-presigned-url-plugin/__tests__/s3-signer.integration.test.ts @@ -1,12 +1,12 @@ /** - * Integration tests for s3-signer against a real MinIO instance. + * Integration tests for s3-signer against a real RustFS instance. * * These tests exercise the presigned URL pipeline end-to-end: * 1. generatePresignedPutUrl → PUT a file via the presigned URL * 2. headObject → verify the file exists with correct metadata * 3. generatePresignedGetUrl → GET the file via the presigned URL * - * Requires MinIO running on localhost:9000 (docker-compose or CI service). + * Requires RustFS running on localhost:9000 (docker-compose or CI service). */ import { S3Client } from '@aws-sdk/client-s3'; @@ -21,12 +21,12 @@ import { } from '../src/s3-signer'; import type { S3Config } from '../src/types'; -// --- MinIO config (matches docker-compose.yml + CI env) --- +// --- RustFS config (matches docker-compose.yml + CI env) --- -const MINIO_ENDPOINT = process.env.CDN_ENDPOINT || 'http://localhost:9000'; +const OBJECT_STORE_ENDPOINT = process.env.CDN_ENDPOINT || 'http://localhost:9000'; const AWS_REGION = process.env.AWS_REGION || 'us-east-1'; -const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY || 'minioadmin'; -const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY || 'minioadmin'; +const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY || 'constructive'; +const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY || 'constructive-dev-secret'; const TEST_BUCKET = 'presigned-url-test-bucket'; // --- S3 client + config --- @@ -37,14 +37,14 @@ const s3Client = new S3Client({ secretAccessKey: AWS_SECRET_KEY, }, region: AWS_REGION, - endpoint: MINIO_ENDPOINT, + endpoint: OBJECT_STORE_ENDPOINT, forcePathStyle: true, }); const s3Config: S3Config = { client: s3Client, bucket: TEST_BUCKET, - endpoint: MINIO_ENDPOINT, + endpoint: OBJECT_STORE_ENDPOINT, region: AWS_REGION, forcePathStyle: true, }; @@ -101,7 +101,7 @@ async function downloadFromPresignedUrl(url: string): Promise<{ // --- Tests --- -describe('s3-signer integration (MinIO)', () => { +describe('s3-signer integration (RustFS)', () => { describe('generatePresignedPutUrl', () => { it('should generate a presigned PUT URL that accepts a valid upload', async () => { const key = 'test-put-basic.txt'; diff --git a/graphile/graphile-presigned-url-plugin/src/types.ts b/graphile/graphile-presigned-url-plugin/src/types.ts index 510bda872e..c28629bf37 100644 --- a/graphile/graphile-presigned-url-plugin/src/types.ts +++ b/graphile/graphile-presigned-url-plugin/src/types.ts @@ -13,7 +13,7 @@ export interface BucketConfig { max_file_size: number | null; allow_custom_keys: boolean; /** - * The physical S3/MinIO bucket name recorded by reconciliation. NULL until + * The physical S3-compatible (RustFS, MinIO) bucket name recorded by reconciliation. NULL until * reconciliation completes. Once set, it is the source of truth for the * physical bucket — reads never reconstruct the name. */ @@ -179,11 +179,11 @@ export interface S3Config { client: S3Client; /** S3 bucket name (the actual S3 bucket, not the logical bucket key) */ bucket: string; - /** S3 endpoint URL (for MinIO/custom S3) */ + /** S3 endpoint URL (for RustFS, MinIO, or custom S3) */ endpoint?: string; /** S3 region */ region?: string; - /** Whether to use path-style URLs (required for MinIO) */ + /** Whether to use path-style URLs (required for path-style S3-compatible storage) */ forcePathStyle?: boolean; /** Public URL prefix for generating download URLs */ publicUrlPrefix?: string; diff --git a/graphile/graphile-settings/src/upload-resolver.ts b/graphile/graphile-settings/src/upload-resolver.ts index 6cfe7d6847..e9781231c1 100644 --- a/graphile/graphile-settings/src/upload-resolver.ts +++ b/graphile/graphile-settings/src/upload-resolver.ts @@ -20,9 +20,10 @@ * ENV VARS (S3 connection only): * BUCKET_PROVIDER - 'minio' | 's3' (default: 'minio') * AWS_REGION - AWS region (default: 'us-east-1') - * AWS_ACCESS_KEY - access key (default: 'minioadmin') - * AWS_SECRET_KEY - secret key (default: 'minioadmin') - * CDN_ENDPOINT - S3-compatible endpoint (default: 'http://localhost:9000') + * Defaults come from `pgpmDefaults.cdn` (dev-only values; set these in production). + * AWS_ACCESS_KEY - access key + * AWS_SECRET_KEY - secret key + * CDN_ENDPOINT - S3-compatible endpoint */ import { getEnvOptions } from '@constructive-io/graphql-env'; @@ -61,21 +62,21 @@ let streamer: Streamer | null = null; function getStreamer(): Streamer { if (streamer) return streamer; - const { cdn = {} } = getEnvOptions(); + const { cdn } = getEnvOptions(); if (process.env.NODE_ENV === 'production' && (!cdn.awsAccessKey || !cdn.awsSecretKey)) { - log.warn('[upload-resolver] WARNING: Using default credentials in production.'); + log.warn('[upload-resolver] WARNING: CDN credentials not configured in production.'); } - const provider = cdn.provider || 'minio'; + const provider = cdn.provider; log.info(`[upload-resolver] Initializing: provider=${provider}`); streamer = new Streamer({ provider, - awsRegion: cdn.awsRegion || 'us-east-1', - awsAccessKey: cdn.awsAccessKey || 'minioadmin', - awsSecretKey: cdn.awsSecretKey || 'minioadmin', - endpoint: cdn.endpoint || 'http://localhost:9000', + awsRegion: cdn.awsRegion, + awsAccessKey: cdn.awsAccessKey, + awsSecretKey: cdn.awsSecretKey, + endpoint: cdn.endpoint, }); return streamer; diff --git a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap index 6383de2044..796744fd63 100644 --- a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap +++ b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap @@ -17,9 +17,9 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and "routingSchema": "routing_public", }, "cdn": { - "awsAccessKey": "minioadmin", + "awsAccessKey": "constructive", "awsRegion": "us-east-1", - "awsSecretKey": "minioadmin", + "awsSecretKey": "constructive-dev-secret", "bucketName": "test-bucket", "endpoint": "http://localhost:9000", "provider": "minio", diff --git a/graphql/server-test/__tests__/db-scope-upload.integration.test.ts b/graphql/server-test/__tests__/db-scope-upload.integration.test.ts index afe417ea00..206bbd3561 100644 --- a/graphql/server-test/__tests__/db-scope-upload.integration.test.ts +++ b/graphql/server-test/__tests__/db-scope-upload.integration.test.ts @@ -7,10 +7,10 @@ * (`buckets`/`files`), because the plane *is* that database's storage. Both must * produce an upload surface, so this pins the two together: one schema build over * both planes has to expose `uploadAppFile` AND `uploadFile`, and the unprefixed - * one has to work end to end — presigned PUT to MinIO, and a physical bucket + * one has to work end to end — presigned PUT to RustFS, and a physical bucket * recorded on the tenant's own bucket row. * - * Uses real MinIO (the `minio_cdn` service in CI, localhost:9000 locally). + * Uses real RustFS (the `rustfs_cdn` service in CI, localhost:9000 locally). * * pnpm test -- --testPathPattern=db-scope-upload */ @@ -103,8 +103,8 @@ describe('database-scope upload surface', () => { provider: 'minio', region: 'us-east-1', endpoint: 'http://localhost:9000', - accessKeyId: 'minioadmin', - secretAccessKey: 'minioadmin' + accessKeyId: 'constructive', + secretAccessKey: 'constructive-dev-secret' }); const post = ( @@ -170,7 +170,7 @@ describe('database-scope upload surface', () => { let contentHash: string; let uploadUrl: string; - // MinIO uses path-style URLs: http://host:9000//?... + // RustFS uses path-style URLs: http://host:9000//?... const bucketFromPresignedUrl = (url: string): string => new URL(url).pathname.replace(/^\/+/, '').split('/')[0]; diff --git a/graphql/server-test/__tests__/upload.integration.test.ts b/graphql/server-test/__tests__/upload.integration.test.ts index b07a235bb5..013190a9b1 100644 --- a/graphql/server-test/__tests__/upload.integration.test.ts +++ b/graphql/server-test/__tests__/upload.integration.test.ts @@ -4,7 +4,7 @@ * Exercises the file-centric upload pipeline: * uploadAppFile mutation -> presigned PUT URL -> PUT to S3 * - * Uses real MinIO (available in CI as minio_cdn service) and reconciled + * Uses real RustFS (available in CI as rustfs_cdn service) and reconciled * physical bucket fixtures. * * Three actors (single beforeAll, single server -- stays fast): @@ -284,8 +284,8 @@ describe('Integration tests (uploads, tenant isolation, RLS)', () => { provider: 'minio', region: 'us-east-1', endpoint: 'http://localhost:9000', - accessKeyId: 'minioadmin', - secretAccessKey: 'minioadmin' + accessKeyId: 'constructive', + secretAccessKey: 'constructive-dev-secret' }); const ensureS3Buckets = async (bucketNames: string[]): Promise => { @@ -506,7 +506,7 @@ describe('Integration tests (uploads, tenant isolation, RLS)', () => { return res.rows[0]?.physical_name ?? null; }; - // MinIO uses path-style URLs: http://host:9000//?... + // RustFS uses path-style URLs: http://host:9000//?... const bucketFromPresignedUrl = (url: string): string => new URL(url).pathname.replace(/^\/+/, '').split('/')[0]; diff --git a/graphql/server/src/scripts/create-bucket.ts b/graphql/server/src/scripts/create-bucket.ts index 6975ff4894..e08fe36e55 100644 --- a/graphql/server/src/scripts/create-bucket.ts +++ b/graphql/server/src/scripts/create-bucket.ts @@ -1,4 +1,4 @@ -// Minimal script to create a bucket in MinIO/S3 using @constructive-io/s3-utils +// Minimal script to create a bucket in RustFS/S3 using @constructive-io/s3-utils import { getEnvOptions } from '@constructive-io/graphql-env'; import type { StorageProvider } from '@constructive-io/s3-utils'; @@ -12,12 +12,12 @@ const log = new Logger('create-bucket'); const opts = getEnvOptions(); const { cdn } = opts; - const provider = (cdn?.provider || 'minio') as StorageProvider; - const bucket = cdn?.bucketName || 'test-bucket'; - const region = cdn?.awsRegion || 'us-east-1'; - const accessKey = cdn?.awsAccessKey || 'minioadmin'; - const secretKey = cdn?.awsSecretKey || 'minioadmin'; - const endpoint = cdn?.endpoint || 'http://localhost:9000'; + const provider = cdn.provider as StorageProvider; + const bucket = cdn.bucketName; + const region = cdn.awsRegion; + const accessKey = cdn.awsAccessKey; + const secretKey = cdn.awsSecretKey; + const endpoint = cdn.endpoint; const client = createS3Client({ provider, diff --git a/packages/bucket-provisioner/README.md b/packages/bucket-provisioner/README.md index 5a4f7dffe0..5bf6b0581a 100644 --- a/packages/bucket-provisioner/README.md +++ b/packages/bucket-provisioner/README.md @@ -20,7 +20,7 @@ S3-compatible bucket provisioning library for the Constructive storage module. C - **CORS configuration** — Browser-compatible rules for presigned URL uploads - **Lifecycle rules** — Auto-cleanup for temp buckets (abandoned uploads) - **Versioning** — Optional S3 versioning for durability -- **Multi-provider** — Works with AWS S3, MinIO, Cloudflare R2, Google Cloud Storage, and DigitalOcean Spaces +- **Multi-provider** — Works with AWS S3, RustFS (or any S3-compatible store), Cloudflare R2, Google Cloud Storage, and DigitalOcean Spaces - **Inspect/audit** — Read back a bucket's current configuration for verification - **Typed errors** — Structured `ProvisionerError` with error codes for programmatic handling @@ -39,9 +39,9 @@ const provisioner = new BucketProvisioner({ connection: { provider: 'minio', region: 'us-east-1', - endpoint: 'http://minio:9000', - accessKeyId: 'minioadmin', - secretAccessKey: 'minioadmin', + endpoint: 'http://rustfs:9000', + accessKeyId: 'constructive', + secretAccessKey: 'constructive-dev-secret', }, allowedOrigins: ['https://app.example.com'], }); @@ -282,7 +282,7 @@ Error codes: | Provider | Endpoint Required | Path Style | Notes | |----------|------------------|------------|-------| | `s3` | No | Virtual-hosted | AWS default | -| `minio` | Yes | Path-style | Local development, self-hosted | +| `minio` | Yes | Path-style S3-compatible (RustFS, MinIO) | Local development, self-hosted | | `r2` | Yes | Path-style | Cloudflare R2 | | `gcs` | Yes | Path-style | GCS S3-compatible API | | `spaces` | Yes | Virtual-hosted | DigitalOcean Spaces | diff --git a/packages/bucket-provisioner/__tests__/provisioner.integration.test.ts b/packages/bucket-provisioner/__tests__/provisioner.integration.test.ts index 96699192a9..7ad78dedb1 100644 --- a/packages/bucket-provisioner/__tests__/provisioner.integration.test.ts +++ b/packages/bucket-provisioner/__tests__/provisioner.integration.test.ts @@ -1,5 +1,5 @@ /** - * Integration tests for BucketProvisioner against a real MinIO instance. + * Integration tests for BucketProvisioner against a real RustFS instance. * * These tests exercise the full provisioning pipeline end-to-end: * 1. provision() — create bucket, set policies, CORS, versioning, lifecycle @@ -7,37 +7,29 @@ * 3. updateCors() — change CORS rules on an existing bucket * 4. bucketExists() — verify bucket existence checks * - * Requires MinIO running on localhost:9000 (docker-compose or CI service). - * Skips gracefully when MinIO is not reachable. + * Requires RustFS running on localhost:9000 (docker-compose or CI service). + * Skips gracefully when RustFS is not reachable. * - * NOTE: MinIO free / edge-cicd does NOT support several S3 APIs: - * - PutBucketCors / GetBucketCors (paid AIStor feature) - * - PutPublicAccessBlock / GetPublicAccessBlock - * - PutBucketPolicy (may partially work) - * - PutBucketVersioning (edge-cicd) - * - PutBucketLifecycleConfiguration (edge-cicd) - * The provisioner gracefully degrades via error-code matching (XmlParseException, - * NotImplemented, etc.), so provision() and updateCors() succeed but these - * features are not actually applied on MinIO free. - * Tests verify the graceful degradation path and focus on APIs MinIO supports: - * bucket creation and bucket existence checks. + * RustFS supports the S3 APIs used by the provisioner. The tests focus on + * bucket creation and bucket existence checks in addition to the provisioning + * result and graceful error handling. */ import { BucketProvisioner } from '../src/provisioner'; import type { StorageConnectionConfig } from '../src/types'; import { ProvisionerError } from '../src/types'; -// --- MinIO config (matches CI env) --- +// --- RustFS config (matches CI env) --- -const MINIO_ENDPOINT = process.env.CDN_ENDPOINT || 'http://localhost:9000'; +const OBJECT_STORE_ENDPOINT = process.env.CDN_ENDPOINT || 'http://localhost:9000'; const AWS_REGION = process.env.AWS_REGION || 'us-east-1'; -const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY || 'minioadmin'; -const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY || 'minioadmin'; +const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY || 'constructive'; +const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY || 'constructive-dev-secret'; const connection: StorageConnectionConfig = { provider: 'minio', region: AWS_REGION, - endpoint: MINIO_ENDPOINT, + endpoint: OBJECT_STORE_ENDPOINT, accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY, }; @@ -54,11 +46,11 @@ function testBucketName(suffix: string): string { } /** - * Check if MinIO is reachable. Skips the entire suite if not. + * Check if RustFS is reachable. Skips the entire suite if not. */ -async function isMinioReachable(): Promise { +async function isObjectStoreReachable(): Promise { try { - const response = await fetch(`${MINIO_ENDPOINT}/minio/health/live`, { + const response = await fetch(`${OBJECT_STORE_ENDPOINT}/health`, { signal: AbortSignal.timeout(3000), }); return response.ok; @@ -68,28 +60,28 @@ async function isMinioReachable(): Promise { } // --- Conditional test runner --- -// If MinIO is not available, all tests in this file pass instantly (early return). +// If RustFS is not available, all tests in this file pass instantly (early return). -let minioAvailable = false; +let objectStoreAvailable = false; beforeAll(async () => { - minioAvailable = await isMinioReachable(); - if (!minioAvailable) { + objectStoreAvailable = await isObjectStoreReachable(); + if (!objectStoreAvailable) { // eslint-disable-next-line no-console console.warn( - 'MinIO not reachable at %s — skipping bucket-provisioner integration tests', - MINIO_ENDPOINT, + 'RustFS not reachable at %s — skipping bucket-provisioner integration tests', + OBJECT_STORE_ENDPOINT, ); } }); // --- Tests --- -describe('BucketProvisioner integration (MinIO)', () => { +describe('BucketProvisioner integration (RustFS)', () => { let provisioner: BucketProvisioner; beforeAll(() => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; provisioner = new BucketProvisioner({ connection, allowedOrigins: TEST_ORIGINS, @@ -100,7 +92,7 @@ describe('BucketProvisioner integration (MinIO)', () => { const bucketName = testBucketName('private'); it('should provision a private bucket successfully', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const result = await provisioner.provision({ bucketName, @@ -112,13 +104,12 @@ describe('BucketProvisioner integration (MinIO)', () => { expect(result.accessType).toBe('private'); expect(result.provider).toBe('minio'); expect(result.region).toBe(AWS_REGION); - expect(result.endpoint).toBe(MINIO_ENDPOINT); + expect(result.endpoint).toBe(OBJECT_STORE_ENDPOINT); expect(result.blockPublicAccess).toBe(true); expect(result.versioning).toBe(false); expect(result.publicUrlPrefix).toBeNull(); expect(result.lifecycleRules).toHaveLength(0); - // CORS rules are built and returned (intent), even though MinIO - // may not actually apply them (PutBucketCors unsupported on free) + // CORS rules are built and returned as part of the intended configuration. expect(result.corsRules).toHaveLength(1); expect(result.corsRules[0].allowedOrigins).toEqual(TEST_ORIGINS); expect(result.corsRules[0].allowedMethods).toContain('PUT'); @@ -127,21 +118,19 @@ describe('BucketProvisioner integration (MinIO)', () => { }); it('should be inspectable after provisioning', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const inspected = await provisioner.inspect(bucketName, 'private'); expect(inspected.bucketName).toBe(bucketName); expect(inspected.accessType).toBe('private'); expect(inspected.versioning).toBe(false); - // MinIO free doesn't support GetPublicAccessBlock — returns false - expect(inspected.blockPublicAccess).toBe(false); - // MinIO free doesn't support GetBucketCors — returns empty - expect(inspected.corsRules).toHaveLength(0); + expect(inspected.blockPublicAccess).toBe(true); + expect(inspected.corsRules).toHaveLength(1); }); it('should survive re-provisioning (idempotent)', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const result = await provisioner.provision({ bucketName, @@ -157,7 +146,7 @@ describe('BucketProvisioner integration (MinIO)', () => { const bucketName = testBucketName('public'); it('should provision a public bucket without error', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const result = await provisioner.provision({ bucketName, @@ -176,22 +165,21 @@ describe('BucketProvisioner integration (MinIO)', () => { }); it('should be inspectable after provisioning', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const inspected = await provisioner.inspect(bucketName, 'public'); expect(inspected.bucketName).toBe(bucketName); expect(inspected.accessType).toBe('public'); - // MinIO free doesn't support CORS/policy reads - expect(inspected.corsRules).toHaveLength(0); + expect(inspected.corsRules).toHaveLength(1); }); }); describe('provision — temp bucket', () => { const bucketName = testBucketName('temp'); - it('should provision a temp bucket (lifecycle rules gracefully skipped on MinIO)', async () => { - if (!minioAvailable) return; + it('should provision a temp bucket with lifecycle rules', async () => { + if (!objectStoreAvailable) return; const result = await provisioner.provision({ bucketName, @@ -202,30 +190,27 @@ describe('BucketProvisioner integration (MinIO)', () => { expect(result.accessType).toBe('temp'); expect(result.blockPublicAccess).toBe(true); expect(result.publicUrlPrefix).toBeNull(); - // provision() returns intended lifecycle rules even though MinIO can't apply them expect(result.lifecycleRules).toHaveLength(1); expect(result.lifecycleRules[0].id).toBe('temp-cleanup'); expect(result.lifecycleRules[0].expirationDays).toBe(1); expect(result.lifecycleRules[0].enabled).toBe(true); }); - it('should be inspectable (lifecycle not visible on MinIO free)', async () => { - if (!minioAvailable) return; + it('should be inspectable with lifecycle rules', async () => { + if (!objectStoreAvailable) return; const inspected = await provisioner.inspect(bucketName, 'temp'); expect(inspected.bucketName).toBe(bucketName); - // MinIO free doesn't support PutBucketLifecycleConfiguration — - // the rules were gracefully skipped, so inspect() returns empty - expect(inspected.lifecycleRules).toHaveLength(0); + expect(inspected.lifecycleRules).toHaveLength(1); }); }); describe('provision — versioning', () => { const bucketName = testBucketName('versioned'); - it('should provision with versioning flag (gracefully skipped on MinIO)', async () => { - if (!minioAvailable) return; + it('should provision with versioning flag', async () => { + if (!objectStoreAvailable) return; const result = await provisioner.provision({ bucketName, @@ -233,16 +218,14 @@ describe('BucketProvisioner integration (MinIO)', () => { versioning: true, }); - // provision() returns intended config even though MinIO can't apply versioning expect(result.versioning).toBe(true); }); - it('should report versioning state on inspect (not applied on MinIO)', async () => { - if (!minioAvailable) return; + it('should report versioning state on inspect', async () => { + if (!objectStoreAvailable) return; const inspected = await provisioner.inspect(bucketName, 'private'); - // MinIO free doesn't support PutBucketVersioning — gracefully skipped - expect(inspected.versioning).toBe(false); + expect(inspected.versioning).toBe(true); }); }); @@ -251,7 +234,7 @@ describe('BucketProvisioner integration (MinIO)', () => { const customOrigins = ['https://custom.example.com', 'https://other.example.com']; it('should accept per-bucket allowedOrigins (returned in provision result)', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const result = await provisioner.provision({ bucketName, @@ -264,12 +247,11 @@ describe('BucketProvisioner integration (MinIO)', () => { expect(result.corsRules[0].allowedOrigins).toEqual(customOrigins); }); - it('should be inspectable (CORS not visible on MinIO free)', async () => { - if (!minioAvailable) return; + it('should be inspectable with CORS rules', async () => { + if (!objectStoreAvailable) return; const inspected = await provisioner.inspect(bucketName, 'private'); - // MinIO free doesn't support GetBucketCors - expect(inspected.corsRules).toHaveLength(0); + expect(inspected.corsRules).toHaveLength(1); }); }); @@ -277,15 +259,15 @@ describe('BucketProvisioner integration (MinIO)', () => { const bucketName = testBucketName('cors-update'); beforeAll(async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; await provisioner.provision({ bucketName, accessType: 'private', }); }); - it('should return updated CORS rules (graceful degradation on MinIO)', async () => { - if (!minioAvailable) return; + it('should return updated CORS rules', async () => { + if (!objectStoreAvailable) return; const newOrigins = ['https://new-app.example.com']; const rules = await provisioner.updateCors({ @@ -294,7 +276,6 @@ describe('BucketProvisioner integration (MinIO)', () => { allowedOrigins: newOrigins, }); - // updateCors() returns the intended rules even on MinIO expect(rules).toHaveLength(1); expect(rules[0].allowedOrigins).toEqual(newOrigins); expect(rules[0].allowedMethods).toContain('PUT'); @@ -302,7 +283,7 @@ describe('BucketProvisioner integration (MinIO)', () => { }); it('should switch from private to public CORS methods on access type change', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const rules = await provisioner.updateCors({ bucketName, @@ -320,7 +301,7 @@ describe('BucketProvisioner integration (MinIO)', () => { const bucketName = testBucketName('exists-check'); beforeAll(async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; await provisioner.provision({ bucketName, accessType: 'private', @@ -328,14 +309,14 @@ describe('BucketProvisioner integration (MinIO)', () => { }); it('should return true for an existing bucket', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const exists = await provisioner.bucketExists(bucketName); expect(exists).toBe(true); }); it('should return false for a non-existent bucket', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; const exists = await provisioner.bucketExists('does-not-exist-' + RUN_ID); expect(exists).toBe(false); @@ -344,7 +325,7 @@ describe('BucketProvisioner integration (MinIO)', () => { describe('inspect — error handling', () => { it('should throw BUCKET_NOT_FOUND for non-existent bucket', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; await expect( provisioner.inspect('no-such-bucket-' + RUN_ID, 'private'), @@ -360,7 +341,7 @@ describe('BucketProvisioner integration (MinIO)', () => { const bucketName = testBucketName('roundtrip'); it('should complete the full workflow without error', async () => { - if (!minioAvailable) return; + if (!objectStoreAvailable) return; // 1. Provision a private bucket with versioning const provisionResult = await provisioner.provision({ @@ -374,14 +355,13 @@ describe('BucketProvisioner integration (MinIO)', () => { expect(provisionResult.versioning).toBe(true); expect(provisionResult.corsRules[0].allowedOrigins).toEqual(TEST_ORIGINS); - // 2. Inspect — versioning gracefully skipped on MinIO, CORS not readable + // 2. Inspect the applied versioning and CORS configuration. const inspected1 = await provisioner.inspect(bucketName, 'private'); expect(inspected1.bucketName).toBe(bucketName); - // MinIO can't apply versioning or CORS - expect(inspected1.versioning).toBe(false); - expect(inspected1.corsRules).toHaveLength(0); + expect(inspected1.versioning).toBe(true); + expect(inspected1.corsRules).toHaveLength(1); - // 3. Update CORS to new origins (graceful degradation on MinIO) + // 3. Update CORS to new origins. const newOrigins = ['https://staging.example.com']; const updatedRules = await provisioner.updateCors({ bucketName, diff --git a/packages/bucket-provisioner/__tests__/provisioner.test.ts b/packages/bucket-provisioner/__tests__/provisioner.test.ts index d4e6207ca4..27701de862 100644 --- a/packages/bucket-provisioner/__tests__/provisioner.test.ts +++ b/packages/bucket-provisioner/__tests__/provisioner.test.ts @@ -28,8 +28,8 @@ const defaultOptions: BucketProvisionerOptions = { provider: 'minio', region: 'us-east-1', endpoint: 'http://minio:9000', - accessKeyId: 'minioadmin', - secretAccessKey: 'minioadmin', + accessKeyId: 'constructive', + secretAccessKey: 'constructive-dev-secret', }, allowedOrigins: ['https://app.example.com'], }; diff --git a/packages/bucket-provisioner/src/index.ts b/packages/bucket-provisioner/src/index.ts index 2b91125959..51b1242867 100644 --- a/packages/bucket-provisioner/src/index.ts +++ b/packages/bucket-provisioner/src/index.ts @@ -13,9 +13,9 @@ * connection: { * provider: 'minio', * region: 'us-east-1', - * endpoint: 'http://minio:9000', - * accessKeyId: 'minioadmin', - * secretAccessKey: 'minioadmin', + * endpoint: 'http://rustfs:9000', + * accessKeyId: 'constructive', + * secretAccessKey: 'constructive-dev-secret', * }, * allowedOrigins: ['https://app.example.com'], * }); diff --git a/packages/bucket-provisioner/src/provisioner.ts b/packages/bucket-provisioner/src/provisioner.ts index ccd78ff1b6..54b42d49f8 100644 --- a/packages/bucket-provisioner/src/provisioner.ts +++ b/packages/bucket-provisioner/src/provisioner.ts @@ -3,7 +3,7 @@ * * Orchestrates S3 bucket creation, privacy configuration, CORS setup, * versioning, and lifecycle rules. Uses the AWS SDK S3 client for all - * operations, which works with any S3-compatible backend (MinIO, R2, etc.). + * operations, which works with any S3-compatible backend (RustFS, MinIO, R2, etc.). * * Privacy model: * - Private/temp buckets: Block All Public Access, no bucket policy, presigned URLs only @@ -73,9 +73,9 @@ export interface BucketProvisionerOptions { * connection: { * provider: 'minio', * region: 'us-east-1', - * endpoint: 'http://minio:9000', - * accessKeyId: 'minioadmin', - * secretAccessKey: 'minioadmin', + * endpoint: 'http://rustfs:9000', + * accessKeyId: 'constructive', + * secretAccessKey: 'constructive-dev-secret', * }, * allowedOrigins: ['https://app.example.com'], * }); diff --git a/packages/bucket-provisioner/src/types.ts b/packages/bucket-provisioner/src/types.ts index 8a976d4ec6..efb2d78f3e 100644 --- a/packages/bucket-provisioner/src/types.ts +++ b/packages/bucket-provisioner/src/types.ts @@ -10,7 +10,7 @@ /** * Supported storage provider identifiers. * - * Used to select provider-specific behavior (e.g., path-style URLs for MinIO, + * Used to select provider-specific behavior (e.g., path-style URLs for RustFS or MinIO, * jurisdiction headers for R2). */ export type StorageProvider = 's3' | 'minio' | 'rustfs' | 'r2' | 'gcs' | 'spaces'; @@ -20,20 +20,20 @@ export type StorageProvider = 's3' | 'minio' | 'rustfs' | 'r2' | 'gcs' | 'spaces * * This is the input you provide to connect to your storage provider. * For AWS S3, only `region` and credentials are needed. - * For MinIO/RustFS/R2/etc., also provide `endpoint`. + * For RustFS/MinIO/R2/etc., also provide `endpoint`. */ export interface StorageConnectionConfig { /** Storage provider type */ provider: StorageProvider; /** S3 region (e.g., "us-east-1"). Required for AWS S3. */ region: string; - /** S3-compatible endpoint URL (e.g., "http://minio:9000"). Required for non-AWS providers. */ + /** S3-compatible endpoint URL (e.g., "http://rustfs:9000"). Required for non-AWS providers. */ endpoint?: string; /** AWS access key ID */ accessKeyId: string; /** AWS secret access key */ secretAccessKey: string; - /** Use path-style URLs (required for MinIO, optional for others) */ + /** Use path-style URLs (required for RustFS and MinIO, optional for others) */ forcePathStyle?: boolean; } diff --git a/pgpm/cli/src/commands/docker.ts b/pgpm/cli/src/commands/docker.ts index d42f08607c..dda97d8856 100644 --- a/pgpm/cli/src/commands/docker.ts +++ b/pgpm/cli/src/commands/docker.ts @@ -25,7 +25,6 @@ PostgreSQL Options: --shm-size Shared memory size for container (default: 2g) Additional Services: - --minio Include MinIO S3-compatible object storage (API: 9000, Console: 9001) --rustfs Include RustFS S3-compatible object storage (API: 9000, Console: 9001) --ollama Include Ollama LLM inference server (API: 11434) --gpu Enable NVIDIA GPU passthrough for Ollama (requires NVIDIA Container Toolkit) @@ -36,16 +35,13 @@ General Options: Examples: pgpm docker start Start PostgreSQL only - pgpm docker start --minio Start PostgreSQL + MinIO pgpm docker start --rustfs Start PostgreSQL + RustFS pgpm docker start --ollama Start PostgreSQL + Ollama (CPU) pgpm docker start --ollama --gpu Start PostgreSQL + Ollama (NVIDIA GPU) pgpm docker start --port 5433 Start on custom port pgpm docker start --shm-size 4g Start with 4GB shared memory pgpm docker start --recreate Remove and recreate containers - pgpm docker start --recreate --minio Recreate PostgreSQL + MinIO pgpm docker stop Stop PostgreSQL - pgpm docker stop --minio Stop PostgreSQL + MinIO pgpm docker stop --rustfs Stop PostgreSQL + RustFS pgpm docker stop --ollama Stop PostgreSQL + Ollama pgpm docker ls List services and status @@ -82,30 +78,16 @@ interface ServiceDefinition { } const ADDITIONAL_SERVICES: Record = { - minio: { - name: 'minio', - image: 'minio/minio', - ports: [ - { host: 9000, container: 9000 }, - { host: 9001, container: 9001 } - ], - env: { - MINIO_ROOT_USER: 'minioadmin', - MINIO_ROOT_PASSWORD: 'minioadmin' - }, - command: ['server', '/data', '--console-address', ':9001'], - volumes: [{ name: 'minio-data', containerPath: '/data' }] - }, rustfs: { name: 'rustfs', - image: 'rustfs/rustfs', + image: 'rustfs/rustfs:1.0.0-rc.5', ports: [ { host: 9000, container: 9000 }, { host: 9001, container: 9001 } ], env: { - RUSTFS_ACCESS_KEY: 'minioadmin', - RUSTFS_SECRET_KEY: 'minioadmin', + RUSTFS_ACCESS_KEY: 'constructive', + RUSTFS_SECRET_KEY: 'constructive-dev-secret', RUSTFS_ADDRESS: ':9000', RUSTFS_CONSOLE_ADDRESS: ':9001', RUSTFS_CONSOLE_ENABLE: 'true' diff --git a/pgpm/cli/src/commands/env.ts b/pgpm/cli/src/commands/env.ts index f75b8b45ba..d765687967 100644 --- a/pgpm/cli/src/commands/env.ts +++ b/pgpm/cli/src/commands/env.ts @@ -14,8 +14,7 @@ Database Profiles: --supabase Use Supabase local development profile Additional Services: - --minio Include MinIO/S3 environment variables - --rustfs Include RustFS/S3 environment variables (same vars as --minio) + --rustfs Include RustFS/S3 environment variables Modes: No command Print export statements for shell evaluation @@ -24,20 +23,15 @@ Modes: Options: --help, -h Show this help message --supabase Use Supabase profile instead of default Postgres - --minio Include CDN_ENDPOINT, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_REGION - --rustfs Alias for --minio (RustFS serves the same S3 API on :9000) + --rustfs Include CDN_ENDPOINT, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_REGION Examples: pgpm env Print default Postgres env exports pgpm env --supabase Print Supabase env exports - pgpm env --minio Print Postgres + MinIO env exports pgpm env --rustfs Print Postgres + RustFS env exports - pgpm env --supabase --minio Print Supabase + MinIO env exports eval "$(pgpm env)" Load default Postgres env into shell - eval "$(pgpm env --minio)" Load Postgres + MinIO env into shell - eval "$(pgpm env --supabase --minio)" Load Supabase + MinIO env into shell - pgpm env pgpm deploy --database db1 Run command with default Postgres env - pgpm env --minio pgpm deploy --database db1 Run command with Postgres + MinIO env + eval "$(pgpm env --rustfs)" Load Postgres + RustFS env into shell + pgpm env --rustfs pgpm deploy --database db1 Run command with Postgres + RustFS env `; const SUPABASE_PROFILE: PgConfig = { @@ -61,8 +55,8 @@ interface ObjectStoreConfig { const OBJECT_STORE_PROFILE: ObjectStoreConfig = { endpoint: 'http://localhost:9000', - accessKey: 'minioadmin', - secretKey: 'minioadmin', + accessKey: 'constructive', + secretKey: 'constructive-dev-secret', region: 'us-east-1', }; diff --git a/pgpm/cli/src/utils/display.ts b/pgpm/cli/src/utils/display.ts index 1791b531eb..154ea48252 100644 --- a/pgpm/cli/src/utils/display.ts +++ b/pgpm/cli/src/utils/display.ts @@ -46,9 +46,9 @@ export const usageText = ` deps Show change dependencies Development Tools: - docker Manage Docker containers (start/stop/ls, --minio) + docker Manage Docker containers (start/stop/ls, --rustfs) doctor Check local dependencies (node, docker, psql) with install guidance - env Manage environment variables (--supabase, --minio) + env Manage environment variables (--supabase, --rustfs) test-packages Run integration tests on workspace packages Global Options: diff --git a/pgpm/env/__tests__/__snapshots__/merge.test.ts.snap b/pgpm/env/__tests__/__snapshots__/merge.test.ts.snap index 46f89bc304..3d7f12db7e 100644 --- a/pgpm/env/__tests__/__snapshots__/merge.test.ts.snap +++ b/pgpm/env/__tests__/__snapshots__/merge.test.ts.snap @@ -3,9 +3,9 @@ exports[`getEnvOptions merges defaults, config, env, and overrides 1`] = ` { "cdn": { - "awsAccessKey": "minioadmin", + "awsAccessKey": "constructive", "awsRegion": "us-east-1", - "awsSecretKey": "minioadmin", + "awsSecretKey": "constructive-dev-secret", "bucketName": "test-bucket", "endpoint": "http://localhost:9000", "provider": "minio", diff --git a/pgpm/env/__tests__/assert.test.ts b/pgpm/env/__tests__/assert.test.ts index e4648f6204..82e36719e9 100644 --- a/pgpm/env/__tests__/assert.test.ts +++ b/pgpm/env/__tests__/assert.test.ts @@ -47,7 +47,7 @@ describe('findUnsafeProductionDefaults', () => { // actual secret/host strings baked into pgpmDefaults). expect(joined).not.toContain('app_password'); expect(joined).not.toContain('admin_password'); - expect(joined).not.toContain('minioadmin'); + expect(joined).not.toContain('constructive-dev-secret'); expect(joined).not.toContain('localhost'); expect(joined).not.toContain('test-bucket'); }); diff --git a/pgpm/env/src/assert.ts b/pgpm/env/src/assert.ts index efdfb4683b..82fc36efbe 100644 --- a/pgpm/env/src/assert.ts +++ b/pgpm/env/src/assert.ts @@ -6,7 +6,7 @@ import { getStrictEnvMode, isProduction } from '12factor-env'; * * `pgpmDefaults` bakes in development-only values so local dev and tests work * out of the box (e.g. `pg.password = 'password'`, `cdn.awsAccessKey = - * 'minioadmin'`, `pg.host = 'localhost'`). Those are a liability in production: + * 'constructive'`, `pg.host = 'localhost'`). Those are a liability in production: * a deploy that forgets to set the real value boots on the dev default instead * of failing. `deepmerge` cannot express "dev default, required in prod", so * this is enforced here as an opt-in assertion callers run at startup. diff --git a/pgpm/types/src/pgpm.ts b/pgpm/types/src/pgpm.ts index 6ef041eefc..12dce4b7b3 100644 --- a/pgpm/types/src/pgpm.ts +++ b/pgpm/types/src/pgpm.ts @@ -115,7 +115,7 @@ export type BucketProvider = 's3' | 'minio' | 'rustfs' | 'gcs'; * CDN and file storage configuration */ export interface CDNOptions { - /** Storage provider type (s3, minio, rustfs, gcs). Defaults to 'minio' for local dev */ + /** Storage provider type (s3, minio, rustfs, gcs). 'minio' means path-style S3-compatible (RustFS, MinIO) */ provider?: BucketProvider; /** S3 bucket name for file storage */ bucketName?: string; @@ -125,7 +125,7 @@ export interface CDNOptions { awsAccessKey?: string; /** AWS secret key for S3 */ awsSecretKey?: string; - /** S3-compatible API endpoint URL (MinIO, R2, DO Spaces, GCS, etc.) */ + /** S3-compatible API endpoint URL (RustFS, MinIO, R2, DO Spaces, GCS, etc.) */ endpoint?: string; /** Public URL prefix for generating download URLs (e.g., CDN domain, S3 public URL) */ publicUrlPrefix?: string; @@ -373,8 +373,8 @@ export const pgpmDefaults: PgpmOptions = { provider: 'minio', bucketName: 'test-bucket', awsRegion: 'us-east-1', - awsAccessKey: 'minioadmin', - awsSecretKey: 'minioadmin', + awsAccessKey: 'constructive', + awsSecretKey: 'constructive-dev-secret', endpoint: 'http://localhost:9000', publicUrlPrefix: 'http://localhost:9000' }, diff --git a/uploads/s3-utils/README.md b/uploads/s3-utils/README.md index 0d63def29b..e37f454658 100644 --- a/uploads/s3-utils/README.md +++ b/uploads/s3-utils/README.md @@ -16,7 +16,7 @@ Unified S3 utilities for the Constructive ecosystem — client factory, file ope ## Features -- **Multi-provider support** — AWS S3, MinIO, RustFS, Cloudflare R2, Google Cloud Storage, DigitalOcean Spaces +- **Multi-provider support** — AWS S3, RustFS (or any S3-compatible store), MinIO, Cloudflare R2, Google Cloud Storage, DigitalOcean Spaces - **Presigned URLs** — generate secure PUT and GET URLs with configurable expiry - **File operations** — streaming upload, download, existence checks, and metadata retrieval - **Bucket management** — create buckets with provider-appropriate policies and CORS @@ -43,9 +43,9 @@ import { const client = createS3Client({ provider: 'minio', region: 'us-east-1', - endpoint: 'http://minio:9000', - accessKeyId: 'minioadmin', - secretAccessKey: 'minioadmin', + endpoint: 'http://rustfs:9000', + accessKeyId: 'constructive', + secretAccessKey: 'constructive-dev-secret', }); // 2. Upload a file via stream @@ -198,7 +198,7 @@ someReadable.pipe(pass); Creates a bucket with provider-appropriate policies and CORS configuration. -- **MinIO**: read-only public access policy (list + get) +- **Path-style S3-compatible (`minio`, covering RustFS and MinIO)**: read-only public access policy (list + get) - **RustFS/S3/GCS**: full-access policy with CORS rules for browser uploads ```typescript diff --git a/uploads/s3-utils/src/client.ts b/uploads/s3-utils/src/client.ts index fefcd3a8f0..c41c4f01c2 100644 --- a/uploads/s3-utils/src/client.ts +++ b/uploads/s3-utils/src/client.ts @@ -6,7 +6,7 @@ * instead of wiring up @aws-sdk/client-s3 directly. * * Handles provider-specific defaults: - * - minio: forces path-style URLs, requires endpoint + * - minio: forces path-style URLs, requires endpoint (RustFS or MinIO) * - rustfs: forces path-style URLs, requires endpoint * - r2: forces path-style URLs, requires endpoint * - gcs: forces path-style URLs, requires endpoint @@ -20,9 +20,9 @@ * const client = createS3Client({ * provider: 'minio', * region: 'us-east-1', - * endpoint: 'http://minio:9000', - * accessKeyId: 'minioadmin', - * secretAccessKey: 'minioadmin', + * endpoint: 'http://rustfs:9000', + * accessKeyId: 'constructive', + * secretAccessKey: 'constructive-dev-secret', * }); * ``` */ @@ -34,7 +34,7 @@ import { S3Client } from '@aws-sdk/client-s3'; /** * Supported storage provider identifiers. * - * Used to select provider-specific behavior (e.g., path-style URLs for MinIO, + * Used to select provider-specific behavior (e.g., path-style URLs for RustFS or MinIO, * jurisdiction headers for R2). */ export type StorageProvider = 's3' | 'minio' | 'rustfs' | 'r2' | 'gcs' | 'spaces'; @@ -44,20 +44,20 @@ export type StorageProvider = 's3' | 'minio' | 'rustfs' | 'r2' | 'gcs' | 'spaces * * This is the input you provide to connect to your storage provider. * For AWS S3, only `region` and credentials are needed. - * For MinIO/RustFS/R2/etc., also provide `endpoint`. + * For RustFS/MinIO/R2/etc., also provide `endpoint`. */ export interface StorageConnectionConfig { /** Storage provider type */ provider: StorageProvider; /** S3 region (e.g., "us-east-1"). Required for AWS S3. */ region: string; - /** S3-compatible endpoint URL (e.g., "http://minio:9000"). Required for non-AWS providers. */ + /** S3-compatible endpoint URL (e.g., "http://rustfs:9000"). Required for non-AWS providers. */ endpoint?: string; /** AWS access key ID */ accessKeyId: string; /** AWS secret access key */ secretAccessKey: string; - /** Use path-style URLs (required for MinIO, optional for others) */ + /** Use path-style URLs (required for RustFS and MinIO, optional for others) */ forcePathStyle?: boolean; } @@ -82,7 +82,7 @@ export class S3ConfigError extends Error { * Create an S3Client from a storage connection config. * * Provider-specific defaults: - * - `minio`: forces path-style URLs (required by MinIO) + * - `minio`: path-style S3-compatible storage (RustFS or MinIO) * - `rustfs`: forces path-style URLs * - `r2`: forces path-style URLs (required by Cloudflare R2) * - `s3`: uses virtual-hosted style (AWS default)