Skip to content

Commit f478cb1

Browse files
StertzaaronpowellLucas Stertz
authored
feat: add SeaweedFS hosting and client integrations (#1349)
* feat: add SeaweedFS hosting and client integrations * Apply PR feedback: DI fixes, namespace adjustments, and code cleanup * Fix CodeQL XSS warning on API endpoints * Update examples/seaweedfs/SeaweedFS.AppHost/SeaweedFS.AppHost.csproj * Simplify AspireExport usage * Add TypeScript app host tests for SeaweedFS * Apply suggestions from code review Co-authored-by: Aaron Powell <me@aaron-powell.com> * Fix: Update TypeScript app host API --------- Co-authored-by: Aaron Powell <me@aaron-powell.com> Co-authored-by: Lucas Stertz <lucasstertz@CONDUSVALE.local>
1 parent c282a98 commit f478cb1

43 files changed

Lines changed: 4844 additions & 3 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CommunityToolkit.Aspire.slnx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@
166166
<Project Path="examples/rust/CommunityToolkit.Aspire.Hosting.Rust.AppHost/CommunityToolkit.Aspire.Hosting.Rust.AppHost.csproj" />
167167
<Project Path="examples/rust/CommunityToolkit.Aspire.Hosting.Rust.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Rust.ServiceDefaults.csproj" />
168168
</Folder>
169+
<Folder Name="/examples/seaweedfs/">
170+
<File Path="examples/seaweedfs/README.md" />
171+
<Project Path="examples/seaweedfs/SeaweedFS.ApiService/SeaweedFS.ApiService.csproj" />
172+
<Project Path="examples/seaweedfs/SeaweedFS.AppHost/SeaweedFS.AppHost.csproj" Id="6e0ec4a6-d1d2-4aed-9a5b-b053c46e41b3" />
173+
<Project Path="examples/seaweedfs/SeaweedFS.ServiceDefaults/SeaweedFS.ServiceDefaults.csproj" />
174+
</Folder>
169175
<Folder Name="/examples/rustfs/">
170176
<Project Path="examples/rustfs/CommunityToolkit.Aspire.Hosting.RustFs.AppHost/CommunityToolkit.Aspire.Hosting.RustFs.AppHost.csproj" />
171177
</Folder>
@@ -250,6 +256,7 @@
250256
<Project Path="src/CommunityToolkit.Aspire.Hosting.RavenDB/CommunityToolkit.Aspire.Hosting.RavenDB.csproj" />
251257
<Project Path="src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/CommunityToolkit.Aspire.Hosting.Redis.Extensions.csproj" />
252258
<Project Path="src/CommunityToolkit.Aspire.Hosting.Rust/CommunityToolkit.Aspire.Hosting.Rust.csproj" />
259+
<Project Path="src/CommunityToolkit.Aspire.Hosting.SeaweedFS/CommunityToolkit.Aspire.Hosting.SeaweedFS.csproj" />
253260
<Project Path="src/CommunityToolkit.Aspire.Hosting.RustFs/CommunityToolkit.Aspire.Hosting.RustFs.csproj" />
254261
<Project Path="src/CommunityToolkit.Aspire.Hosting.Sftp/CommunityToolkit.Aspire.Hosting.Sftp.csproj" />
255262
<Project Path="src/CommunityToolkit.Aspire.Hosting.Solr/CommunityToolkit.Aspire.Hosting.Solr.csproj" />
@@ -269,6 +276,7 @@
269276
<Project Path="src/CommunityToolkit.Aspire.Minio.Client/CommunityToolkit.Aspire.Minio.Client.csproj" />
270277
<Project Path="src/CommunityToolkit.Aspire.OllamaSharp/CommunityToolkit.Aspire.OllamaSharp.csproj" />
271278
<Project Path="src/CommunityToolkit.Aspire.RavenDB.Client/CommunityToolkit.Aspire.RavenDB.Client.csproj" />
279+
<Project Path="src/CommunityToolkit.Aspire.SeaweedFS.Client/CommunityToolkit.Aspire.SeaweedFS.Client.csproj" />
272280
<Project Path="src/CommunityToolkit.Aspire.Sftp/CommunityToolkit.Aspire.Sftp.csproj" />
273281
<Project Path="src/CommunityToolkit.Aspire.SurrealDb/CommunityToolkit.Aspire.SurrealDb.csproj" />
274282
<Project Path="src\CommunityToolkit.Aspire.Hosting.Keycloak.Extensions\CommunityToolkit.Aspire.Hosting.Keycloak.Extensions.csproj" />
@@ -311,12 +319,13 @@
311319
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Ollama.Tests/CommunityToolkit.Aspire.Hosting.Ollama.Tests.csproj" />
312320
<Project Path="tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests.csproj" />
313321
<Project Path="tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests.csproj" />
322+
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/CommunityToolkit.Aspire.Hosting.Perl.Tests.csproj" />
314323
<Project Path="tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests.csproj" />
315324
<Project Path="tests/CommunityToolkit.Aspire.Hosting.PowerShell.Tests/CommunityToolkit.Aspire.Hosting.PowerShell.Tests.csproj" />
316-
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/CommunityToolkit.Aspire.Hosting.Perl.Tests.csproj" />
317325
<Project Path="tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests.csproj" />
318326
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests.csproj" />
319327
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Rust.Tests/CommunityToolkit.Aspire.Hosting.Rust.Tests.csproj" />
328+
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SeaweedFS.Tests/CommunityToolkit.Aspire.Hosting.SeaweedFS.Tests.csproj" />
320329
<Project Path="tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests/CommunityToolkit.Aspire.Hosting.RustFs.Tests.csproj" />
321330
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Sftp.Tests/CommunityToolkit.Aspire.Hosting.Sftp.Tests.csproj" />
322331
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/CommunityToolkit.Aspire.Hosting.Solr.Tests.csproj" />
@@ -336,6 +345,7 @@
336345
<Project Path="tests/CommunityToolkit.Aspire.Minio.Client.Tests/CommunityToolkit.Aspire.Minio.Client.Tests.csproj" />
337346
<Project Path="tests/CommunityToolkit.Aspire.OllamaSharp.Tests/CommunityToolkit.Aspire.OllamaSharp.Tests.csproj" />
338347
<Project Path="tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/CommunityToolkit.Aspire.RavenDB.Client.Tests.csproj" />
348+
<Project Path="tests/CommunityToolkit.Aspire.SeaweedFS.Client.Tests/CommunityToolkit.Aspire.SeaweedFS.Client.Tests.csproj" />
339349
<Project Path="tests/CommunityToolkit.Aspire.Sftp.Tests/CommunityToolkit.Aspire.Sftp.Tests.csproj" />
340350
<Project Path="tests/CommunityToolkit.Aspire.SurrealDb.Tests/CommunityToolkit.Aspire.SurrealDb.Tests.csproj" />
341351
<Project Path="tests/CommunityToolkit.Aspire.Testing/CommunityToolkit.Aspire.Testing.csproj" />
@@ -349,4 +359,4 @@
349359
<Folder Name="/tests/tests-app-hosts/">
350360
<Project Path="tests-app-hosts/Ollama.AppHost/Ollama.AppHost.csproj" />
351361
</Folder>
352-
</Solution>
362+
</Solution>

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
2525
<PackageVersion Include="Logto.AspNetCore.Authentication" Version="0.2.0" />
2626
<PackageVersion Include="AspNetCore.HealthChecks.Network" Version="9.0.0" />
27-
27+
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="10.0.8" />
2828
</ItemGroup>
2929
<ItemGroup Label="Core Packages">
3030
<!-- AspNetCore packages -->
@@ -72,6 +72,7 @@
7272
</ItemGroup>
7373
<ItemGroup Label="Integration Packages">
7474
<!-- External packages -->
75+
<PackageVersion Include="AWSSDK.S3" Version="4.0.23.3" />
7576
<PackageVersion Include="Azure.Provisioning.AppContainers" Version="1.2.0" />
7677
<PackageVersion Include="JsonSchema.Net" Version="7.4.0" />
7778
<PackageVersion Include="OllamaSharp" Version="5.4.12" />

examples/seaweedfs/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# SeaweedFS Aspire Integration Example
2+
3+
This example demonstrates how to integrate and consume a SeaweedFS cluster within a .NET Aspire distributed application. It showcases both the **S3-Compatible API** and the **Native Filer API** in a single cohesive setup.
4+
5+
## Project Structure
6+
7+
* **SeaweedFS.AppHost:** The Aspire orchestrator. It spins up the SeaweedFS Docker container, enables the S3 Gateway and Data Volumes, and injects the dynamic connection strings into the API.
8+
* **SeaweedFS.ServiceDefaults:** Standard Aspire telemetry, resilience, and health check configurations.
9+
* **SeaweedFS.ApiService:** A minimal API application that registers both the `IAmazonS3` client and the `SeaweedFSFilerClient` to interact with the storage cluster.
10+
11+
## Running the Example
12+
13+
1. Ensure you have [Docker Desktop](https://www.docker.com/products/docker-desktop/) or Podman running on your machine.
14+
2. Open a terminal in this directory (`aspire/seaweedfs/`).
15+
3. Run the AppHost:
16+
17+
```dotnetcli
18+
dotnet run --project SeaweedFS.AppHost
19+
20+
```
21+
22+
4. Open the **Aspire Dashboard** URL provided in the console output.
23+
5. Wait for both the `seaweedfs` container and the `apiservice` to show as **Healthy**.
24+
25+
## Exploring the Endpoints
26+
27+
The `SeaweedFS.ApiService` exposes endpoints to test both storage approaches. You can trigger them using Swagger (if enabled) or using tools like `curl` or Postman.
28+
29+
### S3 Endpoints (AWS SDK Compatibility)
30+
31+
These endpoints use the injected `IAmazonS3` client.
32+
33+
* **Create a Bucket:**
34+
`POST /s3/buckets?bucketName=my-bucket`
35+
* **Upload a File:**
36+
`POST /s3/upload?bucketName=my-bucket&key=test.txt`
37+
*(Send raw text in the body)*
38+
* **Download a File:**
39+
`GET /s3/download?bucketName=my-bucket&key=test.txt`
40+
41+
### Filer Endpoints (Native SeaweedFS API)
42+
43+
These endpoints use the injected `SeaweedFSFilerClient` performing direct HTTP calls to the cluster.
44+
45+
* **Upload a File to Root:**
46+
`POST /filer/upload?fileName=native-file.txt`
47+
*(Send raw text in the body)*
48+
* **List Directory Contents:**
49+
`GET /filer/list`
50+
*(Returns a JSON representation of the Filer's directory structure)*
51+
52+
## Viewing Data in SeaweedFS
53+
54+
Since `SeaweedFS.AppHost` maps a data volume using `.WithDataVolume()`, any file you upload using the endpoints above will be persisted in the Docker volume. If you stop the AppHost and run it again, your data will still be accessible.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using Amazon.S3;
2+
using Amazon.S3.Model;
3+
using CommunityToolkit.Aspire.SeaweedFS.Client;
4+
using Microsoft.AspNetCore.Mvc;
5+
6+
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
7+
8+
// Add service defaults (OpenTelemetry, HealthChecks, etc.)
9+
builder.AddServiceDefaults();
10+
11+
// Add essential web services
12+
builder.Services.AddProblemDetails();
13+
14+
// =========================================================================
15+
// 🌊 REGISTER SEAWEEDFS CLIENTS
16+
// =========================================================================
17+
// Basic Setup (Endpoints and credentials are automatically injected by AppHost)
18+
builder.AddSeaweedFSS3Client("seaweedfs");
19+
builder.AddSeaweedFSFilerClient("seaweedfs");
20+
21+
/* // -------------------------------------------------------------------------
22+
// ADVANCED SETUP EXAMPLES
23+
// Uncomment and use the blocks below to override client settings programmatically.
24+
// -------------------------------------------------------------------------
25+
26+
builder.AddSeaweedFSS3Client("seaweedfs", settings =>
27+
{
28+
// The Endpoint is injected automatically, but you can override protocol rules:
29+
settings.UseSsl = false;
30+
settings.ForcePathStyle = true; // Required by SeaweedFS architecture
31+
32+
// Deep configuration of the underlying AWS SDK:
33+
settings.ConfigureS3Config = s3Config =>
34+
{
35+
s3Config.Timeout = TimeSpan.FromSeconds(30);
36+
s3Config.MaxErrorRetry = 3;
37+
};
38+
});
39+
40+
builder.AddSeaweedFSFilerClient("seaweedfs", settings =>
41+
{
42+
// Example of disabling health checks for the Filer specifically
43+
settings.DisableHealthChecks = true;
44+
});
45+
*/
46+
47+
WebApplication app = builder.Build();
48+
49+
app.UseExceptionHandler();
50+
51+
// ==========================================
52+
// 🌊 S3 ENDPOINTS (AWS Compatibility)
53+
// ==========================================
54+
RouteGroupBuilder s3Group = app.MapGroup("/s3").WithTags("S3 API");
55+
56+
s3Group.MapPost("/buckets", async ([FromQuery] string bucketName, IAmazonS3 s3Client) =>
57+
{
58+
await s3Client.PutBucketAsync(new PutBucketRequest { BucketName = bucketName });
59+
return Results.Ok(new { Message = "Bucket successfully created via S3 API." });
60+
});
61+
62+
s3Group.MapPost("/upload", async ([FromQuery] string bucketName, [FromQuery] string key, [FromBody] string content, IAmazonS3 s3Client) =>
63+
{
64+
await s3Client.PutObjectAsync(new PutObjectRequest
65+
{
66+
BucketName = bucketName,
67+
Key = key,
68+
ContentBody = content
69+
});
70+
return Results.Ok(new { Message = "File successfully uploaded to bucket via S3 API." });
71+
});
72+
73+
s3Group.MapGet("/download", async ([FromQuery] string bucketName, [FromQuery] string key, IAmazonS3 s3Client) =>
74+
{
75+
GetObjectResponse response = await s3Client.GetObjectAsync(bucketName, key);
76+
using StreamReader reader = new(response.ResponseStream);
77+
string content = await reader.ReadToEndAsync();
78+
79+
return Results.Ok(new
80+
{
81+
Bucket = System.Text.Encodings.Web.HtmlEncoder.Default.Encode(bucketName),
82+
Key = System.Text.Encodings.Web.HtmlEncoder.Default.Encode(key),
83+
Content = content
84+
});
85+
});
86+
87+
// ==========================================
88+
// 📁 FILER ENDPOINTS (Native API)
89+
// ==========================================
90+
RouteGroupBuilder filerGroup = app.MapGroup("/filer").WithTags("Filer API");
91+
92+
filerGroup.MapGet("/list", async (SeaweedFSFilerClient filerClient) =>
93+
{
94+
HttpRequestMessage request = new(HttpMethod.Get, "/");
95+
request.Headers.Add("Accept", "application/json");
96+
97+
HttpResponseMessage response = await filerClient.HttpClient.SendAsync(request);
98+
response.EnsureSuccessStatusCode();
99+
100+
string content = await response.Content.ReadAsStringAsync();
101+
return Results.Content(content, "application/json");
102+
});
103+
104+
filerGroup.MapPost("/upload", async ([FromQuery] string fileName, [FromBody] string content, SeaweedFSFilerClient filerClient) =>
105+
{
106+
StringContent stringContent = new(content, System.Text.Encoding.UTF8, "text/plain");
107+
108+
string safeFileName = Uri.EscapeDataString(fileName.TrimStart('/'));
109+
110+
HttpResponseMessage response = await filerClient.HttpClient.PutAsync($"/{safeFileName}", stringContent);
111+
112+
response.EnsureSuccessStatusCode();
113+
114+
return Results.Ok(new { Message = "File successfully uploaded via native Filer API." });
115+
});
116+
117+
app.MapDefaultEndpoints();
118+
119+
app.Run();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"profiles": {
3+
"SeaweedFS.ApiService": {
4+
"commandName": "Project",
5+
"launchBrowser": true,
6+
"environmentVariables": {
7+
"ASPNETCORE_ENVIRONMENT": "Development"
8+
},
9+
"applicationUrl": "https://localhost:4530;http://localhost:4531"
10+
}
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\SeaweedFS.ServiceDefaults\SeaweedFS.ServiceDefaults.csproj" />
5+
<ProjectReference Include="..\..\..\src\CommunityToolkit.Aspire.SeaweedFS.Client\CommunityToolkit.Aspire.SeaweedFS.Client.csproj" />
6+
</ItemGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="AWSSDK.S3" />
10+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
11+
</ItemGroup>
12+
13+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createBuilder } from "./.aspire/modules/aspire.mjs";
2+
3+
const builder = await createBuilder();
4+
5+
// 1. S3-Compatible API Gateway
6+
const seaweedS3 = await builder.addSeaweedFS("typescript-seaweedfs-s3");
7+
await seaweedS3.withS3();
8+
await seaweedS3.withDataVolume();
9+
10+
// 2. Native Filer API Only
11+
const seaweedFiler = await builder.addSeaweedFS("typescript-seaweedfs-filer");
12+
await seaweedFiler.withFiler();
13+
14+
await builder.build().run();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"appHost": {
3+
"path": "apphost.mts",
4+
"language": "typescript/nodejs"
5+
},
6+
"sdk": {
7+
"version": "13.4.3"
8+
},
9+
"profiles": {
10+
"http": {
11+
"applicationUrl": "http://localhost:15333"
12+
}
13+
},
14+
"packages": {
15+
"CommunityToolkit.Aspire.Hosting.SeaweedFS": "../../../src/CommunityToolkit.Aspire.Hosting.SeaweedFS/CommunityToolkit.Aspire.Hosting.SeaweedFS.csproj"
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @ts-check
2+
3+
import { defineConfig } from 'eslint/config';
4+
import tseslint from 'typescript-eslint';
5+
6+
export default defineConfig({
7+
files: ['apphost.mts'],
8+
extends: [tseslint.configs.base],
9+
languageOptions: {
10+
parserOptions: {
11+
projectService: true,
12+
},
13+
},
14+
rules: {
15+
'@typescript-eslint/no-floating-promises': ['error', { checkThenables: true }],
16+
},
17+
});

0 commit comments

Comments
 (0)