✨ File uploading
This commit is contained in:
		
							
								
								
									
										3
									
								
								DysonNetwork.Sphere/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								DysonNetwork.Sphere/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,2 @@ | ||||
| Keys | ||||
| Keys | ||||
| Uploads | ||||
| @@ -22,6 +22,7 @@ public class AppDatabase( | ||||
|     public DbSet<Account.AccountAuthFactor> AccountAuthFactors { get; set; } | ||||
|     public DbSet<Auth.Session> AuthSessions { get; set; } | ||||
|     public DbSet<Auth.Challenge> AuthChallenges { get; set; } | ||||
|     public DbSet<Storage.CloudFile> Files { get; set; } | ||||
|  | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|     { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ | ||||
|         <PackageReference Include="Casbin.NET" Version="2.12.0" /> | ||||
|         <PackageReference Include="Casbin.NET.Adapter.EFCore" Version="2.5.0" /> | ||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||
|         <PackageReference Include="FFMpegCore" Version="5.2.0" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" /> | ||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3"> | ||||
| @@ -20,6 +21,11 @@ | ||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" /> | ||||
|         <PackageReference Include="MimeTypes" Version="2.5.2"> | ||||
|           <PrivateAssets>all</PrivateAssets> | ||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="Minio" Version="6.0.4" /> | ||||
|         <PackageReference Include="NodaTime" Version="3.2.2" /> | ||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||
| @@ -27,6 +33,7 @@ | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.0" /> | ||||
|         <PackageReference Include="tusdotnet" Version="2.8.1" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
| @@ -35,4 +42,8 @@ | ||||
|       </Content> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <Folder Include="Uploads\" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
							
								
								
									
										454
									
								
								DysonNetwork.Sphere/Migrations/20250412182922_AddCloudFiles.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										454
									
								
								DysonNetwork.Sphere/Migrations/20250412182922_AddCloudFiles.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,454 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Sphere; | ||||
| 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.Sphere.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250412182922_AddCloudFiles")] | ||||
|     partial class AddCloudFiles | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "9.0.3") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<bool>("IsSuperuser") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_superuser"); | ||||
|  | ||||
|                     b.Property<string>("Language") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(32) | ||||
|                         .HasColumnType("character varying(32)") | ||||
|                         .HasColumnName("language"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<string>("Nick") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("nick"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_accounts"); | ||||
|  | ||||
|                     b.ToTable("accounts", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("Secret") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("secret"); | ||||
|  | ||||
|                     b.Property<int>("Type") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("type"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_account_auth_factors"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_account_auth_factors_account_id"); | ||||
|  | ||||
|                     b.ToTable("account_auth_factors", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<string>("Content") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("content"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<int>("Type") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("type"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("VerifiedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("verified_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_account_contacts"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_account_contacts_account_id"); | ||||
|  | ||||
|                     b.ToTable("account_contacts", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<List<string>>("Audiences") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("audiences"); | ||||
|  | ||||
|                     b.Property<List<long>>("BlacklistFactors") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("blacklist_factors"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("DeviceId") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("device_id"); | ||||
|  | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
|  | ||||
|                     b.Property<string>("IpAddress") | ||||
|                         .HasMaxLength(128) | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("ip_address"); | ||||
|  | ||||
|                     b.Property<string>("Nonce") | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("nonce"); | ||||
|  | ||||
|                     b.Property<List<string>>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("scopes"); | ||||
|  | ||||
|                     b.Property<int>("StepRemain") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("step_remain"); | ||||
|  | ||||
|                     b.Property<int>("StepTotal") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("step_total"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<string>("UserAgent") | ||||
|                         .HasMaxLength(512) | ||||
|                         .HasColumnType("character varying(512)") | ||||
|                         .HasColumnName("user_agent"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_auth_challenges"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_auth_challenges_account_id"); | ||||
|  | ||||
|                     b.ToTable("auth_challenges", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Guid>("ChallengeId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("challenge_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("LastGrantedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("last_granted_at"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_auth_sessions"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_auth_sessions_account_id"); | ||||
|  | ||||
|                     b.HasIndex("ChallengeId") | ||||
|                         .HasDatabaseName("ix_auth_sessions_challenge_id"); | ||||
|  | ||||
|                     b.ToTable("auth_sessions", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("Description") | ||||
|                         .HasMaxLength(4096) | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("FileMeta") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("file_meta"); | ||||
|  | ||||
|                     b.Property<string>("Hash") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("hash"); | ||||
|  | ||||
|                     b.Property<string>("MimeType") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("mime_type"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<long>("Size") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("size"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("UploadedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("uploaded_at"); | ||||
|  | ||||
|                     b.Property<string>("UploadedTo") | ||||
|                         .HasMaxLength(128) | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("uploaded_to"); | ||||
|  | ||||
|                     b.Property<int>("UsedCount") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("used_count"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("UserMeta") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("user_meta"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_files"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_files_account_id"); | ||||
|  | ||||
|                     b.ToTable("files", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany("AuthFactors") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_account_auth_factors_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany("Contacts") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_account_contacts_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany("Challenges") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_auth_challenges_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany("Sessions") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_auth_sessions_accounts_account_id"); | ||||
|  | ||||
|                     b.HasOne("DysonNetwork.Sphere.Auth.Challenge", "Challenge") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ChallengeId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|  | ||||
|                     b.Navigation("Challenge"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_files_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => | ||||
|                 { | ||||
|                     b.Navigation("AuthFactors"); | ||||
|  | ||||
|                     b.Navigation("Challenges"); | ||||
|  | ||||
|                     b.Navigation("Contacts"); | ||||
|  | ||||
|                     b.Navigation("Sessions"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,59 @@ | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddCloudFiles : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "files", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<string>(type: "text", nullable: false), | ||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), | ||||
|                     file_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true), | ||||
|                     user_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true), | ||||
|                     mime_type = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), | ||||
|                     hash = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), | ||||
|                     size = table.Column<long>(type: "bigint", nullable: false), | ||||
|                     uploaded_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||
|                     uploaded_to = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true), | ||||
|                     used_count = table.Column<int>(type: "integer", nullable: false), | ||||
|                     account_id = table.Column<long>(type: "bigint", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_files", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_files_accounts_account_id", | ||||
|                         column: x => x.account_id, | ||||
|                         principalTable: "accounts", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_files_account_id", | ||||
|                 table: "files", | ||||
|                 column: "account_id"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "files"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -289,6 +289,83 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                     b.ToTable("auth_sessions", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("Description") | ||||
|                         .HasMaxLength(4096) | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("FileMeta") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("file_meta"); | ||||
|  | ||||
|                     b.Property<string>("Hash") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("hash"); | ||||
|  | ||||
|                     b.Property<string>("MimeType") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("mime_type"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<long>("Size") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("size"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("UploadedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("uploaded_at"); | ||||
|  | ||||
|                     b.Property<string>("UploadedTo") | ||||
|                         .HasMaxLength(128) | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("uploaded_to"); | ||||
|  | ||||
|                     b.Property<int>("UsedCount") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("used_count"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("UserMeta") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("user_meta"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_files"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_files_account_id"); | ||||
|  | ||||
|                     b.ToTable("files", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
| @@ -346,6 +423,18 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                     b.Navigation("Challenge"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_files_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => | ||||
|                 { | ||||
|                     b.Navigation("AuthFactors"); | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using System.Net; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| @@ -6,6 +7,7 @@ using Casbin.Persist.Adapter.EFCore; | ||||
| using DysonNetwork.Sphere; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Auth; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -13,6 +15,9 @@ using Microsoft.IdentityModel.Tokens; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using tusdotnet; | ||||
| using tusdotnet.Models; | ||||
| using File = System.IO.File; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| @@ -106,6 +111,7 @@ builder.Services.AddOpenApi(); | ||||
|  | ||||
| builder.Services.AddScoped<AccountService>(); | ||||
| builder.Services.AddScoped<AuthService>(); | ||||
| builder.Services.AddScoped<FileService>(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| @@ -129,11 +135,74 @@ app.UseCors(opts => | ||||
|     opts.SetIsOriginAllowed(_ => true) | ||||
|         .AllowCredentials() | ||||
|         .AllowAnyHeader() | ||||
|         .AllowAnyMethod()); | ||||
|         .AllowAnyMethod() | ||||
| ); | ||||
|  | ||||
| app.UseHttpsRedirection(); | ||||
| app.UseAuthorization(); | ||||
|  | ||||
| app.MapControllers(); | ||||
|  | ||||
| var tusDiskStore = new tusdotnet.Stores.TusDiskStore( | ||||
|     builder.Configuration.GetSection("Tus").GetValue<string>("StorePath")! | ||||
| ); | ||||
| app.MapTus("/files/tus", (_) => Task.FromResult<DefaultTusConfiguration>(new() | ||||
| { | ||||
|     Store = tusDiskStore, | ||||
|     Events = new() | ||||
|     { | ||||
|         OnAuthorizeAsync = async eventContext => | ||||
|         { | ||||
|             var httpContext = eventContext.HttpContext; | ||||
|             var user = httpContext.User; | ||||
|             if (!user.Identity?.IsAuthenticated ?? true) | ||||
|             { | ||||
|                 eventContext.FailRequest(HttpStatusCode.Unauthorized); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var userId = httpContext.User.FindFirst("user_id")?.Value; | ||||
|             if (userId == null) return; | ||||
|             var isSuperuser = httpContext.User.FindFirst("is_superuser")?.Value == "1"; | ||||
|             if (isSuperuser) userId = "super:" + userId; | ||||
|  | ||||
|             var enforcer = httpContext.RequestServices.GetRequiredService<IEnforcer>(); | ||||
|             var allowed = await enforcer.EnforceAsync(userId, "global", "files", "create"); | ||||
|             if (!allowed) | ||||
|             { | ||||
|                 eventContext.FailRequest(HttpStatusCode.Forbidden); | ||||
|             } | ||||
|         }, | ||||
|         OnFileCompleteAsync = async eventContext => | ||||
|         { | ||||
|             var httpContext = eventContext.HttpContext; | ||||
|             var user = httpContext.User; | ||||
|             var userId = long.Parse(user.FindFirst("user_id")!.Value); | ||||
|  | ||||
|             var db = httpContext.RequestServices.GetRequiredService<AppDatabase>(); | ||||
|             var account = await db.Accounts.FindAsync(userId); | ||||
|             if (account is null) return; | ||||
|  | ||||
|             var file = await eventContext.GetFileAsync(); | ||||
|             var metadata = await file.GetMetadataAsync(eventContext.CancellationToken); | ||||
|             var fileName = metadata.TryGetValue("filename", out var fn) ? fn.GetString(Encoding.UTF8) : "uploaded_file"; | ||||
|             var contentType = metadata.TryGetValue("content-type", out var ct) ? ct.GetString(Encoding.UTF8) : null; | ||||
|             var fileStream = await file.GetContentAsync(eventContext.CancellationToken); | ||||
|  | ||||
|             var fileService = eventContext.HttpContext.RequestServices.GetRequiredService<FileService>(); | ||||
|  | ||||
|             var info = await fileService.AnalyzeFileAsync(account, file.Id, fileStream, fileName, contentType); | ||||
|             await fileService.UploadFileToRemoteAsync(info, fileStream, null); | ||||
|              | ||||
|             await tusDiskStore.DeleteFileAsync(file.Id, eventContext.CancellationToken); | ||||
|         }, | ||||
|         OnCreateCompleteAsync = eventContext => | ||||
|         { | ||||
|             // var baseUrl = builder.Configuration.GetValue<string>("Storage:BaseUrl")!; | ||||
|             // eventContext.SetUploadUrl(new Uri($"{baseUrl}/files/{eventContext.FileId}")); | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|     } | ||||
| })); | ||||
|  | ||||
| app.Run(); | ||||
							
								
								
									
										59
									
								
								DysonNetwork.Sphere/Storage/CloudFile.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								DysonNetwork.Sphere/Storage/CloudFile.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using Newtonsoft.Json; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
|  | ||||
| public class RemoteStorageConfig | ||||
| { | ||||
|     public string Id { get; set; } = string.Empty; | ||||
|     public string Label { get; set; } = string.Empty; | ||||
|     public string Region { get; set; } = string.Empty; | ||||
|     public string Bucket { get; set; } = string.Empty; | ||||
|     public string Endpoint { get; set; } = string.Empty; | ||||
|     public string SecretId { get; set; } = string.Empty; | ||||
|     public string SecretKey { get; set; } = string.Empty; | ||||
|     public bool EnableSigned { get; set; } | ||||
|     public bool EnableSsl { get; set; } | ||||
|     public string? ImageProxy { get; set; } | ||||
|     public string? AccessProxy { get; set; } | ||||
| } | ||||
|  | ||||
| public class CloudFile : BaseModel | ||||
| { | ||||
|     public string Id { get; set; } = Guid.NewGuid().ToString(); | ||||
|     [MaxLength(1024)] public string Name { get; set; } = string.Empty; | ||||
|     [MaxLength(4096)] public string? Description { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? FileMeta { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? UserMeta { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] List<CloudFileSensitiveMark> SensitiveMarks { get; set; } = new(); | ||||
|     [MaxLength(256)] public string? MimeType { get; set; } | ||||
|     [MaxLength(256)] public string? Hash { get; set; } | ||||
|     public long Size { get; set; } | ||||
|     public Instant? UploadedAt { get; set; } | ||||
|     [MaxLength(128)] public string? UploadedTo { get; set; } | ||||
|  | ||||
|     // Metrics | ||||
|     // When this used count keep zero, it means it's not used by anybody, so it can be recycled | ||||
|     public int UsedCount { get; set; } = 0; | ||||
|  | ||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||
| } | ||||
|  | ||||
| public enum CloudFileSensitiveMark | ||||
| { | ||||
|     Language, | ||||
|     SexualContent, | ||||
|     Violence, | ||||
|     Profanity, | ||||
|     HateSpeech, | ||||
|     Racism, | ||||
|     AdultContent, | ||||
|     DrugAbuse, | ||||
|     AlcoholAbuse, | ||||
|     Gambling, | ||||
|     SelfHarm, | ||||
|     ChildAbuse, | ||||
|     Other | ||||
| } | ||||
							
								
								
									
										142
									
								
								DysonNetwork.Sphere/Storage/FileService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								DysonNetwork.Sphere/Storage/FileService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| using System.Globalization; | ||||
| using FFMpegCore; | ||||
| using System.Security.Cryptography; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Minio; | ||||
| using Minio.DataModel.Args; | ||||
| using Minio.DataModel.Tags; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
|  | ||||
| public class FileService(AppDatabase db, IConfiguration configuration) | ||||
| { | ||||
|     public async Task<CloudFile> AnalyzeFileAsync( | ||||
|         Account.Account account, | ||||
|         string fileId, | ||||
|         Stream stream, | ||||
|         string fileName, | ||||
|         string? contentType | ||||
|     ) | ||||
|     { | ||||
|         var fileSize = stream.Length; | ||||
|         var hash = await HashFileAsync(stream, fileSize: fileSize); | ||||
|         contentType ??= !fileName.Contains('.') ? "application/octet-stream" : MimeTypes.GetMimeType(fileName); | ||||
|  | ||||
|         var existingFile = await db.Files.Where(f => f.Hash == hash).FirstOrDefaultAsync(); | ||||
|         if (existingFile is not null) return existingFile; | ||||
|  | ||||
|         var file = new CloudFile | ||||
|         { | ||||
|             Id = fileId, | ||||
|             Name = fileName, | ||||
|             MimeType = contentType, | ||||
|             Size = fileSize, | ||||
|             Hash = hash, | ||||
|             Account = account, | ||||
|         }; | ||||
|  | ||||
|         switch (contentType.Split('/')[0]) | ||||
|         { | ||||
|             case "video": | ||||
|             case "audio": | ||||
|                 var mediaInfo = await FFProbe.AnalyseAsync(stream); | ||||
|                 file.FileMeta = new Dictionary<string, object> | ||||
|                 { | ||||
|                     ["duration"] = mediaInfo.Duration.TotalSeconds, | ||||
|                     ["format_name"] = mediaInfo.Format.FormatName, | ||||
|                     ["format_long_name"] = mediaInfo.Format.FormatLongName, | ||||
|                     ["start_time"] = mediaInfo.Format.StartTime.ToString(), | ||||
|                     ["bit_rate"] = mediaInfo.Format.BitRate.ToString(CultureInfo.InvariantCulture), | ||||
|                     ["tags"] = mediaInfo.Format.Tags ?? [], | ||||
|                     ["chapters"] = mediaInfo.Chapters, | ||||
|                 }; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         db.Files.Add(file); | ||||
|         await db.SaveChangesAsync(); | ||||
|         return file; | ||||
|     } | ||||
|  | ||||
|     private static async Task<string> HashFileAsync(Stream stream, int chunkSize = 1024 * 1024, long? fileSize = null) | ||||
|     { | ||||
|         fileSize ??= stream.Length; | ||||
|         if (fileSize > chunkSize * 1024 * 5) | ||||
|             return await HashFastApproximateAsync(stream, chunkSize); | ||||
|  | ||||
|         using var md5 = MD5.Create(); | ||||
|         var hashBytes = await md5.ComputeHashAsync(stream); | ||||
|         return Convert.ToHexString(hashBytes).ToLowerInvariant(); | ||||
|     } | ||||
|  | ||||
|     private static async Task<string> HashFastApproximateAsync(Stream stream, int chunkSize = 1024 * 1024) | ||||
|     { | ||||
|         // Scale the chunk size to kB level | ||||
|         chunkSize *= 1024; | ||||
|  | ||||
|         using var md5 = MD5.Create(); | ||||
|  | ||||
|         var buffer = new byte[chunkSize * 2]; | ||||
|         var fileLength = stream.Length; | ||||
|  | ||||
|         var bytesRead = await stream.ReadAsync(buffer.AsMemory(0, chunkSize)); | ||||
|  | ||||
|         if (fileLength > chunkSize) | ||||
|         { | ||||
|             stream.Seek(-chunkSize, SeekOrigin.End); | ||||
|             bytesRead += await stream.ReadAsync(buffer.AsMemory(chunkSize, chunkSize)); | ||||
|         } | ||||
|  | ||||
|         var hash = md5.ComputeHash(buffer, 0, bytesRead); | ||||
|         return Convert.ToHexString(hash).ToLowerInvariant(); | ||||
|     } | ||||
|  | ||||
|     public async Task<CloudFile> UploadFileToRemoteAsync(CloudFile file, Stream stream, string? targetRemote) | ||||
|     { | ||||
|         if (file.UploadedAt.HasValue) return file; | ||||
|  | ||||
|         file.UploadedTo = targetRemote ?? configuration.GetValue<string>("Storage:PreferredRemote")!; | ||||
|  | ||||
|         var dest = GetRemoteStorageConfig(file.UploadedTo); | ||||
|         var client = CreateMinioClient(dest); | ||||
|         if (client is null) | ||||
|             throw new InvalidOperationException( | ||||
|                 $"Failed to configure client for remote destination '{file.UploadedTo}'" | ||||
|             ); | ||||
|  | ||||
|         var bucket = dest.Bucket; | ||||
|         var contentType = file.MimeType ?? "application/octet-stream"; | ||||
|  | ||||
|         await client.PutObjectAsync(new PutObjectArgs() | ||||
|             .WithBucket(bucket) | ||||
|             .WithObject(file.Id) | ||||
|             .WithStreamData(stream) | ||||
|             .WithObjectSize(stream.Length) | ||||
|             .WithContentType(contentType) | ||||
|         ); | ||||
|  | ||||
|         file.UploadedAt = Instant.FromDateTimeUtc(DateTime.UtcNow); | ||||
|         await db.SaveChangesAsync(); | ||||
|         return file; | ||||
|     } | ||||
|  | ||||
|     private RemoteStorageConfig GetRemoteStorageConfig(string destination) | ||||
|     { | ||||
|         var destinations = configuration.GetSection("Storage:Remote").Get<List<RemoteStorageConfig>>()!; | ||||
|         var dest = destinations.FirstOrDefault(d => d.Id == destination); | ||||
|         if (dest is null) throw new InvalidOperationException($"Remote destination '{destination}' not found"); | ||||
|         return dest; | ||||
|     } | ||||
|  | ||||
|     private IMinioClient? CreateMinioClient(RemoteStorageConfig dest) | ||||
|     { | ||||
|         var client = new MinioClient() | ||||
|             .WithEndpoint(dest.Endpoint) | ||||
|             .WithRegion(dest.Region) | ||||
|             .WithCredentials(dest.SecretId, dest.SecretKey); | ||||
|         if (dest.EnableSsl) client = client.WithSSL(); | ||||
|  | ||||
|         return client.Build(); | ||||
|     } | ||||
| } | ||||
| @@ -24,5 +24,25 @@ | ||||
|   "Jwt": { | ||||
|     "PublicKeyPath": "Keys/PublicKey.pem", | ||||
|     "PrivateKeyPath": "Keys/PrivateKey.pem" | ||||
|   }, | ||||
|   "Tus": { | ||||
|     "StorePath": "Uploads" | ||||
|   }, | ||||
|   "Storage": { | ||||
|     "BaseUrl": "http://localhost:5071", | ||||
|     "PreferredRemote": "cloudflare", | ||||
|     "Remote": [ | ||||
|       { | ||||
|         "Id": "cloudflare", | ||||
|         "Label": "Cloudflare R2", | ||||
|         "Region": "auto", | ||||
|         "Bucket": "solar-network", | ||||
|         "Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com", | ||||
|         "SecretId": "8ff5d06c7b1639829d60bc6838a542e6", | ||||
|         "SecretKey": "4b000df2b31936e1ceb0aa48bbd4166214945bd7f83b85b26f9d164318587991", | ||||
|         "EnableSigned": true, | ||||
|         "EnableSsl": true | ||||
|       } | ||||
|     ] | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,24 @@ | ||||
| <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthorizationAppBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2ff26593f91746d7a53418a46dc419d1f200_003F4b_003F56550da2_003FAuthorizationAppBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AChapterData_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fe6_003F64a6c0f7_003FChapterData_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaim_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa7fdc52b6e574ae7b9822133be91162a15800_003Ff7_003Feebffd8d_003FClaim_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACorsPolicyBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F051ad509d0504b7ca10dedd9c2cabb9914200_003F8e_003Fb28257cb_003FCorsPolicyBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fa0b45f29f34f594814a7b1fbc25fe5ef3c18257956ed4f4fbfa68717db58_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADirectory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fde_003F94973e27_003FDirectory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointConventionBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F8a_003F101938e3_003FEndpointConventionBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4a28847852ee9ba45fd3107526c0a749a733bd4f4ebf33aa3c9a59737a3f758_003FEntityFrameworkServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnumerable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F832399abc13b45b6bdbabfa022e4a28487e00_003F7f_003F7aece4dd_003FEnumerable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEvents_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F20_003F86914b63_003FEvents_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AForwardedHeaders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcfe5737f9bb84738979cbfedd11822a8ea00_003F50_003F9a335f87_003FForwardedHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMediaAnalysis_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fd7_003F5c138865_003FMediaAnalysis_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFoundResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F28_003F290250f5_003FNotFoundResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFound_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff2c049af93e430aac427e8ff3cc9edd8763d5c9f006d7121ed1c5921585cba_003FNotFound_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOk_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01d30b32e2ff422cb80129ca2a441c4242600_003F3b_003F237bf104_003FOk_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOptionsConfigurationServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6622dea924b14dc7aa3ee69d7c84e5735000_003Fe0_003F024ba0b7_003FOptionsConfigurationServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASecuritySchemeType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F29898ce74e3763a786ac1bd9a6db2152e1af75769440b1e53b9cbdf1dda1bd99_003FSecuritySchemeType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc0e30e11d8f5456cb7a11b21ebee6c5a35c00_003F60_003F78b485f5_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATusDiskStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fe1_003Fefd9af34_003FTusDiskStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationContext_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F6b_003F741ceebe_003FValidationContext_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> | ||||
		Reference in New Issue
	
	Block a user