Change server HTTP options to allow multiple specified paths.

This is useful if you only want to share specific paths via HTTP, or have the paths stored in multiple places and want to unite them under one URL structure.
save-path-regex
Pat Hartl 2023-11-04 19:43:38 -05:00
parent d6eff92835
commit 4fb11c1dd7
9 changed files with 2010 additions and 18 deletions

View File

@ -20,14 +20,44 @@ namespace LANCommander.Controllers
if (server == null)
return NotFound();
path = path.Replace('/', Path.DirectorySeparatorChar);
var filename = Path.Combine(server.HTTPRootPath, path);
if (!System.IO.File.Exists(filename))
if (server.HttpPaths == null || server.HttpPaths.Count == 0)
return NotFound();
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", Path.GetFileName(filename));
// Sanitize
if (path == null)
path = "/";
path = path.Trim('/');
path = path + "/";
var httpPath = server.HttpPaths.FirstOrDefault(hp => path.StartsWith(hp.Path.TrimStart('/')));
// Check to see if there's a root path defined if nothing else matches
if (httpPath == null)
httpPath = server.HttpPaths.FirstOrDefault(hp => hp.Path == "/");
if (httpPath == null)
return Forbid();
var relativePath = path.Substring(httpPath.Path.TrimStart('/').Length).Replace('/', Path.DirectorySeparatorChar).TrimStart('\\');
var localPath = Path.Combine(httpPath.LocalPath, relativePath).TrimEnd('\\');
var attrs = System.IO.File.GetAttributes(localPath);
if ((attrs & FileAttributes.Directory) == FileAttributes.Directory)
{
if (!System.IO.Directory.Exists(localPath))
return NotFound();
return Json(Directory.GetFileSystemEntries(localPath).Select(fse => fse.Substring(localPath.Length)));
}
else
{
if (!System.IO.File.Exists(localPath))
return NotFound();
return File(new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", Path.GetFileName(localPath));
}
}
}
}

View File

@ -119,6 +119,12 @@ namespace LANCommander.Data
.IsRequired(true)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Server>()
.HasMany<ServerHttpPath>()
.WithOne(s => s.Server)
.IsRequired(true)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Redistributable>()
.HasMany(r => r.Archives)
.WithOne(a => a.Redistributable)

View File

@ -27,5 +27,6 @@ namespace LANCommander.Data.Models
public virtual Game? Game { get; set; }
public virtual ICollection<ServerConsole>? ServerConsoles { get; set; }
public virtual ICollection<ServerHttpPath>? HttpPaths { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace LANCommander.Data.Models
{
public class ServerHttpPath : BaseModel
{
public string LocalPath { get; set; }
public string Path { get; set; }
public Guid ServerId { get; set; }
[JsonIgnore]
[ForeignKey(nameof(ServerId))]
[InverseProperty("HttpPaths")]
public virtual Server Server { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddServerHttpPaths : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ServerHttpPath",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
LocalPath = table.Column<string>(type: "TEXT", nullable: false),
Path = table.Column<string>(type: "TEXT", nullable: false),
ServerId = table.Column<Guid>(type: "TEXT", nullable: false),
ServerId1 = table.Column<Guid>(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_ServerHttpPath", x => x.Id);
table.ForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_CreatedById",
column: x => x.CreatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_UpdatedById",
column: x => x.UpdatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_ServerHttpPath_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ServerHttpPath_Servers_ServerId1",
column: x => x.ServerId1,
principalTable: "Servers",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_ServerHttpPath_CreatedById",
table: "ServerHttpPath",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_ServerHttpPath_ServerId",
table: "ServerHttpPath",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_ServerHttpPath_ServerId1",
table: "ServerHttpPath",
column: "ServerId1");
migrationBuilder.CreateIndex(
name: "IX_ServerHttpPath_UpdatedById",
table: "ServerHttpPath",
column: "UpdatedById");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ServerHttpPath");
}
}
}

View File

@ -859,6 +859,51 @@ namespace LANCommander.Migrations
b.ToTable("ServerConsoles");
});
modelBuilder.Entity("LANCommander.Data.Models.ServerHttpPath", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid?>("CreatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("LocalPath")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("ServerId")
.HasColumnType("TEXT");
b.Property<Guid?>("ServerId1")
.HasColumnType("TEXT");
b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("ServerId");
b.HasIndex("ServerId1");
b.HasIndex("UpdatedById");
b.ToTable("ServerHttpPath");
});
modelBuilder.Entity("LANCommander.Data.Models.Tag", b =>
{
b.Property<Guid>("Id")
@ -1507,6 +1552,33 @@ namespace LANCommander.Migrations
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.ServerHttpPath", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
b.HasOne("LANCommander.Data.Models.Server", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Data.Models.Server", null)
.WithMany("HttpPaths")
.HasForeignKey("ServerId1");
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
b.Navigation("CreatedBy");
b.Navigation("Server");
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.Tag", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
@ -1608,6 +1680,8 @@ namespace LANCommander.Migrations
modelBuilder.Entity("LANCommander.Data.Models.Server", b =>
{
b.Navigation("HttpPaths");
b.Navigation("ServerConsoles");
});

View File

@ -0,0 +1,83 @@
@using LANCommander.Data.Enums
@using LANCommander.Data.Models
@using LANCommander.Extensions;
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
@if (Values == null || Values.Count == 0)
{
<Empty />
}
@foreach (var httpPath in Values)
{
<SpaceItem>
<Table TItem="ServerHttpPath" DataSource="@Values" HidePagination="true" Responsive>
<PropertyColumn Property="p => p.LocalPath" Title="Local Path">
<FilePicker @bind-Value="context.LocalPath" AllowDirectories Title="Select Local Path" Root="@WorkingDirectory" />
</PropertyColumn>
<PropertyColumn Property="p => p.Path">
<Input Type="text" @bind-Value="context.Path" />
</PropertyColumn>
<ActionColumn>
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => Remove(context)" Type="@ButtonType.Text" Danger Icon="@IconType.Outline.Close" />
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
}
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="Add" Type="@ButtonType.Primary">Add Path</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@code {
[Parameter] public ICollection<ServerHttpPath> Values { get; set; }
[Parameter] public EventCallback<ICollection<ServerHttpPath>> ValuesChanged { get; set; }
[Parameter] public Guid ServerId { get; set; }
[Parameter] public string WorkingDirectory { get; set; }
protected override async Task OnInitializedAsync()
{
if (Values == null)
Values = new List<ServerHttpPath>();
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(Values);
StateHasChanged();
}
private async Task Add()
{
if (Values == null)
Values = new List<ServerHttpPath>();
Values.Add(new ServerHttpPath
{
ServerId = ServerId
});
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(Values);
StateHasChanged();
}
private async Task Remove(ServerHttpPath httpPath)
{
Values.Remove(httpPath);
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(Values);
StateHasChanged();
}
}

View File

@ -118,19 +118,9 @@
@if (Panel == "HTTP")
{
<Form Model="@Server" Layout="@FormLayout.Vertical">
<FormItem Label="Enable HTTP">
<Switch @bind-Checked="context.EnableHTTP" />
</FormItem>
<ServerHttpPathEditor @bind-Values="Server.HttpPaths" ServerId="Id" WorkingDirectory="@Server.WorkingDirectory" />
<FormItem Label="Root Path">
<FilePicker Root="@RootPath" Title="Choose Root Path" OkText="Select Directory" EntrySelectable="x => x is FileManagerDirectory" @bind-Value="@context.HTTPRootPath" />
</FormItem>
<FormItem>
<Button Type="@ButtonType.Primary" OnClick="Save" Icon="@IconType.Fill.Save">Save</Button>
</FormItem>
</Form>
}
@if (Panel == "Monitor")
@ -180,6 +170,9 @@
if (Server.GameId.HasValue)
GameId = Server.GameId.Value;
if (Server.HttpPaths == null)
Server.HttpPaths = new List<ServerHttpPath>();
Games = await GameService.Get();
}
@ -210,6 +203,14 @@
}
}
private async Task AddPath()
{
Server.HttpPaths.Add(new ServerHttpPath()
{
ServerId = Server.Id
});
}
private void OnPathSelected(string path)
{
Server.WorkingDirectory = Path.GetDirectoryName(path);