diff --git a/DysonNetwork.Drive/.gitignore b/DysonNetwork.Drive/.gitignore
index 0d7ff5f..c783c91 100644
--- a/DysonNetwork.Drive/.gitignore
+++ b/DysonNetwork.Drive/.gitignore
@@ -1,2 +1,3 @@
/Uploads/
-/wwwroot/dist
\ No newline at end of file
+/Client/node_modules/
+/wwwroot/dist
diff --git a/DysonNetwork.Drive/Client/.gitignore b/DysonNetwork.Drive/Client/.gitignore
index 8ee54e8..f6ab678 100644
--- a/DysonNetwork.Drive/Client/.gitignore
+++ b/DysonNetwork.Drive/Client/.gitignore
@@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
+node_modules/highlight.js
.DS_Store
dist
dist-ssr
diff --git a/DysonNetwork.Drive/Client/src/router/index.ts b/DysonNetwork.Drive/Client/src/router/index.ts
index db85fec..8c8d1d5 100644
--- a/DysonNetwork.Drive/Client/src/router/index.ts
+++ b/DysonNetwork.Drive/Client/src/router/index.ts
@@ -10,7 +10,7 @@ const router = createRouter({
component: () => import('../views/index.vue')
},
{
- path: '/files',
+ path: '/files/:fileId',
name: 'files',
component: () => import('../views/files.vue'),
}
diff --git a/DysonNetwork.Drive/Client/src/types/pool.ts b/DysonNetwork.Drive/Client/src/types/pool.ts
new file mode 100644
index 0000000..740668a
--- /dev/null
+++ b/DysonNetwork.Drive/Client/src/types/pool.ts
@@ -0,0 +1,35 @@
+export interface SnFilePool {
+ id: string;
+ name: string;
+ storage_config: StorageConfig;
+ billing_config: BillingConfig;
+ public_indexable: boolean;
+ public_usable: boolean;
+ no_optimization: boolean;
+ no_metadata: boolean;
+ allow_encryption: boolean;
+ allow_anonymous: boolean;
+ require_privilege: number;
+ account_id: null;
+ resource_identifier: string;
+ created_at: Date;
+ updated_at: Date;
+ deleted_at: null;
+}
+
+export interface BillingConfig {
+ cost_multiplier: number;
+}
+
+export interface StorageConfig {
+ region: string;
+ bucket: string;
+ endpoint: string;
+ secret_id: string;
+ secret_key: string;
+ enable_signed: boolean;
+ enable_ssl: boolean;
+ image_proxy: null;
+ access_proxy: null;
+ expiration: null;
+}
diff --git a/DysonNetwork.Drive/Client/src/views/files.vue b/DysonNetwork.Drive/Client/src/views/files.vue
index d971dd2..01925e0 100644
--- a/DysonNetwork.Drive/Client/src/views/files.vue
+++ b/DysonNetwork.Drive/Client/src/views/files.vue
@@ -1,36 +1,182 @@
-
-
-
-
-
- Download
-
-
-
-
+
+
+
+
+
+
+
+
+ The file has been encrypted. Preview not available. Please enter the password to
+ download it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ File Type
+
+ {{ fileInfo.mime_type }} ({{ fileType }})
+
+
+
+
+
+
+ File Size
+
+ {{ formatBytes(fileInfo.size) }}
+
+
+
+
+
+
+ Uploaded At
+
+ {{ new Date(fileInfo.created_at).toLocaleString() }}
+
+
+
+
+
+
+ Techical Info
+
+
+ {{ showTechDetails ? 'Hide' : 'Show' }}
+
+
+
+
+
+
#{{ fileInfo.id }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Download
+
+
+
+
+
+
diff --git a/DysonNetwork.Drive/Client/src/views/index.vue b/DysonNetwork.Drive/Client/src/views/index.vue
index 360f7d2..6d694ab 100644
--- a/DysonNetwork.Drive/Client/src/views/index.vue
+++ b/DysonNetwork.Drive/Client/src/views/index.vue
@@ -22,17 +22,38 @@
-
-
+
+
+
+
+
File Password
+
+
Only available for Stellar Program and certian file pool.
+
+
+
+
-
+
Click or drag a file to this area to upload
@@ -78,30 +99,131 @@ import {
NP,
NInput,
NSwitch,
+ NSelect,
+ NTag,
+ NCollapseTransition,
+ NFormItem,
type UploadCustomRequestOptions,
type UploadSettledFileInfo,
+ type SelectOption,
+ type SelectRenderTag,
} from 'naive-ui'
-import { onMounted, ref } from 'vue'
-import { UploadOutlined } from '@vicons/material'
+import { computed, h, onMounted, ref } from 'vue'
+import { CloudUploadRound } from '@vicons/material'
import { useUserStore } from '@/stores/user'
+import type { SnFilePool } from '@/types/pool'
import * as tus from 'tus-js-client'
const userStore = useUserStore()
const version = ref(null)
-
async function fetchVersion() {
const resp = await fetch('/api/version')
version.value = await resp.json()
}
-
onMounted(() => fetchVersion())
+type SnFilePoolOption = SnFilePool & any
+
+const pools = ref()
+async function fetchPools() {
+ const resp = await fetch('/api/pools')
+ pools.value = await resp.json()
+}
+onMounted(() => fetchPools())
+
+const renderSingleSelectTag: SelectRenderTag = ({ option }) => {
+ return h(
+ 'div',
+ {
+ style: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ },
+ [option.name as string],
+ )
+}
+
+function renderPoolSelectLabel(option: SelectOption & SnFilePool, selected: boolean) {
+ return h(
+ 'div',
+ {
+ style: {
+ padding: '8px 2px',
+ },
+ },
+ [
+ h('div', null, [option.name as string]),
+ h(
+ 'div',
+ {
+ style: {
+ display: 'flex',
+ gap: '0.25rem',
+ marginTop: '2px',
+ marginLeft: '-2px',
+ marginRight: '-2px',
+ },
+ },
+ [
+ option.public_usable &&
+ h(
+ NTag,
+ {
+ type: 'info',
+ size: 'small',
+ round: true,
+ },
+ { default: () => 'Public Shared' },
+ ),
+ option.public_indexable &&
+ h(
+ NTag,
+ {
+ type: 'success',
+ size: 'small',
+ round: true,
+ },
+ { default: () => 'Public Indexable' },
+ ),
+ option.allow_encryption &&
+ h(
+ NTag,
+ {
+ type: 'warning',
+ size: 'small',
+ round: true,
+ },
+ { default: () => 'Allow Encryption' },
+ ),
+ option.allow_anonymous &&
+ h(
+ NTag,
+ {
+ type: 'info',
+ size: 'small',
+ round: true,
+ },
+ { default: () => 'Allow Anonymous' },
+ ),
+ ],
+ ),
+ ],
+ )
+}
+
const modeAdvanced = ref(false)
+const filePool = ref(null)
const filePass = ref('')
+const currentFilePool = computed(() => {
+ if (!filePool.value) return null
+ return pools.value?.find((pool) => pool.id === filePool.value) ?? null
+})
+
function customRequest({
file,
data,
@@ -112,6 +234,9 @@ function customRequest({
onError,
onProgress,
}: UploadCustomRequestOptions) {
+ const requestHeaders: Record = {}
+ if (filePool.value) requestHeaders['X-FilePool'] = filePool.value
+ if (filePass.value) requestHeaders['X-FilePass'] = filePass.value
const upload = new tus.Upload(file.file, {
endpoint: '/api/tus',
retryDelays: [0, 3000, 5000, 10000, 20000],
@@ -120,7 +245,7 @@ function customRequest({
filetype: file.type ?? 'application/octet-stream',
},
headers: {
- 'X-FilePass': filePass.value,
+ ...requestHeaders,
...headers,
},
onError: function (error) {
@@ -151,7 +276,10 @@ function customRequest({
})
}
-function createThumbnailUrl(_file: File | null, fileInfo: UploadSettledFileInfo): string | undefined {
+function createThumbnailUrl(
+ _file: File | null,
+ fileInfo: UploadSettledFileInfo,
+): string | undefined {
if (!fileInfo) return undefined
return fileInfo.url ?? undefined
}
diff --git a/DysonNetwork.Drive/Migrations/20250726034305_FilePoolAuthorize.Designer.cs b/DysonNetwork.Drive/Migrations/20250726034305_FilePoolAuthorize.Designer.cs
new file mode 100644
index 0000000..0232deb
--- /dev/null
+++ b/DysonNetwork.Drive/Migrations/20250726034305_FilePoolAuthorize.Designer.cs
@@ -0,0 +1,288 @@
+//
+using System;
+using System.Collections.Generic;
+using DysonNetwork.Drive;
+using DysonNetwork.Drive.Storage;
+using DysonNetwork.Shared.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NodaTime;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace DysonNetwork.Drive.Migrations
+{
+ [DbContext(typeof(AppDatabase))]
+ [Migration("20250726034305_FilePoolAuthorize")]
+ partial class FilePoolAuthorize
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.7")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("id");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid")
+ .HasColumnName("account_id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("Description")
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("description");
+
+ b.Property>("FileMeta")
+ .HasColumnType("jsonb")
+ .HasColumnName("file_meta");
+
+ b.Property("HasCompression")
+ .HasColumnType("boolean")
+ .HasColumnName("has_compression");
+
+ b.Property("HasThumbnail")
+ .HasColumnType("boolean")
+ .HasColumnName("has_thumbnail");
+
+ b.Property("Hash")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("hash");
+
+ b.Property("IsEncrypted")
+ .HasColumnType("boolean")
+ .HasColumnName("is_encrypted");
+
+ b.Property("IsMarkedRecycle")
+ .HasColumnType("boolean")
+ .HasColumnName("is_marked_recycle");
+
+ b.Property("MimeType")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("mime_type");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("name");
+
+ b.Property("PoolId")
+ .HasColumnType("uuid")
+ .HasColumnName("pool_id");
+
+ b.Property>("SensitiveMarks")
+ .HasColumnType("jsonb")
+ .HasColumnName("sensitive_marks");
+
+ b.Property("Size")
+ .HasColumnType("bigint")
+ .HasColumnName("size");
+
+ b.Property("StorageId")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("storage_id");
+
+ b.Property("StorageUrl")
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("storage_url");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.Property("UploadedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("uploaded_at");
+
+ b.Property("UploadedTo")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("uploaded_to");
+
+ b.Property>("UserMeta")
+ .HasColumnType("jsonb")
+ .HasColumnName("user_meta");
+
+ b.HasKey("Id")
+ .HasName("pk_files");
+
+ b.HasIndex("PoolId")
+ .HasDatabaseName("ix_files_pool_id");
+
+ b.ToTable("files", (string)null);
+ });
+
+ modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("ExpiredAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expired_at");
+
+ b.Property("FileId")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("file_id");
+
+ b.Property("ResourceId")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("resource_id");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.Property("Usage")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("usage");
+
+ b.HasKey("Id")
+ .HasName("pk_file_references");
+
+ b.HasIndex("FileId")
+ .HasDatabaseName("ix_file_references_file_id");
+
+ b.ToTable("file_references", (string)null);
+ });
+
+ modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid")
+ .HasColumnName("account_id");
+
+ b.Property("AllowAnonymous")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_anonymous");
+
+ b.Property("AllowEncryption")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_encryption");
+
+ b.Property("BillingConfig")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("billing_config");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("name");
+
+ b.Property("NoMetadata")
+ .HasColumnType("boolean")
+ .HasColumnName("no_metadata");
+
+ b.Property("NoOptimization")
+ .HasColumnType("boolean")
+ .HasColumnName("no_optimization");
+
+ b.Property("PublicIndexable")
+ .HasColumnType("boolean")
+ .HasColumnName("public_indexable");
+
+ b.Property("PublicUsable")
+ .HasColumnType("boolean")
+ .HasColumnName("public_usable");
+
+ b.Property("RequirePrivilege")
+ .HasColumnType("integer")
+ .HasColumnName("require_privilege");
+
+ b.Property("StorageConfig")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("storage_config");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.HasKey("Id")
+ .HasName("pk_pools");
+
+ b.ToTable("pools", (string)null);
+ });
+
+ modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
+ {
+ b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
+ .WithMany()
+ .HasForeignKey("PoolId")
+ .HasConstraintName("fk_files_pools_pool_id");
+
+ b.Navigation("Pool");
+ });
+
+ modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
+ {
+ b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
+ .WithMany()
+ .HasForeignKey("FileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_file_references_files_file_id");
+
+ b.Navigation("File");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/DysonNetwork.Drive/Migrations/20250726034305_FilePoolAuthorize.cs b/DysonNetwork.Drive/Migrations/20250726034305_FilePoolAuthorize.cs
new file mode 100644
index 0000000..cae21fc
--- /dev/null
+++ b/DysonNetwork.Drive/Migrations/20250726034305_FilePoolAuthorize.cs
@@ -0,0 +1,51 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DysonNetwork.Drive.Migrations
+{
+ ///
+ public partial class FilePoolAuthorize : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "account_id",
+ table: "pools",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "public_indexable",
+ table: "pools",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "public_usable",
+ table: "pools",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "account_id",
+ table: "pools");
+
+ migrationBuilder.DropColumn(
+ name: "public_indexable",
+ table: "pools");
+
+ migrationBuilder.DropColumn(
+ name: "public_usable",
+ table: "pools");
+ }
+ }
+}
diff --git a/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs
index 853ca45..4328b3b 100644
--- a/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs
+++ b/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs
@@ -192,6 +192,10 @@ namespace DysonNetwork.Drive.Migrations
.HasColumnType("uuid")
.HasColumnName("id");
+ b.Property("AccountId")
+ .HasColumnType("uuid")
+ .HasColumnName("account_id");
+
b.Property("AllowAnonymous")
.HasColumnType("boolean")
.HasColumnName("allow_anonymous");
@@ -227,6 +231,14 @@ namespace DysonNetwork.Drive.Migrations
.HasColumnType("boolean")
.HasColumnName("no_optimization");
+ b.Property("PublicIndexable")
+ .HasColumnType("boolean")
+ .HasColumnName("public_indexable");
+
+ b.Property("PublicUsable")
+ .HasColumnType("boolean")
+ .HasColumnName("public_usable");
+
b.Property("RequirePrivilege")
.HasColumnType("integer")
.HasColumnName("require_privilege");
diff --git a/DysonNetwork.Drive/Storage/FilePool.cs b/DysonNetwork.Drive/Storage/FilePool.cs
index d918c6b..0054984 100644
--- a/DysonNetwork.Drive/Storage/FilePool.cs
+++ b/DysonNetwork.Drive/Storage/FilePool.cs
@@ -30,11 +30,15 @@ public class FilePool : ModelBase, IIdentifiedResource
[MaxLength(1024)] public string Name { get; set; } = string.Empty;
[Column(TypeName = "jsonb")] public RemoteStorageConfig StorageConfig { get; set; } = new();
[Column(TypeName = "jsonb")] public BillingConfig BillingConfig { get; set; } = new();
+ public bool PublicIndexable { get; set; } = false;
+ public bool PublicUsable { get; set; } = false;
public bool NoOptimization { get; set; } = false;
public bool NoMetadata { get; set; } = false;
public bool AllowEncryption { get; set; } = true;
public bool AllowAnonymous { get; set; } = true;
public int RequirePrivilege { get; set; } = 0;
+
+ public Guid? AccountId { get; set; }
public string ResourceIdentifier => $"file-pool/{Id}";
}
\ No newline at end of file
diff --git a/DysonNetwork.Drive/Storage/FilePoolController.cs b/DysonNetwork.Drive/Storage/FilePoolController.cs
new file mode 100644
index 0000000..515f054
--- /dev/null
+++ b/DysonNetwork.Drive/Storage/FilePoolController.cs
@@ -0,0 +1,25 @@
+using DysonNetwork.Shared.Proto;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace DysonNetwork.Drive.Storage;
+
+[ApiController]
+[Route("/api/pools")]
+public class FilePoolController(AppDatabase db) : ControllerBase
+{
+ [HttpGet]
+ [Authorize]
+ public async Task>> ListUsablePools()
+ {
+ if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
+
+ var accountId = Guid.Parse(currentUser.Id);
+ var pools = await db.Pools
+ .Where(p => p.PublicUsable || p.AccountId == accountId)
+ .ToListAsync();
+
+ return Ok(pools);
+ }
+}
\ No newline at end of file
diff --git a/DysonNetwork.Drive/bun.lock b/DysonNetwork.Drive/bun.lock
new file mode 100644
index 0000000..d7a853d
--- /dev/null
+++ b/DysonNetwork.Drive/bun.lock
@@ -0,0 +1,13 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "dependencies": {
+ "highlight.js": "^11.11.1",
+ },
+ },
+ },
+ "packages": {
+ "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
+ }
+}
diff --git a/DysonNetwork.Drive/package.json b/DysonNetwork.Drive/package.json
new file mode 100644
index 0000000..318dde6
--- /dev/null
+++ b/DysonNetwork.Drive/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "highlight.js": "^11.11.1"
+ }
+}
\ No newline at end of file
diff --git a/DysonNetwork.Shared/PageData/Startup.cs b/DysonNetwork.Shared/PageData/Startup.cs
index c163968..ee597a4 100644
--- a/DysonNetwork.Shared/PageData/Startup.cs
+++ b/DysonNetwork.Shared/PageData/Startup.cs
@@ -13,6 +13,13 @@ public static class PageStartup
#pragma warning disable ASP0016
app.MapFallback(async context =>
{
+ if (context.Request.Path.StartsWithSegments("/api") || context.Request.Path.StartsWithSegments("/cgi"))
+ {
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ await context.Response.WriteAsync("Not found");
+ return;
+ }
+
var html = await File.ReadAllTextAsync(defaultFile);
using var scope = app.Services.CreateScope();