Compare commits
No commits in common. "8704305f5ad3d280f488bc79ac5982120b9e4a13" and "31d98199e7adbad2326e31a567818e94edf8d1ac" have entirely different histories.
8704305f5a
...
31d98199e7
1
DysonNetwork.Sphere/.gitignore
vendored
1
DysonNetwork.Sphere/.gitignore
vendored
@ -1,2 +1 @@
|
||||
Keys
|
||||
Uploads
|
@ -7,7 +7,7 @@ namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/accounts")]
|
||||
public class AccountController(AppDatabase db, IHttpContextAccessor httpContext) : ControllerBase
|
||||
public class AccountController(AppDatabase db, IHttpContextAccessor httpContext)
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||
@ -37,7 +37,7 @@ public class AccountController(AppDatabase db, IHttpContextAccessor httpContext)
|
||||
{
|
||||
var dupeNameCount = await db.Accounts.Where(a => a.Name == request.Name).CountAsync();
|
||||
if (dupeNameCount > 0)
|
||||
return BadRequest("The name is already taken.");
|
||||
return new BadRequestObjectResult("The name is already taken.");
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
@ -77,6 +77,6 @@ public class AccountController(AppDatabase db, IHttpContextAccessor httpContext)
|
||||
|
||||
var account = await db.Accounts.FindAsync(userId);
|
||||
|
||||
return Ok(account);
|
||||
return new OkObjectResult(account);
|
||||
}
|
||||
}
|
@ -22,7 +22,6 @@ 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)
|
||||
{
|
||||
|
@ -10,12 +10,7 @@ namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
[ApiController]
|
||||
[Route("/auth")]
|
||||
public class AuthController(
|
||||
AppDatabase db,
|
||||
AccountService accounts,
|
||||
AuthService auth,
|
||||
IHttpContextAccessor httpContext
|
||||
) : ControllerBase
|
||||
public class AuthController(AppDatabase db, AccountService accounts, AuthService auth, IHttpContextAccessor httpContext)
|
||||
{
|
||||
public class ChallengeRequest
|
||||
{
|
||||
@ -29,7 +24,7 @@ public class AuthController(
|
||||
public async Task<ActionResult<Challenge>> StartChallenge([FromBody] ChallengeRequest request)
|
||||
{
|
||||
var account = await accounts.LookupAccount(request.Account);
|
||||
if (account is null) return NotFound("Account was not found.");
|
||||
if (account is null) return new NotFoundObjectResult("Account was not found.");
|
||||
|
||||
var ipAddress = httpContext.HttpContext?.Connection.RemoteIpAddress?.ToString();
|
||||
var userAgent = httpContext.HttpContext?.Request.Headers.UserAgent.ToString();
|
||||
@ -71,7 +66,7 @@ public class AuthController(
|
||||
.Include(e => e.Account.AuthFactors)
|
||||
.Where(e => e.Id == id).FirstOrDefaultAsync();
|
||||
return challenge is null
|
||||
? NotFound("Auth challenge was not found.")
|
||||
? new NotFoundObjectResult("Auth challenge was not found.")
|
||||
: challenge.Account.AuthFactors.ToList();
|
||||
}
|
||||
|
||||
@ -88,14 +83,14 @@ public class AuthController(
|
||||
)
|
||||
{
|
||||
var challenge = await db.AuthChallenges.FindAsync(id);
|
||||
if (challenge is null) return NotFound("Auth challenge was not found.");
|
||||
if (challenge is null) return new NotFoundObjectResult("Auth challenge was not found.");
|
||||
|
||||
var factor = await db.AccountAuthFactors.FindAsync(request.FactorId);
|
||||
if (factor is null) return NotFound("Auth factor was not found.");
|
||||
if (factor is null) return new NotFoundObjectResult("Auth factor was not found.");
|
||||
|
||||
if (challenge.StepRemain == 0) return challenge;
|
||||
if (challenge.ExpiredAt.HasValue && challenge.ExpiredAt.Value < Instant.FromDateTimeUtc(DateTime.UtcNow))
|
||||
return BadRequest();
|
||||
return new BadRequestResult();
|
||||
|
||||
try
|
||||
{
|
||||
@ -107,7 +102,7 @@ public class AuthController(
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest();
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
@ -130,21 +125,21 @@ public class AuthController(
|
||||
case "authorization_code":
|
||||
var code = Guid.TryParse(request.Code, out var codeId) ? codeId : Guid.Empty;
|
||||
if (code == Guid.Empty)
|
||||
return BadRequest("Invalid or missing authorization code.");
|
||||
return new BadRequestObjectResult("Invalid or missing authorization code.");
|
||||
var challenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.Where(e => e.Id == code)
|
||||
.FirstOrDefaultAsync();
|
||||
if (challenge is null)
|
||||
return BadRequest("Authorization code not found or expired.");
|
||||
return new NotFoundObjectResult("Authorization code not found or expired.");
|
||||
if (challenge.StepRemain != 0)
|
||||
return BadRequest("Challenge not yet completed.");
|
||||
return new BadRequestObjectResult("Challenge not yet completed.");
|
||||
|
||||
session = await db.AuthSessions
|
||||
.Where(e => e.Challenge == challenge)
|
||||
.FirstOrDefaultAsync();
|
||||
if (session is not null)
|
||||
return BadRequest("Session already exists for this challenge.");
|
||||
return new BadRequestObjectResult("Session already exists for this challenge.");
|
||||
|
||||
session = new Session
|
||||
{
|
||||
@ -164,21 +159,21 @@ public class AuthController(
|
||||
var sessionIdClaim = token.Claims.FirstOrDefault(c => c.Type == "session_id")?.Value;
|
||||
|
||||
if (!Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
return Unauthorized("Invalid or missing session_id claim in refresh token.");
|
||||
return new UnauthorizedObjectResult("Invalid or missing session_id claim in refresh token.");
|
||||
|
||||
session = await db.AuthSessions
|
||||
.Include(e => e.Account)
|
||||
.Include(e => e.Challenge)
|
||||
.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
if (session is null)
|
||||
return NotFound("Session not found or expired.");
|
||||
return new NotFoundObjectResult("Session not found or expired.");
|
||||
|
||||
session.LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return auth.CreateToken(session);
|
||||
default:
|
||||
return BadRequest("Unsupported grant type.");
|
||||
return new BadRequestObjectResult("Unsupported grant type.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,11 +183,11 @@ public class AuthController(
|
||||
{
|
||||
var sessionIdClaim = httpContext.HttpContext?.User.FindFirst("session_id")?.Value;
|
||||
if (!Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
return Unauthorized();
|
||||
return new UnauthorizedResult();
|
||||
|
||||
var session = await db.AuthSessions.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
if (session is null) return NotFound();
|
||||
if (session is null) return new NotFoundResult();
|
||||
|
||||
return Ok(session);
|
||||
return new OkObjectResult(session);
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@
|
||||
<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">
|
||||
@ -21,11 +20,6 @@
|
||||
<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" />
|
||||
@ -33,7 +27,6 @@
|
||||
<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>
|
||||
@ -42,8 +35,4 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Uploads\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,454 +0,0 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
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,83 +289,6 @@ 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")
|
||||
@ -423,18 +346,6 @@ 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,4 +1,3 @@
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@ -7,24 +6,16 @@ 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.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
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);
|
||||
|
||||
builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddDbContext<AppDatabase>();
|
||||
@ -115,7 +106,6 @@ builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddScoped<AccountService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<FileService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@ -139,95 +129,11 @@ 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 =>
|
||||
{
|
||||
if (eventContext.Intent == IntentType.DeleteFile)
|
||||
{
|
||||
eventContext.FailRequest(
|
||||
HttpStatusCode.BadRequest,
|
||||
"Deleting files from this endpoint was disabled, please refer to the Dyson Network File API."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var jsonOptions = httpContext.RequestServices.GetRequiredService<IOptions<JsonOptions>>().Value
|
||||
.JsonSerializerOptions;
|
||||
var infoJson = JsonSerializer.Serialize(info, jsonOptions);
|
||||
eventContext.HttpContext.Response.Headers.Append("X-FileInfo", infoJson);
|
||||
|
||||
#pragma warning disable CS4014
|
||||
Task.Run(async () =>
|
||||
{
|
||||
using var scope = eventContext.HttpContext.RequestServices
|
||||
.GetRequiredService<IServiceScopeFactory>()
|
||||
.CreateScope();
|
||||
// Keep the service didn't be disposed
|
||||
var fs = scope.ServiceProvider.GetRequiredService<FileService>();
|
||||
// Keep the file stream opened
|
||||
var fileData = await tusDiskStore.GetFileAsync(file.Id, CancellationToken.None);
|
||||
var newStream = await fileData.GetContentAsync(CancellationToken.None);
|
||||
await fs.UploadFileToRemoteAsync(info, newStream, null);
|
||||
await tusDiskStore.DeleteFileAsync(file.Id, CancellationToken.None);
|
||||
});
|
||||
#pragma warning restore CS4014
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
||||
app.Run();
|
@ -1,59 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Minio.DataModel.Args;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
|
||||
[ApiController]
|
||||
[Route("/files")]
|
||||
public class FileController(
|
||||
AppDatabase db,
|
||||
FileService fs,
|
||||
IConfiguration configuration,
|
||||
IWebHostEnvironment env
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult> OpenFile(string id)
|
||||
{
|
||||
var file = await db.Files.FindAsync(id);
|
||||
if (file is null) return NotFound();
|
||||
|
||||
if (file.UploadedTo is null)
|
||||
{
|
||||
var tusStorePath = configuration.GetValue<string>("Tus:StorePath")!;
|
||||
var filePath = Path.Combine(env.ContentRootPath, tusStorePath, file.Id);
|
||||
if (!System.IO.File.Exists(filePath)) return new NotFoundResult();
|
||||
return PhysicalFile(filePath, file.MimeType ?? "application/octet-stream", file.Name);
|
||||
}
|
||||
|
||||
var dest = fs.GetRemoteStorageConfig(file.UploadedTo);
|
||||
|
||||
if (dest.ImageProxy is not null && (file.MimeType?.StartsWith("image/") ?? false))
|
||||
{
|
||||
var proxyUrl = dest.ImageProxy;
|
||||
var baseUri = new Uri(proxyUrl.EndsWith('/') ? proxyUrl : $"{proxyUrl}/");
|
||||
var fullUri = new Uri(baseUri, file.Id);
|
||||
return Redirect(fullUri.ToString());
|
||||
}
|
||||
|
||||
if (dest.AccessProxy is not null)
|
||||
{
|
||||
var proxyUrl = dest.AccessProxy;
|
||||
var baseUri = new Uri(proxyUrl.EndsWith('/') ? proxyUrl : $"{proxyUrl}/");
|
||||
var fullUri = new Uri(baseUri, file.Id);
|
||||
return Redirect(fullUri.ToString());
|
||||
}
|
||||
|
||||
if (dest.EnableSigned)
|
||||
{
|
||||
var client = fs.CreateMinioClient(dest);
|
||||
if (client is null)
|
||||
return BadRequest(
|
||||
"Failed to configure client for remote destination, file got an invalid storage remote.");
|
||||
|
||||
var bucket = dest.Bucket;
|
||||
var openUrl = await client.PresignedGetObjectAsync(
|
||||
new PresignedGetObjectArgs()
|
||||
.WithBucket(bucket)
|
||||
.WithObject(file.Id)
|
||||
.WithExpiry(3600)
|
||||
);
|
||||
|
||||
return Redirect(openUrl);
|
||||
}
|
||||
|
||||
// Fallback redirect to the S3 endpoint (public read)
|
||||
var protocol = dest.EnableSsl ? "https" : "http";
|
||||
// Use the path bucket lookup mode
|
||||
return Redirect($"{protocol}://{dest.Endpoint}/{dest.Bucket}/{file.Id}");
|
||||
}
|
||||
|
||||
[HttpGet("{id}/info")]
|
||||
public async Task<ActionResult<CloudFile>> GetFileInfo(string id)
|
||||
{
|
||||
var file = await db.Files.FindAsync(id);
|
||||
if (file is null) return NotFound();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> DeleteFile(string id)
|
||||
{
|
||||
var userIdClaim = User.FindFirst("user_id")?.Value;
|
||||
if (userIdClaim is null) return Unauthorized();
|
||||
var userId = long.Parse(userIdClaim);
|
||||
|
||||
var file = await db.Files
|
||||
.Where(e => e.Id == id)
|
||||
.Where(e => e.Account.Id == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (file is null) return NotFound();
|
||||
|
||||
await fs.DeleteFileDataAsync(file);
|
||||
|
||||
db.Files.Remove(file);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
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);
|
||||
db.Update(file);
|
||||
await db.SaveChangesAsync();
|
||||
return file;
|
||||
}
|
||||
|
||||
public async Task DeleteFileDataAsync(CloudFile file)
|
||||
{
|
||||
if (file.UploadedTo is null) return;
|
||||
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;
|
||||
await client.RemoveObjectAsync(
|
||||
new RemoveObjectArgs().WithBucket(bucket).WithObject(file.Id)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public 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,25 +24,5 @@
|
||||
"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,31 +1,14 @@
|
||||
<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_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd515fb889657fcdcace3fed90735057b458ff9e0bb60bded7c8fe8b3a4673c_003FBucketArgs_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_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_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_003AFileResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F8c_003F9f6e3f4f_003FFileResult_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_003AIntentType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fbf_003Ffcb84131_003FIntentType_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_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fd3_003F7b05b2bd_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedGetObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F0df26a9d89e29319e9efcaea0a8489db9e97bc1aedcca3f7e360cc50f8f4ea_003FPresignedGetObjectArgs_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>
|
||||
<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_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5d2c480da9be415dab9be535bb6d08713cc00_003Fd0_003Fffc36a51_003FUri_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>
|
||||
<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>
|
Loading…
x
Reference in New Issue
Block a user