From c4ab90d4ebf1b817a44ffeda934840d63bd6a26b Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Sun, 9 Jun 2024 15:00:47 -0400 Subject: [PATCH 1/9] .NET 8 migration --- Certs/Signing.cs | 17 +++++++------ Constants/FileExtensions.cs | 13 ++++++++++ Controllers/Managers/SongManager.cs | 6 ++--- Controllers/Managers/TokenManager.cs | 31 ++++++++++++++++++++++-- Controllers/Utilities/SongCompression.cs | 2 +- Controllers/v1/BaseController.cs | 2 ++ Controllers/v1/SongStreamController.cs | 7 +++++- Icarus.csproj | 21 ++++++++-------- Models/CoverArt.cs | 2 +- Models/Song.cs | 4 +-- Program.cs | 4 +++ Startup.cs | 4 ++- 12 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 Constants/FileExtensions.cs diff --git a/Certs/Signing.cs b/Certs/Signing.cs index 80347fb..a6672a2 100644 --- a/Certs/Signing.cs +++ b/Certs/Signing.cs @@ -1,20 +1,21 @@ using System; -using System.Security.Cryptography; +// using System.Security.Cryptography; using Microsoft.IdentityModel.Tokens; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.OpenSsl; +// using BouncyCastle.Crypto; namespace Icarus.Certs; public class SigningIssuerCertificate : IDisposable { - private readonly RSACryptoServiceProvider _rsa; + private readonly System.Security.Cryptography.RSACryptoServiceProvider _rsa; public SigningIssuerCertificate() { - _rsa = new RSACryptoServiceProvider(); + _rsa = new System.Security.Cryptography.RSACryptoServiceProvider(); } public RsaSecurityKey GetIssuerSigningKey(string publicKeyPath) @@ -26,7 +27,7 @@ public class SigningIssuerCertificate : IDisposable { var pem = new PemReader(reader); var o = (RsaKeyParameters)pem.ReadObject(); - var parameters = new RSAParameters(); + var parameters = new System.Security.Cryptography.RSAParameters(); parameters.Modulus = o.Modulus.ToByteArray(); parameters.Exponent = o.Exponent.ToByteArray(); _rsa.ImportParameters(parameters); @@ -45,11 +46,11 @@ public class SigningIssuerCertificate : IDisposable public class SigningAudienceCertificate : IDisposable { - private readonly RSACryptoServiceProvider _rsa; + private readonly System.Security.Cryptography.RSACryptoServiceProvider _rsa; public SigningAudienceCertificate() { - _rsa = new RSACryptoServiceProvider(); + _rsa = new System.Security.Cryptography.RSACryptoServiceProvider(); } public SigningCredentials GetAudienceSigningKey(string keyPath) @@ -60,8 +61,10 @@ public class SigningAudienceCertificate : IDisposable using (var reader = System.IO.File.OpenText(file)) { var pem = new PemReader(reader); + // var pem = new Org.BouncyCastle.Utilities.IO.Pem.PemReader(reader); + // var pem = new BouncyCastle.OpenSsl.PemReader(reader); var o = (RsaKeyParameters)pem.ReadObject(); - var parameters = new RSAParameters(); + var parameters = new System.Security.Cryptography.RSAParameters(); parameters.Modulus = o.Modulus.ToByteArray(); parameters.Exponent = o.Exponent.ToByteArray(); _rsa.ImportParameters(parameters); diff --git a/Constants/FileExtensions.cs b/Constants/FileExtensions.cs new file mode 100644 index 0000000..9b252c5 --- /dev/null +++ b/Constants/FileExtensions.cs @@ -0,0 +1,13 @@ +namespace Icarus.Constants; + +public class FileExtensions +{ + // Contains file extension with period at the beginning + public static string MP3_EXTENSION = ".mp3"; + + // Contains file extension with period at the beginning + public static string WAV_EXTENSION = ".wav"; + + // Contains file extension with period at the beginning + public static string JPG_EXTENSION = ".jpg"; +} \ No newline at end of file diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index 34d4bfd..dafb215 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -243,10 +243,10 @@ public class SongManager : BaseManager var coverMgr = new CoverArtManager(_config); var meta = new MetadataRetriever(); var coverArt = coverMgr.SaveCoverArt(coverArtData, song); - meta.UpdateCoverArt(song, coverArt); - song.Duration = meta.RetrieveSongDuration(song.SongPath()); + // meta.UpdateCoverArt(song, coverArt); + // song.Duration = meta.RetrieveSongDuration(song.SongPath()); - meta.UpdateMetadata(song, song); + // meta.UpdateMetadata(song, song); DirectoryManager dirMgr = new DirectoryManager(_config, song); diff --git a/Controllers/Managers/TokenManager.cs b/Controllers/Managers/TokenManager.cs index 7d16f4a..004bb58 100644 --- a/Controllers/Managers/TokenManager.cs +++ b/Controllers/Managers/TokenManager.cs @@ -87,6 +87,7 @@ public class TokenManager : BaseManager } + /* [Obsolete("Asymmetric key signing for tokens have been deprecated")] public LoginResult LogIn(User user) { @@ -115,6 +116,7 @@ public class TokenManager : BaseManager Message = "Successfully retrieved token" }; } + */ public LoginResult LoginSymmetric(User user) { @@ -124,6 +126,7 @@ public class TokenManager : BaseManager var payload = Payload(); payload.Add(new System.Security.Claims.Claim("user_id", user.UserID.ToString(), ClaimValueTypes.Integer)); + /* var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:Secret"])); var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( @@ -132,8 +135,28 @@ public class TokenManager : BaseManager payload, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: signIn); + */ + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_config["JWT:Secret"]); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new Claim[] + { + new Claim("user_id", user.UserID.ToString(), ClaimValueTypes.Integer) + // Add more claims as needed + }), + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), + Issuer = _config["Jwt:Issuer"], // Add this line + Audience = _config["Jwt:Audience"] + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); - tokenResult.AccessToken = new JwtSecurityTokenHandler().WriteToken(token); + + // tokenResult.AccessToken = new JwtSecurityTokenHandler().WriteToken(token); + tokenResult.AccessToken = tokenHandler.WriteToken(token); var expClaim = payload.FirstOrDefault(cl => { @@ -152,6 +175,7 @@ public class TokenManager : BaseManager }; } + /* [Obsolete("Asymmetric key signing for tokens have been deprecated")] public bool IsTokenValid(string scope, string accessToken) { @@ -204,6 +228,7 @@ public class TokenManager : BaseManager return tok; } + */ private string AllScopes() @@ -266,6 +291,7 @@ public class TokenManager : BaseManager return claim; } + /* [Obsolete("Asymmetric key signing for tokens have been deprecated")] private string CreateToken(List claims, string privateKey) { @@ -331,7 +357,7 @@ public class TokenManager : BaseManager { using (var tr = new System.IO.StringReader(publicKey)) { - var pemReader = new PemReader(tr); + var pemReader = new Org.BouncyCastle.Crypto.PemReader(tr); var publicKeyParams = pemReader.ReadObject() as RsaKeyParameters; if (publicKeyParams == null) { @@ -340,6 +366,7 @@ public class TokenManager : BaseManager return DotNetUtilities.ToRSAParameters(publicKeyParams); } } + */ private async Task ReadKeyContent(string filepath) { diff --git a/Controllers/Utilities/SongCompression.cs b/Controllers/Utilities/SongCompression.cs index 469c128..1b4dca4 100644 --- a/Controllers/Utilities/SongCompression.cs +++ b/Controllers/Utilities/SongCompression.cs @@ -83,7 +83,7 @@ public class SongCompression Console.WriteLine(exMsg); } - if (songDetails.Filename.Contains(".mp3")) + if (songDetails.Filename.Contains(Constants.FileExtensions.WAV_EXTENSION)) { _compressedSongFilename = StripMP3Extension(songDetails.Filename); } diff --git a/Controllers/v1/BaseController.cs b/Controllers/v1/BaseController.cs index 40765c7..11cb5e5 100644 --- a/Controllers/v1/BaseController.cs +++ b/Controllers/v1/BaseController.cs @@ -39,6 +39,7 @@ public class BaseController : ControllerBase return token; } + /** [ApiExplorerSettings(IgnoreApi = true)] [Obsolete("Asymmetric key signing for tokens have been deprecated")] protected bool IsTokenValid(string scope) @@ -48,5 +49,6 @@ public class BaseController : ControllerBase return tokMgr.IsTokenValid(scope, token); } + */ #endregion } diff --git a/Controllers/v1/SongStreamController.cs b/Controllers/v1/SongStreamController.cs index 957a152..7e5aa07 100644 --- a/Controllers/v1/SongStreamController.cs +++ b/Controllers/v1/SongStreamController.cs @@ -53,7 +53,12 @@ public class SongStreamController : BaseController var stream = new FileStream(song.SongPath(), FileMode.Open, FileAccess.Read); stream.Position = 0; - var filename = $"{song.Title}.mp3"; + var filename = song.Filename; + + if (string.IsNullOrEmpty(song.Filename)) + { + filename = song.GenerateFilename(); + } _logger.LogInformation("Starting to stream song...>"); Console.WriteLine("Starting to streamsong..."); diff --git a/Icarus.csproj b/Icarus.csproj index b4164ab..0c7d70f 100644 --- a/Icarus.csproj +++ b/Icarus.csproj @@ -1,32 +1,33 @@ - net6 + net8 InProcess 0.1.10 + - - - - - + + + + + runtime; build; native; contentfiles; analyzers all - - + + - - + + diff --git a/Models/CoverArt.cs b/Models/CoverArt.cs index 8b3cc1a..fd54089 100644 --- a/Models/CoverArt.cs +++ b/Models/CoverArt.cs @@ -34,7 +34,7 @@ public class CoverArt var random = new Random(); var filename = new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); - var extension = ".mp3"; + var extension = Icarus.Constants.FileExtensions.JPG_EXTENSION; return (flag == 0) ? filename : $"{filename}{extension}"; } diff --git a/Models/Song.cs b/Models/Song.cs index 1fab172..5a76ce4 100644 --- a/Models/Song.cs +++ b/Models/Song.cs @@ -78,7 +78,7 @@ public class Song var random = new Random(); var filename = new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); - var extension = ".mp3"; + var extension = Icarus.Constants.FileExtensions.WAV_EXTENSION; return flag == 0 ? filename : $"{filename}{extension}"; } @@ -86,7 +86,7 @@ public class Song { const int length = 25; const string chars = "ABCDEF0123456789"; - var extension = ".mp3"; + var extension = Icarus.Constants.FileExtensions.WAV_EXTENSION; var random = new Random(); var filename = await Task.Run(() => { diff --git a/Program.cs b/Program.cs index 2fa72b3..7820275 100644 --- a/Program.cs +++ b/Program.cs @@ -41,5 +41,9 @@ public class Program .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); + webBuilder.UseKestrel(options => + { + options.Limits.MaxRequestBodySize = 262144000; + }); }); } diff --git a/Startup.cs b/Startup.cs index 0da5769..b957940 100644 --- a/Startup.cs +++ b/Startup.cs @@ -16,7 +16,7 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json; using NLog; using NLog.Web; -using NLog.Web.AspNetCore; +// using NLog.Web.AspNetCore; using Icarus.Database.Contexts; @@ -55,6 +55,8 @@ public class Startup { ValidateIssuer = true, ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = true, ValidAudience = Configuration["JWT:Audience"], ValidIssuer = Configuration["JWT:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"])) -- 2.47.3 From 76e31b4db5c3fae73689e6514bd0153376a230eb Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Tue, 11 Jun 2024 20:17:05 -0400 Subject: [PATCH 2/9] Fixed bugs with song management --- Controllers/Managers/CoverArtManager.cs | 1 + Controllers/Managers/SongManager.cs | 2 +- Controllers/v1/SongDataController.cs | 2 +- Models/CoverArt.cs | 6 ------ 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Controllers/Managers/CoverArtManager.cs b/Controllers/Managers/CoverArtManager.cs index a4e2441..33e093d 100644 --- a/Controllers/Managers/CoverArtManager.cs +++ b/Controllers/Managers/CoverArtManager.cs @@ -47,6 +47,7 @@ public class CoverArtManager : BaseManager { _logger.Info("Attempting to delete cover art from the database"); + _coverArtContext.Attach(coverArt); _coverArtContext.Remove(coverArt); _coverArtContext.SaveChanges(); } diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index dafb215..6797ef8 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -158,8 +158,8 @@ public class SongManager : BaseManager var coverArt = coverMgr.GetCoverArt(song); coverMgr.DeleteCoverArt(coverArt); - coverMgr.DeleteCoverArtFromDatabase(coverArt); DeleteSongFromDatabase(song); + coverMgr.DeleteCoverArtFromDatabase(coverArt); } catch (Exception ex) { diff --git a/Controllers/v1/SongDataController.cs b/Controllers/v1/SongDataController.cs index 6c24b94..5ae5529 100644 --- a/Controllers/v1/SongDataController.cs +++ b/Controllers/v1/SongDataController.cs @@ -148,7 +148,7 @@ public class SongDataController : BaseController _songMgr.DeleteSong(songMetaData); - return Ok(); + return Ok(songMetaData); } } diff --git a/Models/CoverArt.cs b/Models/CoverArt.cs index fd54089..4bf423f 100644 --- a/Models/CoverArt.cs +++ b/Models/CoverArt.cs @@ -17,12 +17,6 @@ public class CoverArt public string SongTitle { get; set; } [JsonIgnore] public string ImagePath { get; set; } - [JsonIgnore] - [NotMapped] - public int SongID { get; set; } - [JsonIgnore] - [NotMapped] - public List Songs { get; set; } #endregion -- 2.47.3 From 23b3c6ca737f683cbdcc8e1a5ec0fe3873b4e9c4 Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Wed, 12 Jun 2024 20:00:11 -0400 Subject: [PATCH 3/9] More cleanup and making some constants --- Constants/DirectoryPaths.cs | 3 +++ Database/Contexts/SongContext.cs | 32 -------------------------------- Database/Contexts/UserContext.cs | 5 ----- Models/Album.cs | 6 ------ Models/Artist.cs | 5 ----- Models/CoverArt.cs | 7 ++++--- Models/Genre.cs | 5 ----- Models/Song.cs | 8 ++++---- 8 files changed, 11 insertions(+), 60 deletions(-) diff --git a/Constants/DirectoryPaths.cs b/Constants/DirectoryPaths.cs index be323e5..1bb39dd 100644 --- a/Constants/DirectoryPaths.cs +++ b/Constants/DirectoryPaths.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.InteropServices; namespace Icarus.Constants; @@ -7,4 +8,6 @@ public class DirectoryPaths { public static string CoverArtPath => Directory.GetCurrentDirectory() + "/Images/Stock/CoverArt.png"; + public static string FILENAME_CHARACTERS = "ABCDEF0123456789"; + public static int FILENAME_LENGTH = 25; } diff --git a/Database/Contexts/SongContext.cs b/Database/Contexts/SongContext.cs index 3b49a05..ff7ed98 100644 --- a/Database/Contexts/SongContext.cs +++ b/Database/Contexts/SongContext.cs @@ -25,38 +25,6 @@ public class SongContext : DbContext modelBuilder.Entity() .ToTable("Song"); - /** - modelBuilder.Entity() - .HasOne(s => s.Album) - .WithMany(al => al.Songs) - .HasForeignKey(s => s.AlbumID) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(sa => sa.SongArtist) - .WithMany(ar => ar.Songs) - .HasForeignKey(s => s.ArtistID) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(s => s.SongGenre) - .WithMany(gnr => gnr.Songs) - .HasForeignKey(s => s.GenreID) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(s => s.SongYear) - .WithMany(yr => yr.Songs) - .HasForeignKey(s => s.YearID) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(s => s.SongCoverArt) - .WithMany(ca => ca.Songs) - .HasForeignKey(s => s.CoverArtID) - .OnDelete(DeleteBehavior.SetNull); - */ - modelBuilder.Entity() .Property(s => s.Year) .IsRequired(false); diff --git a/Database/Contexts/UserContext.cs b/Database/Contexts/UserContext.cs index 2ec1249..d83287e 100644 --- a/Database/Contexts/UserContext.cs +++ b/Database/Contexts/UserContext.cs @@ -2,11 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -// using MySql.Data.EntityFrameworkCore; -// using MySql.Data; -// using MySql.Data.EntityFrameworkCore.Extensions; -// using MySql.Data.Entity; -// using MySql.Data.MySqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/Models/Album.cs b/Models/Album.cs index 857aacf..7fe3d64 100644 --- a/Models/Album.cs +++ b/Models/Album.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; @@ -20,8 +18,4 @@ public class Album public int SongCount { get; set; } [JsonProperty("year")] public int Year { get; set; } - - [JsonIgnore] - [NotMapped] - public List Songs { get; set; } } diff --git a/Models/Artist.cs b/Models/Artist.cs index b4a8a19..ca66412 100644 --- a/Models/Artist.cs +++ b/Models/Artist.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; @@ -16,7 +14,4 @@ public class Artist [JsonProperty("song_count")] [NotMapped] public int SongCount { get; set; } - [JsonIgnore] - [NotMapped] - public List Songs { get; set; } } diff --git a/Models/CoverArt.cs b/Models/CoverArt.cs index 4bf423f..7b22816 100644 --- a/Models/CoverArt.cs +++ b/Models/CoverArt.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using System.Reflection.Metadata; using System.Text; using Newtonsoft.Json; @@ -23,12 +24,12 @@ public class CoverArt #region Methods public string GenerateFilename(int flag) { - const int length = 25; - const string chars = "ABCDEF0123456789"; + int length = Constants.DirectoryPaths.FILENAME_LENGTH; + string chars = Constants.DirectoryPaths.FILENAME_CHARACTERS; var random = new Random(); var filename = new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); - var extension = Icarus.Constants.FileExtensions.JPG_EXTENSION; + var extension = Constants.FileExtensions.JPG_EXTENSION; return (flag == 0) ? filename : $"{filename}{extension}"; } diff --git a/Models/Genre.cs b/Models/Genre.cs index b3ad87a..b157a5f 100644 --- a/Models/Genre.cs +++ b/Models/Genre.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; @@ -16,7 +14,4 @@ public class Genre [JsonProperty("song_count")] [NotMapped] public int SongCount { get; set; } - [JsonIgnore] - [NotMapped] - public List Songs { get; set; } } diff --git a/Models/Song.cs b/Models/Song.cs index 5a76ce4..828005e 100644 --- a/Models/Song.cs +++ b/Models/Song.cs @@ -73,8 +73,8 @@ public class Song public string GenerateFilename(int flag = 0) { - const int length = 25; - const string chars = "ABCDEF0123456789"; + int length = Constants.DirectoryPaths.FILENAME_LENGTH; + string chars = Constants.DirectoryPaths.FILENAME_CHARACTERS; var random = new Random(); var filename = new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); @@ -84,8 +84,8 @@ public class Song } public async Task GenerateFilenameAsync(int flag = 0) { - const int length = 25; - const string chars = "ABCDEF0123456789"; + int length = Constants.DirectoryPaths.FILENAME_LENGTH; + string chars = Constants.DirectoryPaths.FILENAME_CHARACTERS; var extension = Icarus.Constants.FileExtensions.WAV_EXTENSION; var random = new Random(); var filename = await Task.Run(() => -- 2.47.3 From 64c366472eef4dbc86f50f29ce6a8c385fd3a1af Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Wed, 12 Jun 2024 20:17:41 -0400 Subject: [PATCH 4/9] Updated yml --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index e5c95d6..f909f00 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build -- 2.47.3 From 48f9914a1f06c71dae7eadd5cb73b3a4b7354f11 Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Thu, 13 Jun 2024 20:35:53 -0400 Subject: [PATCH 5/9] Clean up Removing comments, some cleanup, and moving the startup code into Program.cs --- Certs/Signing.cs | 4 - Constants/DirectoryPaths.cs | 2 - Constants/FileExtensions.cs | 2 +- Controllers/Managers/SongManager.cs | 6 - Controllers/Managers/TokenManager.cs | 182 --------------------------- Controllers/v1/BaseController.cs | 13 -- Icarus.csproj | 3 +- Program.cs | 152 ++++++++++++++++------ Startup.cs | 127 ------------------- 9 files changed, 115 insertions(+), 376 deletions(-) delete mode 100644 Startup.cs diff --git a/Certs/Signing.cs b/Certs/Signing.cs index a6672a2..9185892 100644 --- a/Certs/Signing.cs +++ b/Certs/Signing.cs @@ -1,10 +1,8 @@ using System; -// using System.Security.Cryptography; using Microsoft.IdentityModel.Tokens; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.OpenSsl; -// using BouncyCastle.Crypto; namespace Icarus.Certs; @@ -61,8 +59,6 @@ public class SigningAudienceCertificate : IDisposable using (var reader = System.IO.File.OpenText(file)) { var pem = new PemReader(reader); - // var pem = new Org.BouncyCastle.Utilities.IO.Pem.PemReader(reader); - // var pem = new BouncyCastle.OpenSsl.PemReader(reader); var o = (RsaKeyParameters)pem.ReadObject(); var parameters = new System.Security.Cryptography.RSAParameters(); parameters.Modulus = o.Modulus.ToByteArray(); diff --git a/Constants/DirectoryPaths.cs b/Constants/DirectoryPaths.cs index 1bb39dd..8aab732 100644 --- a/Constants/DirectoryPaths.cs +++ b/Constants/DirectoryPaths.cs @@ -1,6 +1,4 @@ -using System; using System.IO; -using System.Runtime.InteropServices; namespace Icarus.Constants; diff --git a/Constants/FileExtensions.cs b/Constants/FileExtensions.cs index 9b252c5..281526e 100644 --- a/Constants/FileExtensions.cs +++ b/Constants/FileExtensions.cs @@ -10,4 +10,4 @@ public class FileExtensions // Contains file extension with period at the beginning public static string JPG_EXTENSION = ".jpg"; -} \ No newline at end of file +} diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index 6797ef8..a17c5f1 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -241,13 +241,7 @@ public class SongManager : BaseManager } var coverMgr = new CoverArtManager(_config); - var meta = new MetadataRetriever(); var coverArt = coverMgr.SaveCoverArt(coverArtData, song); - // meta.UpdateCoverArt(song, coverArt); - // song.Duration = meta.RetrieveSongDuration(song.SongPath()); - - // meta.UpdateMetadata(song, song); - DirectoryManager dirMgr = new DirectoryManager(_config, song); dirMgr.CreateDirectory(); diff --git a/Controllers/Managers/TokenManager.cs b/Controllers/Managers/TokenManager.cs index 004bb58..ece55fc 100644 --- a/Controllers/Managers/TokenManager.cs +++ b/Controllers/Managers/TokenManager.cs @@ -2,20 +2,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using System.Security.Cryptography; using System.Threading.Tasks; using System.IdentityModel.Tokens.Jwt; using System.Text; -using JWT; -using JWT.Serializers; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; -using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.OpenSsl; -using Org.BouncyCastle.Security; using RestSharp; using Icarus.Models; @@ -87,37 +80,6 @@ public class TokenManager : BaseManager } - /* - [Obsolete("Asymmetric key signing for tokens have been deprecated")] - public LoginResult LogIn(User user) - { - var tokenResult = new TokenTierOne(); - tokenResult.TokenType = "Jwt"; - - var privateKey = ReadKeyContent(_privateKeyPath).Result; - var publicKey = ReadKeyContent(_publicKeyPath).Result; - - var payload = Payload(); - - var token = CreateToken(payload, privateKey); - tokenResult.AccessToken = token; - - var expClaim = payload.FirstOrDefault(cl => - { - return cl.Type.Equals("exp"); - }); - - tokenResult.Expiration = System.Convert.ToInt32(expClaim.Value); - - return new LoginResult - { - UserID = user.UserID, Username = user.Username, Token = tokenResult.AccessToken, - TokenType = tokenResult.TokenType, Expiration = tokenResult.Expiration, - Message = "Successfully retrieved token" - }; - } - */ - public LoginResult LoginSymmetric(User user) { var tokenResult = new TokenTierOne(); @@ -126,17 +88,6 @@ public class TokenManager : BaseManager var payload = Payload(); payload.Add(new System.Security.Claims.Claim("user_id", user.UserID.ToString(), ClaimValueTypes.Integer)); - /* - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:Secret"])); - var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - var token = new JwtSecurityToken( - _config["JWT:Issuer"], - _config["JWT:Audience"], - payload, - expires: DateTime.UtcNow.AddMinutes(30), - signingCredentials: signIn); - */ - var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_config["JWT:Secret"]); var tokenDescriptor = new SecurityTokenDescriptor @@ -155,7 +106,6 @@ public class TokenManager : BaseManager var token = tokenHandler.CreateToken(tokenDescriptor); - // tokenResult.AccessToken = new JwtSecurityTokenHandler().WriteToken(token); tokenResult.AccessToken = tokenHandler.WriteToken(token); var expClaim = payload.FirstOrDefault(cl => @@ -175,61 +125,6 @@ public class TokenManager : BaseManager }; } - /* - [Obsolete("Asymmetric key signing for tokens have been deprecated")] - public bool IsTokenValid(string scope, string accessToken) - { - var result = false; - var token = DecodeToken(accessToken); - - if (token == null || token.Erroneous()) - { - return result; - } - - result = (!token.TokenExpired() && token.ContainsScope(scope)) ? true : false; - - // What would make a token valid? - // 1. The expiration date must be before the current date - // 2. The desired scope must be part of the scopes within the access token - // 3. Must be able to be decoded - - return result; - } - - [Obsolete("Asymmetric key signing for tokens have been deprecated")] - public Token DecodeToken(string accessToken) - { - var rsaParams = GetRSAPublic(_publicKey); - Token tok = null; - - try - { - using (var rsa = new RSACryptoServiceProvider()) - { - rsa.ImportParameters(rsaParams); - - IJsonSerializer serializer = new JsonNetSerializer(); - IDateTimeProvider provider = new UtcDateTimeProvider(); - IJwtValidator validator = new JwtValidator(serializer, provider); - IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); - var algorithm = new JWT.Algorithms.RS256Algorithm(rsa); - IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); - - var json = decoder.Decode(accessToken); - tok = JsonConvert.DeserializeObject(json); - } - } - catch (Exception ex) - { - _logger.Error("An error occurred: {0}", ex.Message); - } - - - return tok; - } - */ - private string AllScopes() { @@ -291,83 +186,6 @@ public class TokenManager : BaseManager return claim; } - /* - [Obsolete("Asymmetric key signing for tokens have been deprecated")] - private string CreateToken(List claims, string privateKey) - { - var token = string.Empty; - - if (string.IsNullOrEmpty(privateKey)) - { - privateKey = ReadKeyContent(_privateKeyPath).Result; - } - - RSAParameters rsaParams; - using (var tr = new System.IO.StringReader(privateKey)) - { - var pemReader = new PemReader(tr); - var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair; - if (keyPair == null) - { - throw new Exception("Could not read RSA private key"); - } - var privateRsaParams = keyPair.Private as RsaPrivateCrtKeyParameters; - rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams); - } - - using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) - { - var rsaParamsPublic = GetRSAPublic(ReadKeyContent(_publicKeyPath).Result); - var rsaPublic = new RSACryptoServiceProvider(); - - rsa.ImportParameters(rsaParams); - rsaPublic.ImportParameters(rsaParamsPublic); - - Dictionary payload = new Dictionary(); - - foreach (var claim in claims) - { - var type = claim.Type; - var val = Int32.TryParse(claim.Value, out _); - - if (val) - { - payload.Add(type, Convert.ToInt32(claim.Value)); - } - else - { - payload.Add(type, claim.Value); - } - - } - - var algorithm = new JWT.Algorithms.RS256Algorithm(rsaPublic, rsa); - IJsonSerializer serializer = new JsonNetSerializer(); - IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); - IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); - - token = encoder.Encode(payload, privateKey); - } - - return token; - } - - [Obsolete("Asymmetric key signing for tokens have been deprecated")] - private RSAParameters GetRSAPublic(string publicKey) - { - using (var tr = new System.IO.StringReader(publicKey)) - { - var pemReader = new Org.BouncyCastle.Crypto.PemReader(tr); - var publicKeyParams = pemReader.ReadObject() as RsaKeyParameters; - if (publicKeyParams == null) - { - throw new Exception("Could not read RSA public key"); - } - return DotNetUtilities.ToRSAParameters(publicKeyParams); - } - } - */ - private async Task ReadKeyContent(string filepath) { return await System.IO.File.ReadAllTextAsync(filepath); diff --git a/Controllers/v1/BaseController.cs b/Controllers/v1/BaseController.cs index 11cb5e5..093b470 100644 --- a/Controllers/v1/BaseController.cs +++ b/Controllers/v1/BaseController.cs @@ -4,8 +4,6 @@ using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; -using Icarus.Controllers.Managers; - namespace Icarus.Controllers.V1; @@ -39,16 +37,5 @@ public class BaseController : ControllerBase return token; } - /** - [ApiExplorerSettings(IgnoreApi = true)] - [Obsolete("Asymmetric key signing for tokens have been deprecated")] - protected bool IsTokenValid(string scope) - { - var token = ParseBearerTokenFromHeader(); - var tokMgr = new TokenManager(_config); - - return tokMgr.IsTokenValid(scope, token); - } - */ #endregion } diff --git a/Icarus.csproj b/Icarus.csproj index 0c7d70f..b5fada8 100644 --- a/Icarus.csproj +++ b/Icarus.csproj @@ -1,7 +1,8 @@ - net8 + net8.0 + enable InProcess 0.1.10 diff --git a/Program.cs b/Program.cs index 7820275..fb47ecb 100644 --- a/Program.cs +++ b/Program.cs @@ -1,49 +1,121 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using NLog.Web; +using Icarus.Database.Contexts; -namespace Icarus; -public class Program +var builder = WebApplication.CreateBuilder(args); + +var MAX_REQUEST_BODY_SIZE = 51200000000; + + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => { - public static void Main(string[] args) + c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); + + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Icarus", Version = "v1" }); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Name = "Authorization", + Scheme = "Bearer", + BearerFormat = "JWT", + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header, + Description = "Bearer *Auth Token*", + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement { - var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger(); - - try { - logger.Debug("init main"); - - CreateHostBuilder(args).Build().Run(); - } - catch (Exception ex) - { - logger.Error(ex, "An error occurred"); - throw; - } - finally - { - NLog.LogManager.Shutdown(); - } - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => + new OpenApiSecurityScheme { - webBuilder.UseStartup(); - webBuilder.UseKestrel(options => + Reference = new OpenApiReference { - options.Limits.MaxRequestBodySize = 262144000; - }); - }); + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); +}); + +builder.Services.AddControllers(); +builder.WebHost.UseKestrel(option => +{ + option.Limits.MaxRequestBodySize = MAX_REQUEST_BODY_SIZE; +}); + +var Configuration = builder.Configuration; + +var connString = Configuration.GetConnectionString("DefaultConnection"); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => +{ + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = true, + ValidAudience = Configuration["JWT:Audience"], + ValidIssuer = Configuration["JWT:Issuer"], + IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Configuration["JWT:Secret"])) + }; +}); + +builder.Services.AddDbContext(options => options.UseMySQL(connString)); +builder.Services.AddDbContext(options => options.UseMySQL(connString)); +builder.Services.AddDbContext(options => options.UseMySQL(connString)); +builder.Services.AddDbContext(options => options.UseMySQL(connString)); +builder.Services.AddDbContext(options => options.UseMySQL(connString)); +builder.Services.AddDbContext(options => options.UseMySQL(connString)); + +builder.Services.AddControllers() + .AddNewtonsoftJson(); + +builder.Services.AddEndpointsApiExplorer(); + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); } + +// NOTE: This should be enabled at some point +// app.UseHttpsRedirection(); + +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); +}); + +/** +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast") +.WithOpenApi(); +*/ + +app.Run(); diff --git a/Startup.cs b/Startup.cs deleted file mode 100644 index b957940..0000000 --- a/Startup.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Text; -using System.Linq; - -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using Microsoft.OpenApi.Models; -using Newtonsoft.Json; -using NLog; -using NLog.Web; -// using NLog.Web.AspNetCore; - -using Icarus.Database.Contexts; - -namespace Icarus; -public class Startup -{ - #region Constructors - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - #endregion - - #region Properties - public IConfiguration Configuration { get; } - #endregion - - - #region Methods - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - var auth_id = Configuration["Auth0:Domain"]; - var domain = $"https://{auth_id}/"; - var audience = Configuration["Auth0:ApiIdentifier"]; - - var connString = Configuration.GetConnectionString("DefaultConnection"); - - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => - { - options.RequireHttpsMetadata = false; - options.SaveToken = true; - options.TokenValidationParameters = new TokenValidationParameters() - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - ValidAudience = Configuration["JWT:Audience"], - ValidIssuer = Configuration["JWT:Issuer"], - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"])) - }; - }); - - services.AddDbContext(options => options.UseMySQL(connString)); - services.AddDbContext(options => options.UseMySQL(connString)); - services.AddDbContext(options => options.UseMySQL(connString)); - services.AddDbContext(options => options.UseMySQL(connString)); - services.AddDbContext(options => options.UseMySQL(connString)); - services.AddDbContext(options => options.UseMySQL(connString)); - - services.AddControllers() - .AddNewtonsoftJson(); - - services.AddEndpointsApiExplorer(); - services.AddSwaggerGen(c => - { - c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); - - c.SwaggerDoc("v1", new OpenApiInfo { Title = "Icarus", Version = "v1" }); - c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() - { - Name = "Authorization", - Scheme = "Bearer", - BearerFormat = "JWT", - Type = SecuritySchemeType.ApiKey, - In = ParameterLocation.Header, - Description = "Bearer *Auth Token*", - }); - c.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - new string[] {} - } - }); - }); - } - - // Called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - // NOTE: Dev-related configuration can be done when env.IsDevelopment() evaluated to true - if (env.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(); - } - - app.UseRouting(); - app.UseAuthentication(); - app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - #endregion -} -- 2.47.3 From 4cbb0b9429b9af6d6a4544593b446e89d000a9a5 Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Thu, 13 Jun 2024 20:38:04 -0400 Subject: [PATCH 6/9] Removing comments --- Program.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Program.cs b/Program.cs index fb47ecb..2b5db35 100644 --- a/Program.cs +++ b/Program.cs @@ -101,21 +101,5 @@ app.UseEndpoints(endpoints => endpoints.MapControllers(); }); -/** -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast") -.WithOpenApi(); -*/ app.Run(); -- 2.47.3 From 8c942a5320f704f25d07bc64ee5edc701965bedc Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Fri, 14 Jun 2024 19:22:26 -0400 Subject: [PATCH 7/9] Fixed song deletion issue --- Controllers/Managers/SongManager.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index a17c5f1..508b311 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -488,13 +488,12 @@ public class SongManager : BaseManager var albumMgr = new AlbumManager(_config); var artistMgr = new ArtistManager(_config); var genreMgr = new GenreManager(_config); - albumMgr.DeleteAlbumFromDatabase(song); - artistMgr.DeleteArtistFromDatabase(song); - genreMgr.DeleteGenreFromDatabase(song); - var sngContext = new SongContext(_connectionString); sngContext.Songs.Remove(song); sngContext.SaveChanges(); + artistMgr.DeleteArtistFromDatabase(song); + albumMgr.DeleteAlbumFromDatabase(song); + genreMgr.DeleteGenreFromDatabase(song); } #endregion } -- 2.47.3 From 55fc031e4c2b8e008fb28247fb940679703591a2 Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Fri, 14 Jun 2024 19:39:14 -0400 Subject: [PATCH 8/9] Added functionality to delete song directories --- Controllers/Managers/SongManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index 508b311..3fb3f52 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -145,7 +145,7 @@ public class SongManager : BaseManager { try { - if (DeleteSongFromFilesystem(song)) + if (DeleteSongFromFilesystem(song, true)) { _logger.Error("Failed to delete the song"); @@ -400,7 +400,7 @@ public class SongManager : BaseManager } - private bool DeleteSongFromFilesystem(Song song) + private bool DeleteSongFromFilesystem(Song song, bool deleteDirectory = false) { var songPath = song.SongPath(); @@ -409,6 +409,8 @@ public class SongManager : BaseManager try { System.IO.File.Delete(songPath); + + DeleteEmptyDirectories(ref song, ref song); } catch(Exception ex) { @@ -419,6 +421,7 @@ public class SongManager : BaseManager return DoesSongExistOnFilesystem(song); } + private bool DoesSongExistOnFilesystem(Song song) { if (!System.IO.File.Exists(song.SongPath())) -- 2.47.3 From e61be52704e4262ed543cd680d075e8faf785fd3 Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Sat, 15 Jun 2024 12:16:12 -0400 Subject: [PATCH 9/9] Updated README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c3bb084..5e5362c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ # Icarus -Icarus is a music streaming API Server that interacts with [Mear](https://github.com/amazing-username/mear). +Icarus is a music streaming API Server that interacts with [Mear](https://github.com/kdeng00/mear). ### Interfacing With Icarus One can interface with Icarus the music server either by: -* [Mear](https://github.com/amazing-username/mear) - Partially implemented (under development) -* [IcarusDownloadManager](https://github.com/amazing-username/IcarusDownloadManager) +* [Mear](https://github.com/kdeng00/mear) - Partially implemented (under development) +* [IcarusDownloadManager](https://github.com/kdeng00/IcarusDownloadManager) ## Built With -* C# [.NET](https://dotnet.microsoft.com/) 6 +* C# [.NET](https://dotnet.microsoft.com/) 8 * [MySql](https://www.nuget.org/packages/MySql.Data/) * OpenSSL * BCrypt.Net-Next @@ -120,7 +120,7 @@ Prior to starting the API, the Migrations must be applied. There are 6 tables wi * Year * Genre -There is a script for Linux systems to apply these migrations, it can be found in the [Scripts/Migrations/Linux](https://github.com/amazing-username/Icarus/blob/master/Scripts/Migrations/Linux/AddUpdate.sh) directory. Just merely execute: +There is a script for Linux systems to apply these migrations, it can be found in the [Scripts/Migrations/Linux](https://github.com/kdeng00/Icarus/blob/master/Scripts/Migrations/Linux/AddUpdate.sh) directory. Just merely execute: ```shell scripts/Migrations/Linux/AddUpdate.sh ``` -- 2.47.3