From ee40e0e83eda94b91892eb8ab89ea2145817e7e1 Mon Sep 17 00:00:00 2001 From: iammukeshm Date: Fri, 25 Sep 2026 07:06:33 +0530 Subject: [PATCH] fix(infra): replace MinIO with RustFS for local and test object storage MinIO images are no longer pullable: minio/minio and minio/mc were removed from Docker Hub, and quay.io/minio/* now refuses anonymous pulls. That broke every Testcontainers integration test, the Aspire AppHost and the compose deployment on a clean machine. - Tests: generic Testcontainers container on rustfs/rustfs:1.0.0 (drops Testcontainers.Minio for the base Testcontainers package). - AppHost: RustFS container with CORS for both dev origins; bucket bootstrap moves from minio/mc to amazon/aws-cli (create + public-read GetObject policy). - Compose: rustfs + rustfs-init services; MINIO_ROOT_USER/PASSWORD become RUSTFS_ACCESS_KEY/RUSTFS_SECRET_KEY; volume minio_data -> rustfs_data. - fsh CLI generates the new env keys; docs/rules updated. Co-Authored-By: Claude Opus 5.5 (1M context) --- .agents/rules/architecture.md | 2 +- .agents/rules/integration-testing.md | 6 +- .agents/rules/storage.md | 6 +- .agents/rules/testing.md | 2 +- .agents/skills/testing-guide/SKILL.md | 4 +- AGENTS.md | 6 +- README-template.md | 4 +- README.md | 12 ++-- clients/admin/README.md | 2 +- .../admin/src/components/file/image-input.tsx | 2 +- clients/admin/src/hooks/use-file-upload.ts | 2 +- clients/dashboard/README.md | 2 +- .../src/components/file/image-input.tsx | 2 +- .../dashboard/src/hooks/use-file-upload.ts | 2 +- deploy/docker/.env.example | 6 +- deploy/docker/README.md | 8 +-- deploy/docker/docker-compose.yml | 47 +++++++-------- src/Directory.Packages.props | 3 +- src/Host/FSH.Starter.AppHost/AppHost.cs | 57 +++++++++--------- .../v1/Queries/GetFileDownloadUrlQuery.cs | 2 +- .../MiddlewareWebApplicationFactory.cs | 47 ++++++++------- .../Integration.Middleware.Tests.csproj | 2 +- .../FshWebApplicationFactory.cs | 59 ++++++++++--------- .../Integration.Tests.csproj | 2 +- .../Tests/Files/PurgeJobsTests.cs | 2 +- .../Files/RequestAndFinalizeUploadTests.cs | 2 +- .../Tests/Files/StorageFlowTests.cs | 2 +- src/Tools/CLI/Commands/NewCommand.cs | 4 +- 28 files changed, 154 insertions(+), 143 deletions(-) diff --git a/.agents/rules/architecture.md b/.agents/rules/architecture.md index cf3fa6b6ef..f74b68e860 100644 --- a/.agents/rules/architecture.md +++ b/.agents/rules/architecture.md @@ -95,5 +95,5 @@ No global mutable static collections enumerated under concurrency. `Audit` (Audi - `appsettings.json` (+ `.Development`/`.Production`) live in `src/Host/FSH.Starter.Api/`. DbMigrator links the same files. - Bind config with the Options pattern: `AddOptions().BindConfiguration(nameof(T))`, section name == type name (e.g. `JwtOptions`, `DatabaseOptions`, `CachingOptions`, `CorsOptions`, `QuotaOptions`, `RateLimitingOptions`; **storage section is `Storage`**, not `StorageOptions`). Add `.ValidateDataAnnotations().ValidateOnStart()` for fail-fast. - Validate critical options via `IValidatableObject` — `JwtOptions` requires `SigningKey` ≥32 chars and **rejects placeholder strings containing `"replace-with"`**; `DatabaseOptions` rejects empty connection strings. -- **Production fail-fast** (`Program.cs`, before service registration): missing `DatabaseOptions:ConnectionString`, `CachingOptions:Redis`, or `JwtOptions:SigningKey` throws. Dev secrets via `dotnet user-secrets` (AppHost has a `UserSecretsId`); MinIO creds are Aspire secret parameters. +- **Production fail-fast** (`Program.cs`, before service registration): missing `DatabaseOptions:ConnectionString`, `CachingOptions:Redis`, or `JwtOptions:SigningKey` throws. Dev secrets via `dotnet user-secrets` (AppHost has a `UserSecretsId`); RustFS (S3) creds are Aspire secret parameters. - Platform composition is one call each: `builder.AddHeroPlatform(o => { o.Enable... })` (DI) and `app.UseHeroPlatform(...)` (middleware). Feature flags toggle Caching/Jobs/Mailing/Quotas/Sse/Realtime/OpenTelemetry/CORS/Idempotency. diff --git a/.agents/rules/integration-testing.md b/.agents/rules/integration-testing.md index 63d0b7a264..6f980f9646 100644 --- a/.agents/rules/integration-testing.md +++ b/.agents/rules/integration-testing.md @@ -4,14 +4,14 @@ ## Harness -`WebApplicationFactory` over **real** infra via Testcontainers — PostgreSQL + Redis + MinIO. **Docker must be running**; if it isn't, tests fail fast with `DockerUnavailableException` (environmental, not a regression — run the unit projects instead). +`WebApplicationFactory` over **real** infra via Testcontainers — PostgreSQL + Redis + RustFS (S3-compatible, generic container). **Docker must be running**; if it isn't, tests fail fast with `DockerUnavailableException` (environmental, not a regression — run the unit projects instead). -`FshWebApplicationFactory` (`Integration.Tests/Infrastructure/`) boots the containers, overlays in-memory config, swaps `IMailService` → `NoOpMailService`, and rewires storage to MinIO. +`FshWebApplicationFactory` (`Integration.Tests/Infrastructure/`) boots the containers, overlays in-memory config, swaps `IMailService` → `NoOpMailService`, and rewires storage to RustFS. ## Must-know gotchas - **Tenant context is AsyncLocal — set it inline.** Set the Finbuckle tenant context **in the same method** as the `UserManager`/`DbContext` call. Setting it in an awaited helper loses it across the async boundary → NRE in the tenant query filter. -- **Storage is wired eagerly.** `AddHeroStorage` reads `Storage:Provider` before the test config overlay, so it picks `LocalStorageService`. The factory **removes the `IStorageService`/`LocalStorageService`/`S3StorageService` descriptors post-registration and re-registers the S3 stack** at MinIO. Follow that when a test needs real object storage. (See `storage.md`.) +- **Storage is wired eagerly.** `AddHeroStorage` reads `Storage:Provider` before the test config overlay, so it picks `LocalStorageService`. The factory **removes the `IStorageService`/`LocalStorageService`/`S3StorageService` descriptors post-registration and re-registers the S3 stack** at RustFS. Follow that when a test needs real object storage. (See `storage.md`.) - **SignalR tests force long-polling** — TestServer has no WebSocket. Configure the client transport accordingly. - **Rate limiting is read eagerly** — `Integration.Middleware.Tests` sets `RateLimitingOptions:Enabled` via env var **before** host build, since flipping it after has no effect. diff --git a/.agents/rules/storage.md b/.agents/rules/storage.md index edb483c9ce..00733cf4f4 100644 --- a/.agents/rules/storage.md +++ b/.agents/rules/storage.md @@ -10,7 +10,7 @@ ## Providers -`AddHeroStorage(config)` reads `Storage:Provider` **eagerly at registration**: `"s3"` → `S3StorageService` (supports MinIO via `ServiceUrl` + `ForcePathStyle`), else `LocalStorageService`. When quotas are enabled the service is wrapped in `QuotaMeteredStorageService` (debits `StorageBytes`). +`AddHeroStorage(config)` reads `Storage:Provider` **eagerly at registration**: `"s3"` → `S3StorageService` (supports any S3-compatible store, e.g. RustFS, via `ServiceUrl` + `ForcePathStyle`), else `LocalStorageService`. When quotas are enabled the service is wrapped in `QuotaMeteredStorageService` (debits `StorageBytes`). ## Presigned upload flow (preferred for user uploads) @@ -19,8 +19,8 @@ Don't stream large files through the API. The pattern (see Files module): 2. Client uploads **directly** to storage. 3. `FinalizeUpload` — flips to `Available`, **debits the quota here** (not at request time), publishes `FileFinalizedIntegrationEvent`. -Local/dev without MinIO uses `LocalPresignTokenStore` (in-memory one-shot tokens). +Local/dev without an S3 store uses `LocalPresignTokenStore` (in-memory one-shot tokens). ## Test gotcha -`AddHeroStorage` reads `Storage:Provider` **before** a test factory's in-memory config overlay applies, so it wires `LocalStorageService`. Integration tests that need MinIO must **remove the `IStorageService`/`LocalStorageService`/`S3StorageService` descriptors post-registration and re-register the S3 stack** pointed at the MinIO container (see `FshWebApplicationFactory`). See `integration-testing.md`. +`AddHeroStorage` reads `Storage:Provider` **before** a test factory's in-memory config overlay applies, so it wires `LocalStorageService`. Integration tests that need object storage must **remove the `IStorageService`/`LocalStorageService`/`S3StorageService` descriptors post-registration and re-register the S3 stack** pointed at the RustFS container (see `FshWebApplicationFactory`). See `integration-testing.md`. diff --git a/.agents/rules/testing.md b/.agents/rules/testing.md index e63c1523e9..b19aebb128 100644 --- a/.agents/rules/testing.md +++ b/.agents/rules/testing.md @@ -19,7 +19,7 @@ xUnit · Shouldly (`result.ShouldBe(...)`) · NSubstitute (`Substitute.For Then open the **Aspire dashboard** at `https://localhost:15888`, the **API + Scalar docs** at `https://localhost:7030/scalar`, the **admin** app at `http://localhost:5173`, and the **dashboard** app at `http://localhost:5174`. Sign in with a seeded demo account (e.g. `admin@acme.com` / `Password123!`). @@ -43,7 +43,7 @@ dotnet run --project src/Host/MyApp.AppHost # 🎉 whole stack up: API + 2 Rea - **EF Core 10** on **PostgreSQL** (Npgsql), with domain events, the specification pattern, soft-delete + audit interceptors, and tenant-isolated `DbContext`s. - **JWT auth + ASP.NET Identity** — issuance/refresh, roles & fine-grained permissions, rate-limited auth, password policies, sessions, impersonation. - **Multitenancy** via [Finbuckle](https://www.finbuckle.com/) — tenant resolution, provisioning, per-tenant migrations & seeding, isolation enforced by default. -- **Cross-cutting**: HybridCache on **Valkey** (Redis-compatible), **Hangfire** jobs, presigned S3/**MinIO** storage, mailing, idempotency, quotas, rate limiting, API versioning, RFC 9457 `ProblemDetails`. +- **Cross-cutting**: HybridCache on **Valkey** (Redis-compatible), **Hangfire** jobs, presigned S3 storage (**RustFS** locally), mailing, idempotency, quotas, rate limiting, API versioning, RFC 9457 `ProblemDetails`. - **Observability**: Serilog structured logging + **OpenTelemetry** traces/metrics/logs, health probes, security/exception auditing. - **Docs**: **OpenAPI** + the **Scalar** API reference UI. @@ -55,7 +55,7 @@ dotnet run --project src/Host/MyApp.AppHost # 🎉 whole stack up: API + 2 Rea **Identity · Multitenancy · Billing · Catalog · Tickets · Chat · Files · Webhooks · Auditing · Notifications** — each a runtime project plus a `.Contracts` project (its only public surface), boundaries enforced by architecture tests. ### Cloud-native & DevOps -- **.NET Aspire** orchestrates the entire stack locally with one command (Postgres + pgAdmin, Valkey + RedisInsight, MinIO, migrator, demo-seeder, API, and both React apps). +- **.NET Aspire** orchestrates the entire stack locally with one command (Postgres + pgAdmin, Valkey + RedisInsight, RustFS, migrator, demo-seeder, API, and both React apps). - **Docker Compose** production stack (`deploy/docker`) and **Terraform** for AWS (`deploy/terraform`); API image published to GHCR. - A one-shot **DbMigrator** (migrations are never run at API startup), and the **`fsh` CLI** + `dotnet new` template for distribution. @@ -96,7 +96,7 @@ git clone https://github.com/fullstackhero/dotnet-starter-kit.git MyApp && cd My dotnet run --project src/Host/FSH.Starter.AppHost ``` -> **Prerequisites:** [.NET 10 SDK](https://dotnet.microsoft.com/download) · [Docker](https://www.docker.com/) (Postgres/Valkey/MinIO via Aspire) · [Node 20+](https://nodejs.org/) (for the React apps). +> **Prerequisites:** [.NET 10 SDK](https://dotnet.microsoft.com/download) · [Docker](https://www.docker.com/) (Postgres/Valkey/RustFS via Aspire) · [Node 20+](https://nodejs.org/) (for the React apps). **`fsh` commands:** `new` · `doctor` · `info` · `update` · `--version`. Full reference → [fullstackhero.net/docs/cli](https://fullstackhero.net/docs/cli/). @@ -113,7 +113,7 @@ dotnet run --project src/Host/FSH.Starter.AppHost | Auth | JWT + ASP.NET Identity | Realtime | SignalR · SSE | | Multitenancy | Finbuckle 10 | Tests | Playwright | | Cache / Jobs | Valkey · Hangfire | | | -| Storage | S3 / MinIO (presigned) | **Infra** | | +| Storage | S3 / RustFS (presigned) | **Infra** | | | Docs | OpenAPI + Scalar | Orchestration | .NET Aspire | | Observability | Serilog + OpenTelemetry | Deploy | Docker Compose · Terraform | | Testing | xUnit · Testcontainers · NetArchTest | | | @@ -127,7 +127,7 @@ dotnet run --project src/Host/FSH.Starter.AppHost | `src/BuildingBlocks/` | Shared framework libraries (Core, Persistence, Web, Caching, Eventing, Storage, Quota…) | | `src/Modules/{Name}/` | Bounded contexts — each with a runtime project + a `.Contracts` project (its public API) | | `src/Host/FSH.Starter.Api` | Composition-root Web API host | -| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Valkey, MinIO, migrator, API, both React apps) | +| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Valkey, RustFS, migrator, API, both React apps) | | `src/Host/FSH.Starter.DbMigrator` | One-shot migrate/seed runner (DB is **not** migrated at API startup) | | `src/Tools/CLI` | The `fsh` CLI (Spectre.Console) | | `clients/admin`, `clients/dashboard` | The two React apps | diff --git a/clients/admin/README.md b/clients/admin/README.md index 97328a36ea..40378a762c 100644 --- a/clients/admin/README.md +++ b/clients/admin/README.md @@ -15,7 +15,7 @@ Two options — pick whichever matches how you want to develop. ### Option A — run everything through Aspire (recommended) -The AppHost launches Postgres, Redis, MinIO, the API, **and** this Vite app together, with `VITE_API_BASE_URL` wired via service discovery. +The AppHost launches Postgres, Redis, RustFS, the API, **and** this Vite app together, with `VITE_API_BASE_URL` wired via service discovery. ```bash npm install --prefix clients/admin # one-time diff --git a/clients/admin/src/components/file/image-input.tsx b/clients/admin/src/components/file/image-input.tsx index 7a9c9c1f74..33f0f58023 100644 --- a/clients/admin/src/components/file/image-input.tsx +++ b/clients/admin/src/components/file/image-input.tsx @@ -31,7 +31,7 @@ const IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".gif"]; /** * ImageInput — composite control that lets a user either upload a new image - * (presigned PUT to S3/MinIO) OR paste an external URL. After a successful + * (presigned PUT to S3) OR paste an external URL. After a successful * upload the component fetches the FileAsset metadata to retrieve the durable * `publicUrl` and forwards it through `onChange`. */ diff --git a/clients/admin/src/hooks/use-file-upload.ts b/clients/admin/src/hooks/use-file-upload.ts index 4ed92c4c56..5d8d40d97c 100644 --- a/clients/admin/src/hooks/use-file-upload.ts +++ b/clients/admin/src/hooks/use-file-upload.ts @@ -68,7 +68,7 @@ const DEFAULT_OPTIONS = { /** * Orchestrates the three-step presigned-upload protocol: * 1. POST /api/v1/files/upload-url — server mints presigned PUT + reserves a FileAsset row - * 2. PUT — browser pushes bytes straight to S3/MinIO (XHR for progress) + * 2. PUT — browser pushes bytes straight to S3 (RustFS locally) (XHR for progress) * 3. POST /api/v1/files/{id}/finalize — server HEADs the object, transitions to Available * * Progress is reported via the in-state `progress` snapshot — XMLHttpRequest is used (instead of fetch) diff --git a/clients/dashboard/README.md b/clients/dashboard/README.md index b836a88ef7..e8e085a286 100644 --- a/clients/dashboard/README.md +++ b/clients/dashboard/README.md @@ -15,7 +15,7 @@ Two options — pick whichever matches how you want to develop. ### Option A — run everything through Aspire (recommended) -The AppHost launches Postgres, Redis, MinIO, the API, the admin app, **and** this dashboard together, with `VITE_API_BASE_URL` wired via service discovery. +The AppHost launches Postgres, Redis, RustFS, the API, the admin app, **and** this dashboard together, with `VITE_API_BASE_URL` wired via service discovery. ```bash npm install --prefix clients/dashboard # one-time diff --git a/clients/dashboard/src/components/file/image-input.tsx b/clients/dashboard/src/components/file/image-input.tsx index 204d3c6a1d..6b46a8841c 100644 --- a/clients/dashboard/src/components/file/image-input.tsx +++ b/clients/dashboard/src/components/file/image-input.tsx @@ -31,7 +31,7 @@ const IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".gif"]; /** * ImageInput — composite control that lets a user either upload a new image - * (presigned PUT to S3/MinIO) OR paste an external URL. After a successful + * (presigned PUT to S3) OR paste an external URL. After a successful * upload the component fetches the FileAsset metadata to retrieve the durable * `publicUrl` and forwards it through `onChange`. */ diff --git a/clients/dashboard/src/hooks/use-file-upload.ts b/clients/dashboard/src/hooks/use-file-upload.ts index 5dae5aa6fa..9718c23409 100644 --- a/clients/dashboard/src/hooks/use-file-upload.ts +++ b/clients/dashboard/src/hooks/use-file-upload.ts @@ -68,7 +68,7 @@ const DEFAULT_OPTIONS = { /** * Orchestrates the three-step presigned-upload protocol: * 1. POST /api/v1/files/upload-url — server mints presigned PUT + reserves a FileAsset row - * 2. PUT — browser pushes bytes straight to S3/MinIO (XHR for progress) + * 2. PUT — browser pushes bytes straight to S3 (RustFS locally) (XHR for progress) * 3. POST /api/v1/files/{id}/finalize — server HEADs the object, transitions to Available * * Progress is reported via the in-state `progress` snapshot — XMLHttpRequest is used (instead of fetch) diff --git a/deploy/docker/.env.example b/deploy/docker/.env.example index 3cc3c4a432..169155e5d4 100644 --- a/deploy/docker/.env.example +++ b/deploy/docker/.env.example @@ -17,7 +17,7 @@ FSH_DASHBOARD_URL=https://app.example.com FSH_DEFAULT_TENANT=root # ── Host ports for the external proxy to point at ─────────────────── -# Only FSH services publish ports. Postgres/Redis/MinIO stay +# Only FSH services publish ports. Postgres/Redis/RustFS stay # compose-internal by default (uncomment their `ports:` blocks in # docker-compose.yml if you need host access for psql / redis-cli). FSH_API_PORT=8080 @@ -44,8 +44,8 @@ HANGFIRE_PASSWORD= # ── Data plane (defaults are fine for self-hosted compose) ────────── POSTGRES_PASSWORD= REDIS_PASSWORD= -MINIO_ROOT_USER= -MINIO_ROOT_PASSWORD= +RUSTFS_ACCESS_KEY= +RUSTFS_SECRET_KEY= # ── Observability (optional) ──────────────────────────────────────── # Point at an OTLP collector to ship traces/metrics. Leave blank to diff --git a/deploy/docker/README.md b/deploy/docker/README.md index bcb1304593..ea6ca83352 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -10,7 +10,7 @@ This brings up the full stack on a single host: | `migrator` | `fsh/dbmigrator:local` | — | One-shot: applies EF migrations + seeds the root tenant + creates the default admin user | | `postgres` | `postgres:17-alpine` | (internal) | Identity, tenant catalog, module schemas | | `redis` | `redis:7-alpine` | (internal) | HybridCache L2, Data Protection keys, idempotency store | -| `minio` | `minio/minio:latest` | (internal) | S3-compatible blob store for the Files module | +| `rustfs` | `rustfs/rustfs:1.0.0` | (internal) | S3-compatible blob store for the Files module ([RustFS](https://rustfs.com)) | The compose file does **not** include a reverse proxy or TLS terminator. You bring your own edge — Cloudflare Tunnel, AWS ALB, Tailscale Funnel, your existing nginx, anything that can route a TLS subdomain to a host:port on this machine. @@ -87,21 +87,21 @@ docker run --rm \ -v "$PWD":/backup \ alpine \ tar czf /backup/pg_data-$(date +%Y%m%d).tar.gz -C /source . -# Repeat for fsh_redis_data and fsh_minio_data. +# Repeat for fsh_redis_data and fsh_rustfs_data. ``` ## Swapping in managed services Single-host compose is the default story; production deployments often point at managed Postgres / Redis / S3. To do that: -1. Comment out the `postgres` / `redis` / `minio` service blocks AND remove them from the `depends_on:` of `api` and `migrator`. +1. Comment out the `postgres` / `redis` / `rustfs` service blocks (and `rustfs-init`) AND remove them from the `depends_on:` of `api` and `migrator`. 2. Swap the matching env vars on `api` and `migrator`: - `DatabaseOptions__ConnectionString` → your managed Postgres connection string - `CachingOptions__Redis` → your managed Redis connection string (`host:port,password=...,ssl=True` etc.) - `Storage__Provider`, `Storage__S3__*` → your S3-compatible store 3. `docker compose up -d`. -The data-plane volumes (`pg_data`, `redis_data`, `minio_data`) can be deleted once you've migrated. +The data-plane volumes (`pg_data`, `redis_data`, `rustfs_data`) can be deleted once you've migrated. ## Troubleshooting diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index d43c744f5b..a517f2ce30 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -5,7 +5,7 @@ # # Operator owns the edge (TLS / subdomain routing). This file publishes # only the FSH services on host ports; the data plane (postgres/redis/ -# minio) stays compose-internal unless you uncomment their ports blocks. +# rustfs) stays compose-internal unless you uncomment their ports blocks. name: fsh @@ -53,18 +53,18 @@ services: # ports: # - "6379:6379" - minio: - image: minio/minio:latest - container_name: fsh-minio + rustfs: + image: rustfs/rustfs:1.0.0 + container_name: fsh-rustfs restart: unless-stopped - command: ["server", "/data", "--console-address", ":9001"] environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required} + RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY:?RUSTFS_ACCESS_KEY is required} + RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY:?RUSTFS_SECRET_KEY is required} + RUSTFS_CONSOLE_ENABLE: "true" volumes: - - minio_data:/data + - rustfs_data:/data healthcheck: - test: ["CMD", "mc", "ready", "local"] + test: ["CMD", "curl", "-fsS", "http://localhost:9000/health"] interval: 10s timeout: 3s retries: 12 @@ -78,21 +78,22 @@ services: # so without this the first upload fails with NoSuchBucket. No anonymous # policy is set — objects are served via the API / presigned URLs, not a # public bucket. - minio-init: - image: minio/mc:latest - container_name: fsh-minio-init + rustfs-init: + image: amazon/aws-cli:2.37.3 + container_name: fsh-rustfs-init restart: "no" depends_on: - minio: { condition: service_healthy } + rustfs: { condition: service_healthy } environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + AWS_ACCESS_KEY_ID: ${RUSTFS_ACCESS_KEY} + AWS_SECRET_ACCESS_KEY: ${RUSTFS_SECRET_KEY} + AWS_DEFAULT_REGION: us-east-1 entrypoint: /bin/sh command: - -c - | - mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" - mc mb --ignore-existing local/fsh + aws --endpoint-url http://rustfs:9000 s3api head-bucket --bucket fsh 2>/dev/null || + aws --endpoint-url http://rustfs:9000 s3api create-bucket --bucket fsh migrator: build: @@ -121,8 +122,8 @@ services: depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } - minio: { condition: service_healthy } - minio-init: { condition: service_completed_successfully } + rustfs: { condition: service_healthy } + rustfs-init: { condition: service_completed_successfully } migrator: { condition: service_completed_successfully } environment: DOTNET_ENVIRONMENT: Production @@ -132,9 +133,9 @@ services: JwtOptions__SigningKey: ${JWT_SIGNING_KEY} Seed__DefaultAdminPassword: ${SEED_ADMIN_PASSWORD} Storage__Provider: s3 - Storage__S3__ServiceUrl: http://minio:9000 - Storage__S3__AccessKey: ${MINIO_ROOT_USER} - Storage__S3__SecretKey: ${MINIO_ROOT_PASSWORD} + Storage__S3__ServiceUrl: http://rustfs:9000 + Storage__S3__AccessKey: ${RUSTFS_ACCESS_KEY} + Storage__S3__SecretKey: ${RUSTFS_SECRET_KEY} Storage__S3__Bucket: fsh Storage__S3__ForcePathStyle: "true" AllowedHosts: "*" @@ -178,4 +179,4 @@ services: volumes: pg_data: redis_data: - minio_data: + rustfs_data: diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 47ed63f9a6..5cc70bbb6f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -122,10 +122,9 @@ + - - diff --git a/src/Host/FSH.Starter.AppHost/AppHost.cs b/src/Host/FSH.Starter.AppHost/AppHost.cs index e7a70abd05..5c2fd16c23 100644 --- a/src/Host/FSH.Starter.AppHost/AppHost.cs +++ b/src/Host/FSH.Starter.AppHost/AppHost.cs @@ -44,42 +44,43 @@ .WithLifetime(ContainerLifetime.Persistent) .WaitFor(redis); -// Object storage (MinIO, S3-compatible). CORS via MINIO_API_CORS_ALLOW_ORIGIN so browser presigned PUTs from the admin (:5173)/dashboard (:5174) dev origins work without proxying through the API. -const string MinioBucket = "fsh-uploads"; +// Object storage (RustFS, S3-compatible; replaces MinIO, whose images were withdrawn). CORS via RUSTFS_CORS_ALLOWED_ORIGINS so browser presigned PUTs from the admin (:5173)/dashboard (:5174) dev origins work without proxying through the API. +const string S3Bucket = "fsh-uploads"; const string AdminOrigin = "http://localhost:5173"; const string DashboardOrigin = "http://localhost:5174"; -var minioUser = builder.AddParameter("minio-user", "minioadmin"); -var minioPassword = builder.AddParameter("minio-password", "minioadmin", secret: true); +var s3User = builder.AddParameter("rustfs-user", "rustfsadmin"); +var s3Password = builder.AddParameter("rustfs-password", "rustfsadmin", secret: true); -var minio = builder.AddContainer("minio", "minio/minio") - .WithArgs("server", "/data", "--console-address", ":9001") +var rustfs = builder.AddContainer("rustfs", "rustfs/rustfs", "1.0.0") .WithHttpEndpoint(port: 9000, targetPort: 9000, name: "api") .WithHttpEndpoint(port: 9001, targetPort: 9001, name: "console") - .WithEnvironment("MINIO_ROOT_USER", minioUser) - .WithEnvironment("MINIO_ROOT_PASSWORD", minioPassword) - .WithEnvironment("MINIO_API_CORS_ALLOW_ORIGIN", $"{AdminOrigin},{DashboardOrigin}") - .WithVolume($"{appPrefix}-minio-data", "/data") + .WithEnvironment("RUSTFS_ACCESS_KEY", s3User) + .WithEnvironment("RUSTFS_SECRET_KEY", s3Password) + .WithEnvironment("RUSTFS_CONSOLE_ENABLE", "true") + .WithEnvironment("RUSTFS_CORS_ALLOWED_ORIGINS", $"{AdminOrigin},{DashboardOrigin}") + .WithVolume($"{appPrefix}-rustfs-data", "/data") .WithLifetime(ContainerLifetime.Persistent); -// Init container: bucket bootstrap (create + public-read). Script normalized to LF so /bin/sh in minio/mc doesn't choke on Windows CRLF. -var minioInitScript = ($$""" -until mc alias set local http://minio:9000 "$MC_USER" "$MC_PASS"; do - echo "waiting for minio..."; +// Init container: bucket bootstrap (create + public-read GetObject policy). Script normalized to LF so /bin/sh in aws-cli doesn't choke on Windows CRLF. +var s3InitScript = ($$""" +until aws --endpoint-url http://rustfs:9000 s3api list-buckets > /dev/null 2>&1; do + echo "waiting for rustfs..."; sleep 2; done; -mc mb --ignore-existing local/{{MinioBucket}}; -mc anonymous set download local/{{MinioBucket}}; +aws --endpoint-url http://rustfs:9000 s3api head-bucket --bucket {{S3Bucket}} 2>/dev/null || aws --endpoint-url http://rustfs:9000 s3api create-bucket --bucket {{S3Bucket}}; +aws --endpoint-url http://rustfs:9000 s3api put-bucket-policy --bucket {{S3Bucket}} --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::{{S3Bucket}}/*"]}]}'; """).ReplaceLineEndings("\n"); -var minioInit = builder.AddContainer("minio-init", "minio/mc") +var s3Init = builder.AddContainer("rustfs-init", "amazon/aws-cli", "2.37.3") .WithEntrypoint("/bin/sh") - .WithArgs("-c", minioInitScript) - .WithEnvironment("MC_USER", minioUser) - .WithEnvironment("MC_PASS", minioPassword) - .WaitFor(minio); + .WithArgs("-c", s3InitScript) + .WithEnvironment("AWS_ACCESS_KEY_ID", s3User) + .WithEnvironment("AWS_SECRET_ACCESS_KEY", s3Password) + .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") + .WaitFor(rustfs); -var minioApiEndpoint = minio.GetEndpoint("api"); +var s3ApiEndpoint = rustfs.GetEndpoint("api"); // DB migrator: applies pending migrations + seeds the root admin (admin@root.com), then exits; the API waits for its completion so it never starts against an unmigrated DB. Seed password is a dev-only default. var migrator = builder.AddProject($"{appPrefix}-db-migrator") @@ -108,7 +109,7 @@ .WithReference(postgres) .WaitFor(postgres) .WaitFor(redis) - .WaitForCompletion(minioInit) + .WaitForCompletion(s3Init) .WaitForCompletion(migrator) .WaitForCompletion(demoSeeder) .WithExternalHttpEndpoints() @@ -129,13 +130,13 @@ .WithEnvironment("MailOptions__Smtp__UserName", "nicole.lueilwitz0@ethereal.email") .WithEnvironment("MailOptions__Smtp__Password", "x4VJz2r9x2NDss9KpC") .WithEnvironment("Storage__Provider", "s3") - .WithEnvironment("Storage__S3__Bucket", MinioBucket) + .WithEnvironment("Storage__S3__Bucket", S3Bucket) .WithEnvironment("Storage__S3__Region", "us-east-1") - .WithEnvironment("Storage__S3__ServiceUrl", minioApiEndpoint) - .WithEnvironment("Storage__S3__AccessKey", minioUser) - .WithEnvironment("Storage__S3__SecretKey", minioPassword) + .WithEnvironment("Storage__S3__ServiceUrl", s3ApiEndpoint) + .WithEnvironment("Storage__S3__AccessKey", s3User) + .WithEnvironment("Storage__S3__SecretKey", s3Password) .WithEnvironment("Storage__S3__ForcePathStyle", "true") - .WithEnvironment("Storage__S3__PublicBaseUrl", ReferenceExpression.Create($"{minioApiEndpoint}/{MinioBucket}")); + .WithEnvironment("Storage__S3__PublicBaseUrl", ReferenceExpression.Create($"{s3ApiEndpoint}/{S3Bucket}")); //#if (frontend) // Admin console (React + Vite). Target the API's HTTPS endpoint directly — UseHttpsRedirection's 307 to https is cross-origin and strips the Authorization header. diff --git a/src/Modules/Files/Modules.Files.Contracts/v1/Queries/GetFileDownloadUrlQuery.cs b/src/Modules/Files/Modules.Files.Contracts/v1/Queries/GetFileDownloadUrlQuery.cs index edb28c8897..d41c743845 100644 --- a/src/Modules/Files/Modules.Files.Contracts/v1/Queries/GetFileDownloadUrlQuery.cs +++ b/src/Modules/Files/Modules.Files.Contracts/v1/Queries/GetFileDownloadUrlQuery.cs @@ -5,7 +5,7 @@ namespace FSH.Modules.Files.Contracts.v1.Queries; /// /// Mint a short-lived presigned GET URL for a FileAsset. When is -/// true, the URL asks S3/MinIO to send Content-Disposition: inline so the +/// true, the URL asks S3 to send Content-Disposition: inline so the /// browser renders the file in place (PDF viewer, image, etc.) instead of downloading it. /// Default is false (attachment) — the click-to-save behavior. /// diff --git a/src/Tests/Integration.Middleware.Tests/Infrastructure/MiddlewareWebApplicationFactory.cs b/src/Tests/Integration.Middleware.Tests/Infrastructure/MiddlewareWebApplicationFactory.cs index 4c2939c454..e7aae251ba 100644 --- a/src/Tests/Integration.Middleware.Tests/Infrastructure/MiddlewareWebApplicationFactory.cs +++ b/src/Tests/Integration.Middleware.Tests/Infrastructure/MiddlewareWebApplicationFactory.cs @@ -22,7 +22,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -using Testcontainers.Minio; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; using Testcontainers.PostgreSql; namespace Integration.Middleware.Tests.Infrastructure; @@ -42,9 +43,9 @@ namespace Integration.Middleware.Tests.Infrastructure; /// public sealed class MiddlewareWebApplicationFactory : WebApplicationFactory, IAsyncLifetime { - private const string MinioAccessKey = "minioadmin"; - private const string MinioSecretKey = "minioadmin"; - private const string MinioBucket = "fsh-middleware-test-uploads"; + private const string S3AccessKey = "rustfsadmin"; + private const string S3SecretKey = "rustfsadmin"; + private const string S3Bucket = "fsh-middleware-test-uploads"; private static readonly SemaphoreSlim _migrationLock = new(1, 1); private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder("postgres:17-alpine") @@ -55,9 +56,13 @@ public sealed class MiddlewareWebApplicationFactory : WebApplicationFactory r.ForPort(S3Port).ForPath("/health"))) .WithAutoRemove(true) .WithCleanUp(true) .Build(); @@ -78,8 +83,8 @@ public MiddlewareWebApplicationFactory() public async Task InitializeAsync() { - await Task.WhenAll(_postgres.StartAsync(), _minio.StartAsync()); - await CreateMinioBucketAsync(); + await Task.WhenAll(_postgres.StartAsync(), _s3.StartAsync()); + await CreateS3BucketAsync(); // Force host creation via the Server property (no leaked HttpClient) _ = Server; @@ -101,29 +106,29 @@ public async Task InitializeAsync() { await base.DisposeAsync(); await _postgres.DisposeAsync(); - await _minio.DisposeAsync(); + await _s3.DisposeAsync(); } - /// The MinIO endpoint URL exposed to the host configuration; useful for tests that need to PUT bytes directly. - public string MinioServiceUrl => _minio.GetConnectionString(); + /// The S3 (RustFS) endpoint URL exposed to the host configuration; useful for tests that need to PUT bytes directly. + public string S3ServiceUrl => $"http://{_s3.Hostname}:{_s3.GetMappedPublicPort(S3Port)}"; - private async Task CreateMinioBucketAsync() + private async Task CreateS3BucketAsync() { var config = new AmazonS3Config { - ServiceURL = _minio.GetConnectionString(), + ServiceURL = S3ServiceUrl, ForcePathStyle = true, UseHttp = true, AuthenticationRegion = "us-east-1" }; using var client = new AmazonS3Client( - new Amazon.Runtime.BasicAWSCredentials(MinioAccessKey, MinioSecretKey), + new Amazon.Runtime.BasicAWSCredentials(S3AccessKey, S3SecretKey), config); try { - await client.PutBucketAsync(new PutBucketRequest { BucketName = MinioBucket }); + await client.PutBucketAsync(new PutBucketRequest { BucketName = S3Bucket }); } catch (AmazonS3Exception ex) when (ex.ErrorCode == "BucketAlreadyOwnedByYou" || ex.ErrorCode == "BucketAlreadyExists") { @@ -181,10 +186,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) ["SecurityHeadersOptions:Enabled"] = "true", ["Storage:Provider"] = "s3", - ["Storage:S3:Bucket"] = MinioBucket, - ["Storage:S3:ServiceUrl"] = _minio.GetConnectionString(), - ["Storage:S3:AccessKey"] = MinioAccessKey, - ["Storage:S3:SecretKey"] = MinioSecretKey, + ["Storage:S3:Bucket"] = S3Bucket, + ["Storage:S3:ServiceUrl"] = S3ServiceUrl, + ["Storage:S3:AccessKey"] = S3AccessKey, + ["Storage:S3:SecretKey"] = S3SecretKey, ["Storage:S3:ForcePathStyle"] = "true", ["Storage:S3:PublicRead"] = "false", ["Storage:S3:Region"] = "us-east-1", @@ -225,7 +230,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.AddSingleton(); // DELIBERATELY keep the production GlobalExceptionHandler (no swap) so /__test/throw yields real - // RFC 9457 output; storage stays unrewired (no test hits it; MinIO + S3 keys kept so host binds). + // RFC 9457 output; storage stays unrewired (no test hits it; RustFS + S3 keys kept so host binds). // Append a throwing endpoint via IStartupFilter: run next(app) first (UseRouting stamps the real // IEndpointRouteBuilder into app.Properties), then MapGet onto it — lazy data sources still match. diff --git a/src/Tests/Integration.Middleware.Tests/Integration.Middleware.Tests.csproj b/src/Tests/Integration.Middleware.Tests/Integration.Middleware.Tests.csproj index 057056d088..6d760bdceb 100644 --- a/src/Tests/Integration.Middleware.Tests/Integration.Middleware.Tests.csproj +++ b/src/Tests/Integration.Middleware.Tests/Integration.Middleware.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Tests/Integration.Tests/Infrastructure/FshWebApplicationFactory.cs b/src/Tests/Integration.Tests/Infrastructure/FshWebApplicationFactory.cs index ab8cfe3c65..7cf6a6cadf 100644 --- a/src/Tests/Integration.Tests/Infrastructure/FshWebApplicationFactory.cs +++ b/src/Tests/Integration.Tests/Infrastructure/FshWebApplicationFactory.cs @@ -21,16 +21,17 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -using Testcontainers.Minio; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; using Testcontainers.PostgreSql; namespace Integration.Tests.Infrastructure; public sealed class FshWebApplicationFactory : WebApplicationFactory, IAsyncLifetime { - private const string MinioAccessKey = "minioadmin"; - private const string MinioSecretKey = "minioadmin"; - private const string MinioBucket = "fsh-integration-test-uploads"; + private const string S3AccessKey = "rustfsadmin"; + private const string S3SecretKey = "rustfsadmin"; + private const string S3Bucket = "fsh-integration-test-uploads"; private static readonly SemaphoreSlim _migrationLock = new(1, 1); private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder("postgres:17-alpine") @@ -41,17 +42,21 @@ public sealed class FshWebApplicationFactory : WebApplicationFactory, I .WithCleanUp(true) .Build(); - private readonly MinioContainer _minio = new MinioBuilder("minio/minio:latest") - .WithUsername(MinioAccessKey) - .WithPassword(MinioSecretKey) + // RustFS (S3-compatible) replaces MinIO, whose images were withdrawn from Docker Hub and quay.io. + private const int S3Port = 9000; + private readonly IContainer _s3 = new ContainerBuilder("rustfs/rustfs:1.0.0") + .WithPortBinding(S3Port, true) + .WithEnvironment("RUSTFS_ACCESS_KEY", S3AccessKey) + .WithEnvironment("RUSTFS_SECRET_KEY", S3SecretKey) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(S3Port).ForPath("/health"))) .WithAutoRemove(true) .WithCleanUp(true) .Build(); public async Task InitializeAsync() { - await Task.WhenAll(_postgres.StartAsync(), _minio.StartAsync()); - await CreateMinioBucketAsync(); + await Task.WhenAll(_postgres.StartAsync(), _s3.StartAsync()); + await CreateS3BucketAsync(); // Force host creation via the Server property (no leaked HttpClient) _ = Server; @@ -73,29 +78,29 @@ public async Task InitializeAsync() { await base.DisposeAsync(); await _postgres.DisposeAsync(); - await _minio.DisposeAsync(); + await _s3.DisposeAsync(); } - /// The MinIO endpoint URL exposed to the host configuration; useful for tests that need to PUT bytes directly. - public string MinioServiceUrl => _minio.GetConnectionString(); + /// The S3 (RustFS) endpoint URL exposed to the host configuration; useful for tests that need to PUT bytes directly. + public string S3ServiceUrl => $"http://{_s3.Hostname}:{_s3.GetMappedPublicPort(S3Port)}"; - private async Task CreateMinioBucketAsync() + private async Task CreateS3BucketAsync() { var config = new AmazonS3Config { - ServiceURL = _minio.GetConnectionString(), + ServiceURL = S3ServiceUrl, ForcePathStyle = true, UseHttp = true, AuthenticationRegion = "us-east-1" }; using var client = new AmazonS3Client( - new Amazon.Runtime.BasicAWSCredentials(MinioAccessKey, MinioSecretKey), + new Amazon.Runtime.BasicAWSCredentials(S3AccessKey, S3SecretKey), config); try { - await client.PutBucketAsync(new PutBucketRequest { BucketName = MinioBucket }); + await client.PutBucketAsync(new PutBucketRequest { BucketName = S3Bucket }); } catch (AmazonS3Exception ex) when (ex.ErrorCode == "BucketAlreadyOwnedByYou" || ex.ErrorCode == "BucketAlreadyExists") { @@ -141,10 +146,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) ["Seed:DefaultAdminPassword"] = TestConstants.DefaultPassword, ["SecurityHeadersOptions:Enabled"] = "false", ["Storage:Provider"] = "s3", - ["Storage:S3:Bucket"] = MinioBucket, - ["Storage:S3:ServiceUrl"] = _minio.GetConnectionString(), - ["Storage:S3:AccessKey"] = MinioAccessKey, - ["Storage:S3:SecretKey"] = MinioSecretKey, + ["Storage:S3:Bucket"] = S3Bucket, + ["Storage:S3:ServiceUrl"] = S3ServiceUrl, + ["Storage:S3:AccessKey"] = S3AccessKey, + ["Storage:S3:SecretKey"] = S3SecretKey, ["Storage:S3:ForcePathStyle"] = "true", ["Storage:S3:PublicRead"] = "false", ["Storage:S3:Region"] = "us-east-1", @@ -191,7 +196,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.AddExceptionHandler(); // AddHeroStorage reads `Storage:Provider` eagerly (before the test config overlay applies), so it - // wires LocalStorageService. Replace it here with the S3 stack pointed at the MinIO testcontainer. + // wires LocalStorageService. Replace it here with the S3 stack pointed at the RustFS testcontainer. RewireStorageForS3(services); }); } @@ -208,10 +213,10 @@ private void RewireStorageForS3(IServiceCollection services) services.Configure(opts => { - opts.Bucket = MinioBucket; - opts.ServiceUrl = _minio.GetConnectionString(); - opts.AccessKey = MinioAccessKey; - opts.SecretKey = MinioSecretKey; + opts.Bucket = S3Bucket; + opts.ServiceUrl = S3ServiceUrl; + opts.AccessKey = S3AccessKey; + opts.SecretKey = S3SecretKey; opts.ForcePathStyle = true; opts.PublicRead = false; opts.Region = "us-east-1"; @@ -221,13 +226,13 @@ private void RewireStorageForS3(IServiceCollection services) { var config = new AmazonS3Config { - ServiceURL = _minio.GetConnectionString(), + ServiceURL = S3ServiceUrl, ForcePathStyle = true, UseHttp = true, AuthenticationRegion = "us-east-1" }; return new AmazonS3Client( - new Amazon.Runtime.BasicAWSCredentials(MinioAccessKey, MinioSecretKey), + new Amazon.Runtime.BasicAWSCredentials(S3AccessKey, S3SecretKey), config); }); services.AddTransient(); diff --git a/src/Tests/Integration.Tests/Integration.Tests.csproj b/src/Tests/Integration.Tests/Integration.Tests.csproj index 1aea12e3ab..5fe88bf1aa 100644 --- a/src/Tests/Integration.Tests/Integration.Tests.csproj +++ b/src/Tests/Integration.Tests/Integration.Tests.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Tests/Integration.Tests/Tests/Files/PurgeJobsTests.cs b/src/Tests/Integration.Tests/Tests/Files/PurgeJobsTests.cs index 9405cabdaf..a6a2f8eaea 100644 --- a/src/Tests/Integration.Tests/Tests/Files/PurgeJobsTests.cs +++ b/src/Tests/Integration.Tests/Tests/Files/PurgeJobsTests.cs @@ -20,7 +20,7 @@ namespace Integration.Tests.Tests.Files; /// Coverage for the two Hangfire purge jobs. They are NOT run by the scheduler in tests, so we /// resolve each from a DI scope (with the Finbuckle tenant context set INLINE in that scope to /// avoid the AsyncLocal NRE in the tenant filter) and invoke RunAsync directly after seeding -/// a purgeable precondition. The real S3/MinIO RemoveAsync runs against bytes that were genuinely +/// a purgeable precondition. The real S3 (RustFS) RemoveAsync runs against bytes that were genuinely /// PUT, so this also exercises S3StorageService.RemoveAsync (DeleteObject) end-to-end. /// [Collection(FshCollectionDefinition.Name)] diff --git a/src/Tests/Integration.Tests/Tests/Files/RequestAndFinalizeUploadTests.cs b/src/Tests/Integration.Tests/Tests/Files/RequestAndFinalizeUploadTests.cs index 699b9ff717..4de46f932b 100644 --- a/src/Tests/Integration.Tests/Tests/Files/RequestAndFinalizeUploadTests.cs +++ b/src/Tests/Integration.Tests/Tests/Files/RequestAndFinalizeUploadTests.cs @@ -25,7 +25,7 @@ public async Task UploadUrl_Then_Finalize_Should_TransitionToAvailable() var presigned = await RequestPresignedUploadAsync(client, "doc.pdf", "application/pdf", bytes.Length, "Document"); - // PUT the bytes to MinIO using the presigned URL. + // PUT the bytes to RustFS using the presigned URL. using var raw = new HttpClient(); using var put = new HttpRequestMessage(HttpMethod.Put, presigned.UploadUrl) { diff --git a/src/Tests/Integration.Tests/Tests/Files/StorageFlowTests.cs b/src/Tests/Integration.Tests/Tests/Files/StorageFlowTests.cs index ea83497be1..9061e75340 100644 --- a/src/Tests/Integration.Tests/Tests/Files/StorageFlowTests.cs +++ b/src/Tests/Integration.Tests/Tests/Files/StorageFlowTests.cs @@ -34,7 +34,7 @@ public async Task DownloadUrl_Should_RoundTrip_The_Exact_Bytes_That_Were_Uploade RandomNumberGenerator.Fill(bytes); var id = await UploadAndFinalizeAsync(client, "roundtrip.pdf", "application/pdf", bytes); - // Act — mint a presigned GET and fetch the bytes straight from MinIO. + // Act — mint a presigned GET and fetch the bytes straight from RustFS. using var urlResp = await client.GetAsync($"{FilesBasePath}/{id}/url"); urlResp.StatusCode.ShouldBe(HttpStatusCode.OK); var download = await urlResp.DeserializeAsync(); diff --git a/src/Tools/CLI/Commands/NewCommand.cs b/src/Tools/CLI/Commands/NewCommand.cs index b9b4f7890c..e555b03f97 100644 --- a/src/Tools/CLI/Commands/NewCommand.cs +++ b/src/Tools/CLI/Commands/NewCommand.cs @@ -342,8 +342,8 @@ private static bool GenerateDockerEnv(string output) ["HANGFIRE_PASSWORD"] = GenerateSecret(20), ["POSTGRES_PASSWORD"] = GenerateSecret(24), ["REDIS_PASSWORD"] = GenerateSecret(24), - ["MINIO_ROOT_USER"] = "minioadmin", - ["MINIO_ROOT_PASSWORD"] = GenerateSecret(24), + ["RUSTFS_ACCESS_KEY"] = "rustfsadmin", + ["RUSTFS_SECRET_KEY"] = GenerateSecret(24), // Local-working defaults: the compose stack publishes these host ports. ["FSH_API_URL"] = "http://localhost:8080", ["FSH_ADMIN_URL"] = "http://localhost:8081",