Merge branch 'redistributables'

media
Pat Hartl 2023-10-27 18:54:50 -05:00
commit ff9ec5a17b
30 changed files with 4042 additions and 36 deletions

View File

@ -50,7 +50,7 @@ namespace LANCommander.PlaynitePlugin
var result = RetryHelper.RetryOnException<ExtractionResult>(10, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () =>
{
Logger.Trace("Attempting to download and extract game...");
return DownloadAndExtract(game);
return DownloadAndExtractGame(game);
});
if (!result.Success && !result.Canceled)
@ -88,6 +88,12 @@ namespace LANCommander.PlaynitePlugin
SaveScript(game, result.Directory, ScriptType.NameChange);
SaveScript(game, result.Directory, ScriptType.KeyChange);
if (game.Redistributables != null && game.Redistributables.Count() > 0)
{
Logger.Trace("Installing required redistributables...");
InstallRedistributables(game);
}
try
{
PowerShellRuntime.RunScript(PlayniteGame, ScriptType.Install);
@ -106,7 +112,7 @@ namespace LANCommander.PlaynitePlugin
InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
}
private ExtractionResult DownloadAndExtract(LANCommander.SDK.Models.Game game)
private ExtractionResult DownloadAndExtractGame(LANCommander.SDK.Models.Game game)
{
if (game == null)
{
@ -204,6 +210,153 @@ namespace LANCommander.PlaynitePlugin
return extractionResult;
}
private void InstallRedistributables(LANCommander.SDK.Models.Game game)
{
foreach (var redistributable in game.Redistributables)
{
string installScriptTempFile = null;
string detectionScriptTempFile = null;
string extractTempPath = null;
try
{
var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install);
installScriptTempFile = SaveTempScript(installScript);
var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall);
detectionScriptTempFile = SaveTempScript(detectionScript);
var detectionResult = PowerShellRuntime.RunScript(detectionScriptTempFile, detectionScript.RequiresAdmin);
// Redistributable is not installed
if (detectionResult == 0)
{
var extractionResult = DownloadAndExtractRedistributable(redistributable);
if (extractionResult.Success)
{
extractTempPath = extractionResult.Directory;
PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath);
}
}
}
catch (Exception ex)
{
Logger.Error(ex, $"Redistributable {redistributable.Name} failed to install");
}
finally
{
if (File.Exists(installScriptTempFile))
File.Delete(installScriptTempFile);
if (File.Exists(detectionScriptTempFile))
File.Delete(detectionScriptTempFile);
if (Directory.Exists(extractTempPath))
Directory.Delete(extractTempPath);
}
}
}
private ExtractionResult DownloadAndExtractRedistributable(LANCommander.SDK.Models.Redistributable redistributable)
{
if (redistributable == null)
{
Logger.Trace("Redistributable failed to download! No redistributable was specified!");
throw new Exception("Redistributable failed to download!");
}
var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename());
Logger.Trace($"Downloading and extracting \"{redistributable.Name}\" to path {destination}");
var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
try
{
Directory.CreateDirectory(destination);
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
using (var redistributableStream = Plugin.LANCommander.StreamRedistributable(redistributable.Id))
using (var reader = ReaderFactory.Open(redistributableStream))
{
progress.ProgressMaxValue = redistributableStream.Length;
redistributableStream.OnProgress += (pos, len) =>
{
progress.CurrentProgressValue = pos;
};
reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
{
if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
{
reader.Cancel();
progress.IsIndeterminate = true;
reader.Dispose();
redistributableStream.Dispose();
}
};
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
}
catch (Exception ex)
{
if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
{
Logger.Trace("User cancelled the download");
if (Directory.Exists(destination))
{
Logger.Trace("Cleaning up orphaned install files after cancelled install...");
Directory.Delete(destination, true);
}
}
else
{
Logger.Error(ex, $"Could not extract to path {destination}");
if (Directory.Exists(destination))
{
Logger.Trace("Cleaning up orphaned install files after bad install...");
Directory.Delete(destination, true);
}
throw new Exception("The redistributable archive could not be extracted. Please try again or fix the archive!");
}
}
},
new GlobalProgressOptions($"Downloading {redistributable.Name}...")
{
IsIndeterminate = false,
Cancelable = true,
});
var extractionResult = new ExtractionResult
{
Canceled = result.Canceled
};
if (!result.Canceled)
{
extractionResult.Success = true;
extractionResult.Directory = destination;
Logger.Trace($"Redistributable successfully downloaded and extracted to {destination}");
}
return extractionResult;
}
private string Download(LANCommander.SDK.Models.Game game)
{
string tempFile = String.Empty;
@ -294,6 +447,21 @@ namespace LANCommander.PlaynitePlugin
File.WriteAllText(destination, yaml);
}
private string SaveTempScript(LANCommander.SDK.Models.Script script)
{
var tempPath = Path.GetTempFileName();
File.Move(tempPath, tempPath + ".ps1");
tempPath = tempPath + ".ps1";
Logger.Trace($"Writing script {script.Name} to {tempPath}");
File.WriteAllText(tempPath, script.Contents);
return tempPath;
}
private void SaveScript(LANCommander.SDK.Models.Game game, string installationDirectory, ScriptType type)
{
var script = game.Scripts.FirstOrDefault(s => s.Type == type);

View File

@ -206,6 +206,11 @@ namespace LANCommander.PlaynitePlugin
return DownloadRequest($"/api/Archives/Download/{id}", progressHandler, completeHandler);
}
public TrackableStream StreamRedistributable(Guid id)
{
return StreamRequest($"/api/Redistributables/{id}/Download");
}
public string DownloadSave(Guid id, Action<DownloadProgressChangedEventArgs> progressHandler, Action<AsyncCompletedEventArgs> completeHandler)
{
return DownloadRequest($"/api/Saves/Download/{id}", progressHandler, completeHandler);

View File

@ -39,7 +39,7 @@ namespace LANCommander.PlaynitePlugin
File.Delete(tempScript);
}
public void RunScript(string path, bool asAdmin = false, string arguments = null)
public int RunScript(string path, bool asAdmin = false, string arguments = null, string workingDirectory = null)
{
Logger.Trace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}");
@ -58,6 +58,9 @@ namespace LANCommander.PlaynitePlugin
if (arguments != null)
process.StartInfo.Arguments += " " + arguments;
if (workingDirectory != null)
process.StartInfo.WorkingDirectory = workingDirectory;
if (asAdmin)
{
process.StartInfo.Verb = "runas";
@ -68,6 +71,8 @@ namespace LANCommander.PlaynitePlugin
process.WaitForExit();
Wow64RevertWow64FsRedirection(ref wow64Value);
return process.ExitCode;
}
public void RunScript(Game game, ScriptType type, string arguments = null)

View File

@ -5,6 +5,9 @@
Install,
Uninstall,
NameChange,
KeyChange
KeyChange,
SaveUpload,
SaveDownload,
DetectInstall
}
}

View File

@ -16,5 +16,6 @@ namespace LANCommander.SDK.Models
public virtual Company Developer { get; set; }
public virtual IEnumerable<Archive> Archives { get; set; }
public virtual IEnumerable<Script> Scripts { get; set; }
public virtual IEnumerable<Redistributable> Redistributables { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace LANCommander.SDK.Models
{
public class Redistributable : BaseModel
{
public string Name { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public DateTime ReleasedOn { get; set; }
public virtual IEnumerable<Archive> Archives { get; set; }
public virtual IEnumerable<Script> Scripts { get; set; }
}
}

View File

@ -11,7 +11,7 @@
<Space Direction="DirectionVHType.Vertical" Style="width: 100%">
<SpaceItem>
<Table TItem="Archive" DataSource="@Game.Archives.OrderByDescending(a => a.CreatedOn)" HidePagination="true" Responsive>
<Table TItem="Archive" DataSource="@Archives.OrderByDescending(a => a.CreatedOn)" HidePagination="true" Responsive>
<PropertyColumn Property="a => a.Version" />
<PropertyColumn Property="a => a.CompressedSize">
@ByteSizeLib.ByteSize.FromBytes(context.CompressedSize)
@ -23,7 +23,7 @@
<ActionColumn Title="">
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<a href="/Download/Game/@context.Id" target="_blank" class="ant-btn ant-btn-text ant-btn-icon-only">
<a href="/Download/Archive/@context.Id" target="_blank" class="ant-btn ant-btn-text ant-btn-icon-only">
<Icon Type="@IconType.Outline.Download" />
</a>
</SpaceItem>
@ -49,7 +49,10 @@
<ArchiveUploader @ref="Uploader" OnArchiveUploaded="AddArchive" />
@code {
[Parameter] public Game Game { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
[Parameter] public ICollection<Archive> Archives { get; set; }
[Parameter] public EventCallback<ICollection<Archive>> ArchivesChanged { get; set; }
Archive Archive;
ArchiveUploader Uploader;
@ -63,10 +66,7 @@
private async Task LoadData()
{
Game.Archives = await ArchiveService.Get(a => a.GameId == Game.Id).OrderByDescending(a => a.CreatedOn).ToListAsync();
if (Game.Archives == null)
Game.Archives = new List<Archive>();
Archives = await ArchiveService.Get(a => a.GameId == GameId).OrderByDescending(a => a.CreatedOn).ToListAsync();
}
private async Task Download(Archive archive)
@ -78,18 +78,18 @@
private async Task UploadArchive()
{
Archive = new Archive()
{
GameId = Game.Id,
Id = Guid.NewGuid()
};
if (GameId != Guid.Empty)
Archive = new Archive() { GameId = GameId, Id = Guid.NewGuid() };
if (RedistributableId != Guid.Empty)
Archive = new Archive() { RedistributableId = RedistributableId, Id = Guid.NewGuid() };
await Uploader.Open(Archive);
}
private async Task AddArchive(Archive archive)
{
var lastArchive = Game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault();
var lastArchive = Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault();
Archive = await ArchiveService.Add(archive);

View File

@ -56,7 +56,7 @@
</FormItem>
<FormItem Label="Type">
<Select @bind-Value="context.Type" TItem="ScriptType" TItemValue="ScriptType" DataSource="Enum.GetValues<ScriptType>()">
<Select @bind-Value="context.Type" TItem="ScriptType" TItemValue="ScriptType" DataSource="Enum.GetValues<ScriptType>().Where(st => AllowedTypes == null || AllowedTypes.Contains(st))">
<LabelTemplate Context="Value">@Value.GetDisplayName()</LabelTemplate>
<ItemTemplate Context="Value">@Value.GetDisplayName()</ItemTemplate>
</Select>
@ -111,11 +111,13 @@
}
</style>
@code {
@code {
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public ICollection<Script> Scripts { get; set; }
[Parameter] public EventCallback<ICollection<Script>> ScriptsChanged { get; set; }
[Parameter] public IEnumerable<ScriptType> AllowedTypes { get; set; }
Script Script;
@ -150,10 +152,11 @@
private async void Edit(Script script = null)
{
if (script == null) {
Script = new Script()
{
GameId = GameId
};
if (GameId != Guid.Empty)
Script = new Script() { GameId = GameId };
if (RedistributableId != Guid.Empty)
Script = new Script() { RedistributableId = RedistributableId };
if (Editor != null)
await Editor.SetValue("");

View File

@ -0,0 +1,37 @@
@typeparam TItem where TItem : BaseModel
<Transfer DataSource="TransferItems" TargetKeys="TargetKeys" OnChange="OnChange" Titles="new string[] { LeftTitle, RightTitle }" />
@code {
[Parameter] public string LeftTitle { get; set; } = "";
[Parameter] public string RightTitle { get; set; } = "";
[Parameter] public Func<TItem, string> TitleSelector { get; set; }
[Parameter] public IEnumerable<TItem> DataSource { get; set; }
[Parameter] public ICollection<TItem> Values { get; set; } = new List<TItem>();
[Parameter] public EventCallback<ICollection<TItem>> ValuesChanged { get; set; }
IEnumerable<TransferItem> TransferItems { get; set; } = new List<TransferItem>();
List<string> TargetKeys { get; set; } = new List<string>();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
TransferItems = DataSource.Select(i => new TransferItem()
{
Key = i.Id.ToString(),
Title = TitleSelector.Invoke(i)
});
TargetKeys = Values.Select(i => i.Id.ToString()).ToList();
}
}
async Task OnChange(TransferChangeArgs e)
{
Values = DataSource.Where(i => e.TargetKeys.Contains(i.Id.ToString())).ToList();
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(Values);
}
}

View File

@ -0,0 +1,57 @@
using LANCommander.Data.Models;
using LANCommander.Extensions;
using LANCommander.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers.Api
{
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
[ApiController]
public class RedistributableController : ControllerBase
{
private readonly RedistributableService RedistributableService;
private readonly LANCommanderSettings Settings = SettingService.GetSettings();
public RedistributableController(RedistributableService redistributableService)
{
RedistributableService = redistributableService;
}
[HttpGet]
public async Task<IEnumerable<Redistributable>> Get()
{
return await RedistributableService.Get();
}
[HttpGet("{id}")]
public async Task<Redistributable> Get(Guid id)
{
return await RedistributableService.Get(id);
}
[HttpGet("{id}/Download")]
public async Task<IActionResult> Download(Guid id)
{
var redistributable = await RedistributableService.Get(id);
if (redistributable == null)
return NotFound();
if (redistributable.Archives == null || redistributable.Archives.Count == 0)
return NotFound();
var archive = redistributable.Archives.OrderByDescending(a => a.CreatedOn).First();
var filename = Path.Combine(Settings.Archives.StoragePath, archive.ObjectKey);
if (!System.IO.File.Exists(filename))
return NotFound();
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{redistributable.Name.SanitizeFilename()}.zip");
}
}
}

View File

@ -19,7 +19,7 @@ namespace LANCommander.Controllers
ArchiveService = archiveService;
}
public async Task<IActionResult> Game(Guid id)
public async Task<IActionResult> Archive(Guid id)
{
var archive = await ArchiveService.Get(id);

View File

@ -37,13 +37,13 @@ namespace LANCommander.Data
builder.Entity<Game>()
.HasMany(g => g.Archives)
.WithOne(g => g.Game)
.IsRequired(true)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Game>()
.HasMany(g => g.Scripts)
.WithOne(s => s.Game)
.IsRequired(true)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Game>()
@ -81,6 +81,15 @@ namespace LANCommander.Data
g => g.HasOne<Game>().WithMany().HasForeignKey("GameId")
);
builder.Entity<Game>()
.HasMany(g => g.Redistributables)
.WithMany(r => r.Games)
.UsingEntity<Dictionary<string, object>>(
"GameRedistributable",
gr => gr.HasOne<Redistributable>().WithMany().HasForeignKey("RedistributableId"),
gr => gr.HasOne<Game>().WithMany().HasForeignKey("GameId")
);
builder.Entity<User>()
.HasMany(u => u.GameSaves)
.WithOne(gs => gs.User)
@ -104,6 +113,18 @@ namespace LANCommander.Data
.WithOne(sl => sl.Server)
.IsRequired(true)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Redistributable>()
.HasMany(r => r.Archives)
.WithOne(a => a.Redistributable)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Redistributable>()
.HasMany(r => r.Scripts)
.WithOne(s => s.Redistributable)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
}
public DbSet<Game>? Games { get; set; }
@ -123,5 +144,7 @@ namespace LANCommander.Data
public DbSet<Server>? Servers { get; set; }
public DbSet<ServerConsole>? ServerConsoles { get; set; }
public DbSet<Redistributable>? Redistributables { get; set; }
}
}

View File

@ -13,6 +13,8 @@ namespace LANCommander.Data.Enums
[Display(Name = "Save Upload")]
SaveUpload,
[Display(Name = "Save Download")]
SaveDownload
SaveDownload,
[Display(Name = "Detect Install")]
DetectInstall
}
}

View File

@ -13,12 +13,18 @@ namespace LANCommander.Data.Models
[Required]
public string Version { get; set; }
public Guid GameId { get; set; }
public Guid? GameId { get; set; }
[JsonIgnore]
[ForeignKey(nameof(GameId))]
[InverseProperty("Archives")]
public virtual Game? Game { get; set; }
public Guid? RedistributableId { get; set; }
[JsonIgnore]
[ForeignKey(nameof(RedistributableId))]
[InverseProperty("Archives")]
public virtual Redistributable? Redistributable { get; set; }
[Display(Name = "Last Version")]
public virtual Archive? LastVersion { get; set; }

View File

@ -34,6 +34,7 @@ namespace LANCommander.Data.Models
public virtual ICollection<GameSave>? GameSaves { get; set; }
public virtual ICollection<SavePath>? SavePaths { get; set; }
public virtual ICollection<Server>? Servers { get; set; }
public virtual ICollection<Redistributable>? Redistributables { get; set; }
public string? ValidKeyRegex { get; set; }
public virtual ICollection<Key>? Keys { get; set; }

View File

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace LANCommander.Data.Models
{
[Table("Redistributables")]
public class Redistributable : BaseModel
{
public string Name { get; set; }
public string? Description { get; set; }
public string? Notes { get; set; }
public virtual ICollection<Archive>? Archives { get; set; }
public virtual ICollection<Script>? Scripts { get; set; }
public virtual ICollection<Game>? Games { get; set; }
}
}

View File

@ -18,5 +18,11 @@ namespace LANCommander.Data.Models
[ForeignKey(nameof(GameId))]
[InverseProperty("Scripts")]
public virtual Game? Game { get; set; }
public Guid? RedistributableId { get; set; }
[JsonIgnore]
[ForeignKey(nameof(RedistributableId))]
[InverseProperty("Scripts")]
public virtual Redistributable? Redistributable { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,158 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddRedistributables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<Guid>(
name: "GameId",
table: "Scripts",
type: "TEXT",
nullable: true,
oldClrType: typeof(Guid),
oldType: "TEXT");
migrationBuilder.AddColumn<Guid>(
name: "RedistributableId",
table: "Scripts",
type: "TEXT",
nullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "GameId",
table: "Archive",
type: "TEXT",
nullable: true,
oldClrType: typeof(Guid),
oldType: "TEXT");
migrationBuilder.AddColumn<Guid>(
name: "RedistributableId",
table: "Archive",
type: "TEXT",
nullable: true);
migrationBuilder.CreateTable(
name: "Redistributables",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: true),
Notes = table.Column<string>(type: "TEXT", nullable: true),
CreatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedById = table.Column<Guid>(type: "TEXT", nullable: true),
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedById = table.Column<Guid>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Redistributables", x => x.Id);
table.ForeignKey(
name: "FK_Redistributables_AspNetUsers_CreatedById",
column: x => x.CreatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Redistributables_AspNetUsers_UpdatedById",
column: x => x.UpdatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_Scripts_RedistributableId",
table: "Scripts",
column: "RedistributableId");
migrationBuilder.CreateIndex(
name: "IX_Archive_RedistributableId",
table: "Archive",
column: "RedistributableId");
migrationBuilder.CreateIndex(
name: "IX_Redistributables_CreatedById",
table: "Redistributables",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_Redistributables_UpdatedById",
table: "Redistributables",
column: "UpdatedById");
migrationBuilder.AddForeignKey(
name: "FK_Archive_Redistributables_RedistributableId",
table: "Archive",
column: "RedistributableId",
principalTable: "Redistributables",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Scripts_Redistributables_RedistributableId",
table: "Scripts",
column: "RedistributableId",
principalTable: "Redistributables",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Archive_Redistributables_RedistributableId",
table: "Archive");
migrationBuilder.DropForeignKey(
name: "FK_Scripts_Redistributables_RedistributableId",
table: "Scripts");
migrationBuilder.DropTable(
name: "Redistributables");
migrationBuilder.DropIndex(
name: "IX_Scripts_RedistributableId",
table: "Scripts");
migrationBuilder.DropIndex(
name: "IX_Archive_RedistributableId",
table: "Archive");
migrationBuilder.DropColumn(
name: "RedistributableId",
table: "Scripts");
migrationBuilder.DropColumn(
name: "RedistributableId",
table: "Archive");
migrationBuilder.AlterColumn<Guid>(
name: "GameId",
table: "Scripts",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "GameId",
table: "Archive",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "TEXT",
oldNullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddGameRedistributableRelationship : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "GameRedistributable",
columns: table => new
{
GameId = table.Column<Guid>(type: "TEXT", nullable: false),
RedistributableId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GameRedistributable", x => new { x.GameId, x.RedistributableId });
table.ForeignKey(
name: "FK_GameRedistributable_Games_GameId",
column: x => x.GameId,
principalTable: "Games",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_GameRedistributable_Redistributables_RedistributableId",
column: x => x.RedistributableId,
principalTable: "Redistributables",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_GameRedistributable_RedistributableId",
table: "GameRedistributable",
column: "RedistributableId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GameRedistributable");
}
}
}

View File

@ -81,6 +81,21 @@ namespace LANCommander.Migrations
b.ToTable("GamePublisher");
});
modelBuilder.Entity("GameRedistributable", b =>
{
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<Guid>("RedistributableId")
.HasColumnType("TEXT");
b.HasKey("GameId", "RedistributableId");
b.HasIndex("RedistributableId");
b.ToTable("GameRedistributable");
});
modelBuilder.Entity("GameTag", b =>
{
b.Property<Guid>("GamesId")
@ -165,7 +180,7 @@ namespace LANCommander.Migrations
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<Guid>("GameId")
b.Property<Guid?>("GameId")
.HasColumnType("TEXT");
b.Property<Guid?>("LastVersionId")
@ -175,6 +190,9 @@ namespace LANCommander.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid?>("RedistributableId")
.HasColumnType("TEXT");
b.Property<long>("UncompressedSize")
.HasColumnType("INTEGER");
@ -196,6 +214,8 @@ namespace LANCommander.Migrations
b.HasIndex("LastVersionId");
b.HasIndex("RedistributableId");
b.HasIndex("UpdatedById");
b.ToTable("Archive");
@ -505,6 +525,43 @@ namespace LANCommander.Migrations
b.ToTable("MultiplayerModes");
});
modelBuilder.Entity("LANCommander.Data.Models.Redistributable", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid?>("CreatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("UpdatedById");
b.ToTable("Redistributables");
});
modelBuilder.Entity("LANCommander.Data.Models.Role", b =>
{
b.Property<Guid>("Id")
@ -591,13 +648,15 @@ namespace LANCommander.Migrations
.HasColumnType("TEXT");
b.Property<Guid?>("GameId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid?>("RedistributableId")
.HasColumnType("TEXT");
b.Property<bool>("RequiresAdmin")
.HasColumnType("INTEGER");
@ -616,6 +675,8 @@ namespace LANCommander.Migrations
b.HasIndex("GameId");
b.HasIndex("RedistributableId");
b.HasIndex("UpdatedById");
b.ToTable("Scripts");
@ -1027,6 +1088,21 @@ namespace LANCommander.Migrations
.IsRequired();
});
modelBuilder.Entity("GameRedistributable", b =>
{
b.HasOne("LANCommander.Data.Models.Game", null)
.WithMany()
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Data.Models.Redistributable", null)
.WithMany()
.HasForeignKey("RedistributableId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("GameTag", b =>
{
b.HasOne("LANCommander.Data.Models.Game", null)
@ -1074,13 +1150,17 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Archives")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("LANCommander.Data.Models.Archive", "LastVersion")
.WithMany()
.HasForeignKey("LastVersionId");
b.HasOne("LANCommander.Data.Models.Redistributable", "Redistributable")
.WithMany("Archives")
.HasForeignKey("RedistributableId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
@ -1091,6 +1171,8 @@ namespace LANCommander.Migrations
b.Navigation("LastVersion");
b.Navigation("Redistributable");
b.Navigation("UpdatedBy");
});
@ -1243,6 +1325,21 @@ namespace LANCommander.Migrations
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.Redistributable", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
b.Navigation("CreatedBy");
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.SavePath", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
@ -1273,8 +1370,12 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Scripts")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("LANCommander.Data.Models.Redistributable", "Redistributable")
.WithMany("Scripts")
.HasForeignKey("RedistributableId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
@ -1284,6 +1385,8 @@ namespace LANCommander.Migrations
b.Navigation("Game");
b.Navigation("Redistributable");
b.Navigation("UpdatedBy");
});
@ -1426,6 +1529,13 @@ namespace LANCommander.Migrations
b.Navigation("Servers");
});
modelBuilder.Entity("LANCommander.Data.Models.Redistributable", b =>
{
b.Navigation("Archives");
b.Navigation("Scripts");
});
modelBuilder.Entity("LANCommander.Data.Models.Server", b =>
{
b.Navigation("ServerConsoles");

View File

@ -2,6 +2,7 @@
@page "/Games/{id:guid}/{panel}"
@page "/Games/Add"
@using LANCommander.Components.FileManagerComponents;
@using LANCommander.Data.Enums;
@using LANCommander.Models;
@using LANCommander.Pages.Games.Components
@using System.IO.Compression;
@ -12,6 +13,7 @@
@inject TagService TagService
@inject ArchiveService ArchiveService
@inject ScriptService ScriptService
@inject RedistributableService RedistributableService
@inject IMessageService MessageService
@inject NavigationManager NavigationManager
@inject ModalService ModalService
@ -97,6 +99,9 @@
<FormItem Label="Tags">
<TagsInput Entities="Tags" @bind-Values="Game.Tags" OptionLabelSelector="c => c.Name" TItem="Data.Models.Tag" />
</FormItem>
<FormItem Label="Redistributables">
<TransferInput LeftTitle="Available" RightTitle="Selected" DataSource="Redistributables" TitleSelector="r => r.Name" @bind-Values="Game.Redistributables" />
</FormItem>
</Form>
</div>
@ -121,11 +126,11 @@
</div>
<div data-panel="Scripts">
<ScriptEditor @bind-Scripts="Game.Scripts" GameId="Game.Id" ArchiveId="@LatestArchiveId" />
<ScriptEditor @bind-Scripts="Game.Scripts" GameId="Game.Id" ArchiveId="@LatestArchiveId" AllowedTypes="new ScriptType[] { ScriptType.Install, ScriptType.Uninstall, ScriptType.NameChange, ScriptType.KeyChange }" />
</div>
<div data-panel="Archives">
<ArchiveEditor Game="Game" />
<ArchiveEditor @bind-Archives="Game.Archives" GameId="Game.Id" />
</div>
}
@ -160,6 +165,9 @@ else
IEnumerable<Company> Companies;
IEnumerable<Genre> Genres;
IEnumerable<Data.Models.Tag> Tags;
IEnumerable<Redistributable> Redistributables = new List<Redistributable>();
IEnumerable<TransferItem> RedistributableTargetItems = new List<TransferItem>();
IEnumerable<string> TargetRedistributables = new List<string>();
FilePickerDialog ArchiveFilePickerDialog;
@ -206,6 +214,13 @@ else
Companies = await CompanyService.Get();
Genres = await GenreService.Get();
Tags = await TagService.Get();
Redistributables = await RedistributableService.Get();
RedistributableTargetItems = Redistributables.Select(r => new TransferItem
{
Title = r.Name,
Description = r.Description,
Key = r.Id.ToString()
});
}
private async Task Save()

View File

@ -0,0 +1,117 @@
@page "/Redistributables/{id:guid}"
@page "/Redistributables/{id:guid}/{panel}"
@page "/Redistributables/Add"
@using LANCommander.Data.Enums;
@inject RedistributableService RedistributableService
@inject IMessageService MessageService
@inject NavigationManager NavigationManager
<Layout Class="panel-layout" Style="padding: 24px 0;">
<Sider Width="200">
<Menu Mode="@MenuMode.Inline" Style="height: 100%;">
<MenuItem RouterLink="@($"/Redistributables/{Redistributable.Id}/General")">General</MenuItem>
@if (Redistributable.Id != Guid.Empty)
{
<MenuItem RouterLink="@($"/Redistributables/{Redistributable.Id}/Scripts")">Scripts</MenuItem>
<MenuItem RouterLink="@($"/Redistributables/{Redistributable.Id}/Archives")">Archives</MenuItem>
}
</Menu>
</Sider>
<Content>
<PageHeader>
<PageHeaderTitle>@Panel</PageHeaderTitle>
</PageHeader>
<div class="panel-layout-content">
@if (Panel == "General" || String.IsNullOrWhiteSpace(Panel))
{
<Form Model="@Redistributable" Layout="@FormLayout.Vertical">
<FormItem Label="Name">
<Input @bind-Value="@context.Name" />
</FormItem>
<FormItem Label="Notes">
<TextArea @bind-Value="@context.Notes" MaxLength=2000 ShowCount />
</FormItem>
<FormItem Label="Description">
<TextArea @bind-Value="@context.Description" MaxLength=500 ShowCount />
</FormItem>
<FormItem>
<Button Type="@ButtonType.Primary" OnClick="Save" Icon="@IconType.Fill.Save">Save</Button>
</FormItem>
</Form>
}
@if (Panel == "Scripts")
{
<ScriptEditor @bind-Scripts="Redistributable.Scripts" RedistributableId="Redistributable.Id" ArchiveId="@LatestArchiveId" AllowedTypes="new ScriptType[] { ScriptType.Install, ScriptType.DetectInstall }" />
}
@if (Panel == "Archives")
{
<ArchiveEditor @bind-Archives="Redistributable.Archives" RedistributableId="Redistributable.Id" />
}
</div>
</Content>
</Layout>
@code {
[Parameter] public Guid Id { get; set; }
[Parameter] public string Panel { get; set; }
Redistributable Redistributable;
private Guid LatestArchiveId
{
get
{
if (Redistributable != null && Redistributable.Archives != null && Redistributable.Archives.Count > 0)
return Redistributable.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault().Id;
else
return Guid.Empty;
}
}
protected override async Task OnInitializedAsync()
{
if (Id == Guid.Empty)
Redistributable = new Redistributable();
else
Redistributable = await RedistributableService.Get(Id);
}
private async Task Save()
{
try
{
if (Redistributable.Id != Guid.Empty)
{
Redistributable = await RedistributableService.Update(Redistributable);
await MessageService.Success("Redistributable updated!");
}
else
{
Redistributable = await RedistributableService.Add(Redistributable);
NavigationManager.LocationChanged += NotifyRedistributableAdded;
NavigationManager.NavigateTo($"/Redistributables/{Redistributable.Id}");
}
}
catch (Exception ex)
{
await MessageService.Error("Could not save!");
}
}
private void NotifyRedistributableAdded(object? sender, LocationChangedEventArgs e)
{
NavigationManager.LocationChanged -= NotifyRedistributableAdded;
MessageService.Success("Redistributable added!");
}
}

View File

@ -0,0 +1,115 @@
@page "/Redistributables"
@using Microsoft.EntityFrameworkCore;
@attribute [Authorize]
@inject RedistributableService RedistributableService
@inject NavigationManager NavigationManager
@inject IMessageService MessageService
<PageHeader Title="Redistributables">
<PageHeaderExtra>
<Space Direction="DirectionVHType.Horizontal">
<SpaceItem>
<Search Placeholder="Search" @bind-Value="Search" BindOnInput DebounceMilliseconds="150" OnChange="() => LoadData()" />
</SpaceItem>
<SpaceItem>
<Button OnClick="() => Add()" Type="@ButtonType.Primary">Add Redistributable</Button>
</SpaceItem>
</Space>
</PageHeaderExtra>
</PageHeader>
<TableColumnPicker @ref="Picker" Key="Redistributables" @bind-Visible="ColumnPickerVisible" />
<Table TItem="Redistributable" DataSource="@Redistributables" Loading="@Loading" PageSize="25" Responsive>
<PropertyColumn Property="r => r.Name" Sortable Hidden="@(Picker.IsColumnHidden("Name"))" />
<PropertyColumn Property="s => s.CreatedOn" Format="MM/dd/yyyy hh:mm tt" Sortable Hidden="@(Picker.IsColumnHidden("Created On"))" />
<PropertyColumn Property="s => s.CreatedBy" Sortable Hidden="@(Picker.IsColumnHidden("Created By"))">
@context.CreatedBy?.UserName
</PropertyColumn>
<PropertyColumn Property="g => g.UpdatedOn" Format="MM/dd/yyyy hh:mm tt" Sortable Hidden="@(Picker.IsColumnHidden("Updated On"))" />
<PropertyColumn Property="g => g.UpdatedBy" Sortable Hidden="@(Picker.IsColumnHidden("Updated By"))">
@context.UpdatedBy?.UserName
</PropertyColumn>
<ActionColumn Title="" Style="text-align: right; white-space: nowrap">
<TitleTemplate>
<div style="text-align: right">
<Button Icon="@IconType.Outline.Edit" Type="@ButtonType.Text" OnClick="() => OpenColumnPicker()" />
</div>
</TitleTemplate>
<ChildContent>
<Space Direction="DirectionVHType.Horizontal">
<SpaceItem>
<Button OnClick="() => Edit(context)">Edit</Button>
</SpaceItem>
<SpaceItem>
<Popconfirm OnConfirm="() => Delete(context)" Title="Are you sure you want to delete this redistributable?">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
</Popconfirm>
</SpaceItem>
</Space>
</ChildContent>
</ActionColumn>
</Table>
@code {
IEnumerable<Redistributable> Redistributables { get; set; } = new List<Redistributable>();
bool Loading = true;
string Search = "";
TableColumnPicker Picker;
bool ColumnPickerVisible = false;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
LoadData();
Loading = false;
StateHasChanged();
}
}
private async Task LoadData()
{
var fuzzySearch = Search.ToLower().Trim();
Redistributables = await RedistributableService.Get(r => r.Name.ToLower().Contains(fuzzySearch)).OrderBy(r => r.Name).ToListAsync();
}
private void Add()
{
NavigationManager.NavigateTo("/Redistributables/Add");
}
private void Edit(Redistributable redistributable)
{
NavigationManager.NavigateTo($"/Redistributables/{redistributable.Id}/General");
}
private async Task Delete(Redistributable redistributable)
{
Redistributables = new List<Redistributable>();
Loading = true;
await RedistributableService.Delete(redistributable);
Redistributables = await RedistributableService.Get(x => true).OrderBy(r => r.Name).ToListAsync();
Loading = false;
}
private async Task OpenColumnPicker()
{
ColumnPickerVisible = true;
}
private async Task CloseColumnPicker()
{
ColumnPickerVisible = false;
}
}

View File

@ -141,6 +141,7 @@ namespace LANCommander
builder.Services.AddScoped<ServerService>();
builder.Services.AddScoped<ServerConsoleService>();
builder.Services.AddScoped<GameSaveService>();
builder.Services.AddScoped<RedistributableService>();
builder.Services.AddSingleton<ServerProcessService>();
builder.Services.AddSingleton<IPXRelayService>();

View File

@ -0,0 +1,12 @@
using LANCommander.Data;
using LANCommander.Data.Models;
namespace LANCommander.Services
{
public class RedistributableService : BaseDatabaseService<Redistributable>
{
public RedistributableService(DatabaseContext dbContext, IHttpContextAccessor httpContextAccessor) : base(dbContext, httpContextAccessor)
{
}
}
}

View File

@ -9,6 +9,7 @@
@if (User != null && User.IsInRole("Administrator"))
{
<MenuItem RouterLink="/Games">Games</MenuItem>
<MenuItem RouterLink="/Redistributables">Redistributables</MenuItem>
<MenuItem RouterLink="/Servers">Servers</MenuItem>
<MenuItem RouterLink="/Files">Files</MenuItem>
<MenuItem RouterLink="/Settings">Settings</MenuItem>