diff --git a/Moonlight/App/Database/Entities/Server.cs b/Moonlight/App/Database/Entities/Server.cs index aa55d81..bea519d 100644 --- a/Moonlight/App/Database/Entities/Server.cs +++ b/Moonlight/App/Database/Entities/Server.cs @@ -13,6 +13,8 @@ public class Server public string OverrideStartup { get; set; } = ""; public bool Installing { get; set; } = false; public bool Suspended { get; set; } = false; + public bool IsArchived { get; set; } = false; + public ServerBackup? Archive { get; set; } = null; public List Variables { get; set; } = new(); public List Backups { get; set; } = new(); diff --git a/Moonlight/App/Database/Migrations/20230614010621_AddedServerAchive.Designer.cs b/Moonlight/App/Database/Migrations/20230614010621_AddedServerAchive.Designer.cs new file mode 100644 index 0000000..42978d0 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230614010621_AddedServerAchive.Designer.cs @@ -0,0 +1,1073 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.App.Database; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20230614010621_AddedServerAchive")] + partial class AddedServerAchive + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ApiKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ApiUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Host") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("CloudPanels"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("bigint"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Ongoing") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.ToTable("DdosAttacks"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("DockerImages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("SharedDomainId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("SharedDomainId"); + + b.ToTable("Domains"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Allocations") + .HasColumnType("int"); + + b.Property("BackgroundImageUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ConfigFiles") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallEntrypoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Startup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartupDetection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TagsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("LoadingMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.AuditLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("AuditLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.ErrorLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Class") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Stacktrace") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("ErrorLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.SecurityLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SecurityLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WebSpaceId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WebSpaceId"); + + b.ToTable("Databases"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NewsEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Markdown") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("NewsEntries"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HttpPort") + .HasColumnType("int"); + + b.Property("MoonlightDaemonPort") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SftpPort") + .HasColumnType("int"); + + b.Property("Ssl") + .HasColumnType("tinyint(1)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TokenId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServerId"); + + b.ToTable("NodeAllocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NotificationClientId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NotificationClientId"); + + b.ToTable("NotificationActions"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationClients"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Identifier") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Revokes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ArchiveId") + .HasColumnType("int"); + + b.Property("Cpu") + .HasColumnType("int"); + + b.Property("Disk") + .HasColumnType("bigint"); + + b.Property("DockerImageIndex") + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Installing") + .HasColumnType("tinyint(1)"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("IsCleanupException") + .HasColumnType("tinyint(1)"); + + b.Property("MainAllocationId") + .HasColumnType("int"); + + b.Property("Memory") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("OverrideStartup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Suspended") + .HasColumnType("tinyint(1)"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ArchiveId"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Bytes") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerBackups"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SharedDomain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CloudflareId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("SharedDomains"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.StatisticsData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Chart") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("Statistics"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LimitsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Attachment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("QuestionType") + .HasColumnType("int"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("SupportChatMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Admin") + .HasColumnType("tinyint(1)"); + + b.Property("City") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Country") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentSubscriptionId") + .HasColumnType("int"); + + b.Property("DiscordId") + .HasColumnType("bigint unsigned"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HasRated") + .HasColumnType("tinyint(1)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastVisitedAt") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubscriptionDuration") + .HasColumnType("int"); + + b.Property("SubscriptionSince") + .HasColumnType("datetime(6)"); + + b.Property("SupportPending") + .HasColumnType("tinyint(1)"); + + b.Property("TokenValidTime") + .HasColumnType("datetime(6)"); + + b.Property("TotpEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("TotpSecret") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurrentSubscriptionId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CloudPanelId") + .HasColumnType("int"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VHostTemplate") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CloudPanelId"); + + b.HasIndex("OwnerId"); + + b.ToTable("WebSpaces"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => + { + b.HasOne("Moonlight.App.Database.Entities.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Node"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("DockerImages") + .HasForeignKey("ImageId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.SharedDomain", "SharedDomain") + .WithMany() + .HasForeignKey("SharedDomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("SharedDomain"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("Variables") + .HasForeignKey("ImageId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => + { + b.HasOne("Moonlight.App.Database.Entities.WebSpace", "WebSpace") + .WithMany("Databases") + .HasForeignKey("WebSpaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WebSpace"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.HasOne("Moonlight.App.Database.Entities.Node", null) + .WithMany("Allocations") + .HasForeignKey("NodeId"); + + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b => + { + b.HasOne("Moonlight.App.Database.Entities.Notification.NotificationClient", "NotificationClient") + .WithMany() + .HasForeignKey("NotificationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationClient"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.HasOne("Moonlight.App.Database.Entities.ServerBackup", "Archive") + .WithMany() + .HasForeignKey("ArchiveId"); + + b.HasOne("Moonlight.App.Database.Entities.Image", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.NodeAllocation", "MainAllocation") + .WithMany() + .HasForeignKey("MainAllocationId"); + + b.HasOne("Moonlight.App.Database.Entities.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Node"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Backups") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.Navigation("Recipient"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription") + .WithMany() + .HasForeignKey("CurrentSubscriptionId"); + + b.Navigation("CurrentSubscription"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.HasOne("Moonlight.App.Database.Entities.CloudPanel", "CloudPanel") + .WithMany() + .HasForeignKey("CloudPanelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CloudPanel"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Backups"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.Navigation("Databases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20230614010621_AddedServerAchive.cs b/Moonlight/App/Database/Migrations/20230614010621_AddedServerAchive.cs new file mode 100644 index 0000000..033be1a --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230614010621_AddedServerAchive.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddedServerAchive : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ArchiveId", + table: "Servers", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsArchived", + table: "Servers", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateIndex( + name: "IX_Servers_ArchiveId", + table: "Servers", + column: "ArchiveId"); + + migrationBuilder.AddForeignKey( + name: "FK_Servers_ServerBackups_ArchiveId", + table: "Servers", + column: "ArchiveId", + principalTable: "ServerBackups", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Servers_ServerBackups_ArchiveId", + table: "Servers"); + + migrationBuilder.DropIndex( + name: "IX_Servers_ArchiveId", + table: "Servers"); + + migrationBuilder.DropColumn( + name: "ArchiveId", + table: "Servers"); + + migrationBuilder.DropColumn( + name: "IsArchived", + table: "Servers"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index 18d134d..f4aaf20 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -496,6 +496,9 @@ namespace Moonlight.App.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); + b.Property("ArchiveId") + .HasColumnType("int"); + b.Property("Cpu") .HasColumnType("int"); @@ -511,6 +514,9 @@ namespace Moonlight.App.Database.Migrations b.Property("Installing") .HasColumnType("tinyint(1)"); + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + b.Property("IsCleanupException") .HasColumnType("tinyint(1)"); @@ -542,6 +548,8 @@ namespace Moonlight.App.Database.Migrations b.HasKey("Id"); + b.HasIndex("ArchiveId"); + b.HasIndex("ImageId"); b.HasIndex("MainAllocationId"); @@ -935,6 +943,10 @@ namespace Moonlight.App.Database.Migrations modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => { + b.HasOne("Moonlight.App.Database.Entities.ServerBackup", "Archive") + .WithMany() + .HasForeignKey("ArchiveId"); + b.HasOne("Moonlight.App.Database.Entities.Image", "Image") .WithMany() .HasForeignKey("ImageId") @@ -957,6 +969,8 @@ namespace Moonlight.App.Database.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Archive"); + b.Navigation("Image"); b.Navigation("MainAllocation"); diff --git a/Moonlight/App/Events/EventSystem.cs b/Moonlight/App/Events/EventSystem.cs index 58b27de..7ae2da7 100644 --- a/Moonlight/App/Events/EventSystem.cs +++ b/Moonlight/App/Events/EventSystem.cs @@ -112,4 +112,22 @@ public class EventSystem return Task.CompletedTask; } + + public Task WaitForEvent(string id, object handle, Func filter) + { + var taskCompletionSource = new TaskCompletionSource(); + + Func action = async data => + { + if (filter.Invoke(data)) + { + taskCompletionSource.SetResult(data); + await Off(id, handle); + } + }; + + On(id, handle, action); + + return taskCompletionSource.Task; + } } \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/ServerImageDataModel.cs b/Moonlight/App/Models/Forms/ServerImageDataModel.cs new file mode 100644 index 0000000..e881b79 --- /dev/null +++ b/Moonlight/App/Models/Forms/ServerImageDataModel.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Models.Forms; + +public class ServerImageDataModel +{ + public string OverrideStartup { get; set; } + + public int DockerImageIndex { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/ServerOverviewDataModel.cs b/Moonlight/App/Models/Forms/ServerOverviewDataModel.cs new file mode 100644 index 0000000..2079970 --- /dev/null +++ b/Moonlight/App/Models/Forms/ServerOverviewDataModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Models.Forms; + +public class ServerOverviewDataModel +{ + [Required(ErrorMessage = "You need to enter a name")] + [MaxLength(32, ErrorMessage = "The name cannot be longer that 32 characters")] + public string Name { get; set; } + + [Required(ErrorMessage = "You need to specify a owner")] + public User Owner { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/ServerResourcesDataModel.cs b/Moonlight/App/Models/Forms/ServerResourcesDataModel.cs new file mode 100644 index 0000000..87f358e --- /dev/null +++ b/Moonlight/App/Models/Forms/ServerResourcesDataModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.App.Models.Forms; + +public class ServerResourcesDataModel +{ + [Required(ErrorMessage = "You need to specify the cpu cores")] + public int Cpu { get; set; } + + [Required(ErrorMessage = "You need to specify the memory")] + public long Memory { get; set; } + + [Required(ErrorMessage = "You need to specify the disk")] + public long Disk { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Services/NodeService.cs b/Moonlight/App/Services/NodeService.cs index 1a4d028..57a5636 100644 --- a/Moonlight/App/Services/NodeService.cs +++ b/Moonlight/App/Services/NodeService.cs @@ -72,7 +72,7 @@ public class NodeService { try { - await GetSystemMetrics(node); + await GetStatus(node); return true; } diff --git a/Moonlight/App/Services/ServerService.cs b/Moonlight/App/Services/ServerService.cs index 6452594..8972169 100644 --- a/Moonlight/App/Services/ServerService.cs +++ b/Moonlight/App/Services/ServerService.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using Logging.Net; +using Microsoft.EntityFrameworkCore; using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings.Requests; using Moonlight.App.ApiClients.Wings.Resources; @@ -446,4 +447,65 @@ public class ServerService return await NodeService.IsHostUp(server.Node); } + + public async Task ArchiveServer(Server server) + { + if (server.IsArchived) + throw new DisplayException("Unable to archive an already archived server"); + + // Archive server + + var backup = await CreateBackup(server); + server.IsArchived = true; + server.Archive = backup; + + ServerRepository.Update(server); + + await Event.WaitForEvent("wings.backups.create", this, x => backup.Id == x.Id); + + // Reset server + + var access = await CreateFileAccess(server, null!); + var files = await access.Ls(); + foreach (var file in files) + { + try + { + await access.Delete(file); + } + catch (Exception) + { + // ignored + } + } + + await Event.Emit($"server.{server.Uuid}.archiveStatusChanged", server); + } + + public async Task UnArchiveServer(Server s) + { + if (!s.IsArchived) + throw new DisplayException("Unable to unarchive a server which is not archived"); + + var server = ServerRepository + .Get() + .Include(x => x.Archive) + .First(x => x.Id == s.Id); + + if (server.Archive == null) + throw new DisplayException("Archive from server not found"); + + if (!server.Archive.Created) + throw new DisplayException("Creating the server archive is in progress"); + + await RestoreBackup(server, server.Archive); + + await Event.WaitForEvent("wings.backups.restore", this, + x => x.Id == server.Archive.Id); + + server.IsArchived = false; + ServerRepository.Update(server); + + await Event.Emit($"server.{server.Uuid}.archiveStatusChanged", server); + } } \ No newline at end of file diff --git a/Moonlight/Shared/Components/Navigations/AdminServersViewNavigation.razor b/Moonlight/Shared/Components/Navigations/AdminServersViewNavigation.razor new file mode 100644 index 0000000..40814fe --- /dev/null +++ b/Moonlight/Shared/Components/Navigations/AdminServersViewNavigation.razor @@ -0,0 +1,51 @@ +@using Moonlight.App.Database.Entities + + +@code +{ + [Parameter] + public int Index { get; set; } + + [Parameter] + public Server Server { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Router/Route.razor b/Moonlight/Shared/Components/Router/Route.razor new file mode 100644 index 0000000..255d2ab --- /dev/null +++ b/Moonlight/Shared/Components/Router/Route.razor @@ -0,0 +1,20 @@ +@{ + var route = "/" + (Router.Route ?? ""); +} + +@if (route == Path) +{ + @ChildContent +} + +@code +{ + [CascadingParameter] + public SmartRouter Router { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + [Parameter] + public string Path { get; set; } +} diff --git a/Moonlight/Shared/Components/Router/SmartRouter.razor b/Moonlight/Shared/Components/Router/SmartRouter.razor new file mode 100644 index 0000000..e633533 --- /dev/null +++ b/Moonlight/Shared/Components/Router/SmartRouter.razor @@ -0,0 +1,12 @@ + + @ChildContent + + +@code +{ + [Parameter] + public string? Route { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor index aff316a..23d8018 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor @@ -57,7 +57,7 @@ @if (User.Admin) { - @(CurrentServer.Id) + @(CurrentServer.Id) } else { diff --git a/Moonlight/Shared/Views/Admin/Servers/Edit.razor b/Moonlight/Shared/Views/Admin/Servers/Edit.razor index fb06eee..2014c19 100644 --- a/Moonlight/Shared/Views/Admin/Servers/Edit.razor +++ b/Moonlight/Shared/Views/Admin/Servers/Edit.razor @@ -1,4 +1,4 @@ -@page "/admin/servers/edit/{id:int}" +@page "/admin/servers/editx/{id:int}" @using Moonlight.App.Services @using Moonlight.App.Repositories.Servers diff --git a/Moonlight/Shared/Views/Admin/Servers/Index.razor b/Moonlight/Shared/Views/Admin/Servers/Index.razor index c4da20a..d71c299 100644 --- a/Moonlight/Shared/Views/Admin/Servers/Index.razor +++ b/Moonlight/Shared/Views/Admin/Servers/Index.razor @@ -44,7 +44,7 @@ diff --git a/Moonlight/Shared/Views/Admin/Servers/View/Archive.razor b/Moonlight/Shared/Views/Admin/Servers/View/Archive.razor new file mode 100644 index 0000000..719cf96 --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Servers/View/Archive.razor @@ -0,0 +1,68 @@ +@using Moonlight.App.Database.Entities +@using Moonlight.App.Services +@using Moonlight.App.Services.Interop + +@inject ServerService ServerService +@inject SmartTranslateService SmartTranslateService +@inject AlertService AlertService + +
+
+ @if (Server.IsArchived) + { + Server is currently archived + } + else + { + Server is currently not archived + } +
+ +
+ +@code +{ + [CascadingParameter] + public Server Server { get; set; } + + private async Task ArchiveServer() + { + await ServerService.ArchiveServer(Server); + + await InvokeAsync(StateHasChanged); + + await AlertService.Success( + SmartTranslateService.Translate("Successfully archived the server") + ); + } + + private async Task UnArchiveServer() + { + await ServerService.UnArchiveServer(Server); + + await InvokeAsync(StateHasChanged); + + await AlertService.Success( + SmartTranslateService.Translate("Successfully unarchived the server") + ); + } +} diff --git a/Moonlight/Shared/Views/Admin/Servers/View/Debug.razor b/Moonlight/Shared/Views/Admin/Servers/View/Debug.razor new file mode 100644 index 0000000..95d8c59 --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Servers/View/Debug.razor @@ -0,0 +1,31 @@ +@using Moonlight.App.Database.Entities +@using Moonlight.App.Services + +@inject ServerService ServerService +@inject SmartTranslateService SmartTranslateService + +
+
+ + Reinstall + +
+ +
+ +@code +{ + [CascadingParameter] + public Server Server { get; set; } + + private async Task Reinstall() + { + await ServerService.Reinstall(Server!); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Servers/View/Image.razor b/Moonlight/Shared/Views/Admin/Servers/View/Image.razor new file mode 100644 index 0000000..29d8500 --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Servers/View/Image.razor @@ -0,0 +1,111 @@ +@using Moonlight.App.Database.Entities +@using Moonlight.App.Repositories +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Models.Forms +@using Mappy.Net +@using Moonlight.App.Services +@using Moonlight.App.Services.Interop + +@inject Repository ImageRepository +@inject Repository ServerRepository +@inject SmartTranslateService SmartTranslateService +@inject ToastService ToastService + + + +
+
+ +
+ + + + +
+ + +
+
+ @foreach (var vars in Server.Variables.Chunk(4)) + { +
+ @foreach (var variable in vars) + { +
+
+ +
+ +
+ +
+ +
+
+
+ } +
+ } +
+ +
+
+
+ +@code +{ + [CascadingParameter] + public Server Server { get; set; } + + private List DockerImages; + private List Images; + + private ServerImageDataModel Model; + + private Task Load(LazyLoader arg) + { + Images = ImageRepository + .Get() + .Include(x => x.Variables) + .Include(x => x.DockerImages) + .ToList(); + + DockerImages = Images + .First(x => x.Id == Server.Image.Id).DockerImages + .ToList(); + + Model = Mapper.Map(Server); + + return Task.CompletedTask; + } + + private async Task OnValidSubmit() + { + Server = Mapper.Map(Server, Model); + + ServerRepository.Update(Server); + + await ToastService.Success( + SmartTranslateService.Translate("Successfully saved changes") + ); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Servers/View/Index.razor b/Moonlight/Shared/Views/Admin/Servers/View/Index.razor new file mode 100644 index 0000000..cf2bc28 --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Servers/View/Index.razor @@ -0,0 +1,79 @@ +@page "/admin/servers/view/{Id:int}/{Route?}" + +@using Moonlight.App.Repositories +@using Moonlight.App.Database.Entities +@using Microsoft.EntityFrameworkCore +@using Moonlight.Shared.Components.Navigations + +@inject Repository ServerRepository + + + + @if (Server == null) + { +
+ No server with this id found +
+ } + else + { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } +
+
+ +@code +{ + [Parameter] + public string? Route { get; set; } + + [Parameter] + public int Id { get; set; } + + private LazyLoader LazyLoader; + private Server? Server; + + private Task Load(LazyLoader arg) + { + Server = ServerRepository + .Get() + .Include(x => x.Image) + .Include(x => x.Owner) + .Include(x => x.Archive) + .Include(x => x.Allocations) + .Include(x => x.MainAllocation) + .Include(x => x.Variables) + .FirstOrDefault(x => x.Id == Id); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Servers/View/Overview.razor b/Moonlight/Shared/Views/Admin/Servers/View/Overview.razor new file mode 100644 index 0000000..8990cdd --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Servers/View/Overview.razor @@ -0,0 +1,91 @@ +@using Moonlight.App.Repositories +@using Moonlight.App.Database.Entities +@using Moonlight.App.Models.Forms +@using Moonlight.App.Services +@using Moonlight.App.Services.Interop +@using Mappy.Net + +@inject Repository UserRepository +@inject Repository ServerRepository +@inject ToastService ToastService +@inject SmartTranslateService SmartTranslateService + + + +
+
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + +
+
+ +
+
+
+ +@code +{ + [CascadingParameter] + public Server Server { get; set; } + + private ServerOverviewDataModel Model; + private User[] Users; + + private Task Load(LazyLoader arg) + { + Users = UserRepository.Get().ToArray(); + + Model = Mapper.Map(Server); + + return Task.CompletedTask; + } + + private async Task OnValidSubmit() + { + Server = Mapper.Map(Server, Model); + ServerRepository.Update(Server); + + await ToastService.Success( + SmartTranslateService.Translate("Successfully saved changes") + ); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Servers/View/Resources.razor b/Moonlight/Shared/Views/Admin/Servers/View/Resources.razor new file mode 100644 index 0000000..4acc93a --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Servers/View/Resources.razor @@ -0,0 +1,88 @@ +@using Moonlight.App.Database.Entities +@using Moonlight.App.Models.Forms +@using Moonlight.App.Repositories +@using Moonlight.App.Services +@using Moonlight.App.Services.Interop +@using Mappy.Net + +@inject Repository ServerRepository +@inject SmartTranslateService SmartTranslateService +@inject ToastService ToastService + + + +
+
+ +
+ + + + + + CPU Cores (100% = 1 Core) + +
+ +
+ + + + + + MB + +
+ +
+ + + + + + MB + +
+
+ +
+
+
+ +@code +{ + [CascadingParameter] + public Server Server { get; set; } + + private ServerResourcesDataModel Model; + + private Task Load(LazyLoader arg) + { + Model = Mapper.Map(Server); + + return Task.CompletedTask; + } + + private async Task OnValidSubmit() + { + Server = Mapper.Map(Server, Model); + + ServerRepository.Update(Server); + + await ToastService.Success( + SmartTranslateService.Translate("Successfully saved changes") + ); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Server/Index.razor b/Moonlight/Shared/Views/Server/Index.razor index 7d6c09a..2a83a24 100644 --- a/Moonlight/Shared/Views/Server/Index.razor +++ b/Moonlight/Shared/Views/Server/Index.razor @@ -22,6 +22,7 @@ @inject ServerService ServerService @inject NavigationManager NavigationManager @inject DynamicBackgroundService DynamicBackgroundService +@inject SmartTranslateService SmartTranslateService @implements IDisposable @@ -78,6 +79,28 @@ } + else if (CurrentServer.IsArchived) + { +
+
+ Not found image +
+

+ Server is currently archived +

+

+ This server is archived. The data of this server is stored as a backup. To restore click the unarchive button an be patient +

+ + + +
+
+
+ } else { @@ -236,7 +259,7 @@ } catch (Exception) { - // ignored + // ignored } if (CurrentServer != null) @@ -260,8 +283,8 @@ .Include(x => x.Variables) .First(x => x.Id == CurrentServer.Image.Id); - // Live variable migration - + // Live variable migration + foreach (var variable in image.Variables) { if (!CurrentServer.Variables.Any(x => x.Key == variable.Key)) @@ -271,13 +294,13 @@ Key = variable.Key, Value = variable.DefaultValue }); - + ServerRepository.Update(CurrentServer); } } - - // Tags - + + // Tags + await lazyLoader.SetText("Requesting tags"); Tags = JsonConvert.DeserializeObject(image.TagsJson) ?? Array.Empty(); @@ -294,6 +317,13 @@ return Task.CompletedTask; }); + await Event.On($"server.{CurrentServer.Uuid}.archiveStatusChanged", this, server => + { + NavigationManager.NavigateTo(NavigationManager.Uri, true); + + return Task.CompletedTask; + }); + if (string.IsNullOrEmpty(Image.BackgroundImageUrl)) await DynamicBackgroundService.Reset(); else @@ -305,7 +335,7 @@ Logger.Debug("Server is null"); } } - + private async Task ReconnectConsole() { await Console!.Disconnect(); @@ -317,6 +347,7 @@ if (CurrentServer != null) { await Event.Off($"server.{CurrentServer.Uuid}.installComplete", this); + await Event.Off($"server.{CurrentServer.Uuid}.archiveStatusChanged", this); } if (Console != null) @@ -324,4 +355,9 @@ Console.Dispose(); } } + + private async Task UnArchive() + { + await ServerService.UnArchiveServer(CurrentServer!); + } } \ No newline at end of file diff --git a/Moonlight/_Imports.razor b/Moonlight/_Imports.razor index 8998fc6..585b789 100644 --- a/Moonlight/_Imports.razor +++ b/Moonlight/_Imports.razor @@ -11,4 +11,5 @@ @using Moonlight.Shared.Components.StateLogic @using Moonlight.Shared.Components.Alerts @using Moonlight.Shared.Components.Forms -@using Moonlight.Shared.Components.Partials \ No newline at end of file +@using Moonlight.Shared.Components.Partials +@using Moonlight.Shared.Components.Router \ No newline at end of file diff --git a/Moonlight/wwwroot/assets/media/svg/archive.svg b/Moonlight/wwwroot/assets/media/svg/archive.svg new file mode 100644 index 0000000..6878f5b --- /dev/null +++ b/Moonlight/wwwroot/assets/media/svg/archive.svg @@ -0,0 +1 @@ +heavy_box \ No newline at end of file