Backend support for game saves.
This commit is contained in:
parent
90b9d3bb75
commit
4f07c62247
16 changed files with 231 additions and 18 deletions
96
LANCommander/Controllers/Api/GameSavesController.cs
Normal file
96
LANCommander/Controllers/Api/GameSavesController.cs
Normal file
|
@ -0,0 +1,96 @@
|
|||
using LANCommander.Data;
|
||||
using LANCommander.Data.Models;
|
||||
using LANCommander.Extensions;
|
||||
using LANCommander.Models;
|
||||
using LANCommander.SDK;
|
||||
using LANCommander.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LANCommander.Controllers.Api
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = "Bearer")]
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class GameSavesController : ControllerBase
|
||||
{
|
||||
private readonly GameSaveService GameSaveService;
|
||||
private readonly GameService GameService;
|
||||
private readonly UserManager<User> UserManager;
|
||||
|
||||
public GameSavesController(GameSaveService gameSaveService, GameService gameService, UserManager<User> userManager)
|
||||
{
|
||||
GameSaveService = gameSaveService;
|
||||
GameService = gameService;
|
||||
UserManager = userManager;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<GameSave> Get(Guid id)
|
||||
{
|
||||
var gameSave = await GameSaveService.Get(id);
|
||||
|
||||
if (gameSave == null || gameSave.User == null)
|
||||
throw new FileNotFoundException();
|
||||
|
||||
if (gameSave.User.UserName != HttpContext.User.Identity.Name)
|
||||
throw new UnauthorizedAccessException();
|
||||
|
||||
return await GameSaveService.Get(id);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/Download")]
|
||||
public async Task<IActionResult> Download(Guid id)
|
||||
{
|
||||
var game = await GameService.Get(id);
|
||||
|
||||
if (game == null)
|
||||
return NotFound();
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
var path = GameSaveService.GetSavePath(game.Id, user.Id);
|
||||
|
||||
if (!System.IO.File.Exists(path))
|
||||
return NotFound();
|
||||
|
||||
return File(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{game.Id}.zip");
|
||||
}
|
||||
|
||||
[HttpPost("{id}/Upload")]
|
||||
public async Task<IActionResult> Upload(Guid id, [FromForm] SaveUpload save)
|
||||
{
|
||||
// Arbitrary file size limit of 25MB
|
||||
if (save.File.Length > (ByteSizeLib.ByteSize.BytesInMebiByte * 25))
|
||||
return BadRequest("Save file archive is too large");
|
||||
|
||||
var game = await GameService.Get(id);
|
||||
|
||||
if (game == null)
|
||||
return NotFound();
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
var path = GameSaveService.GetSavePath(game.Id, user.Id);
|
||||
|
||||
var fileInfo = new FileInfo(path);
|
||||
|
||||
if (!Directory.Exists(fileInfo.Directory.FullName))
|
||||
Directory.CreateDirectory(fileInfo.Directory.FullName);
|
||||
|
||||
using (var stream = System.IO.File.Create(path))
|
||||
{
|
||||
await save.File.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ namespace LANCommander.Controllers
|
|||
model.TotalStorageSize = ByteSize.FromBytes(drives.Where(d => d.IsReady && d.Name == root).Sum(d => d.TotalSize));
|
||||
model.TotalAvailableFreeSpace = ByteSize.FromBytes(drives.Where(d => d.IsReady && d.Name == root).Sum(d => d.AvailableFreeSpace));
|
||||
model.TotalUploadDirectorySize = ByteSize.FromBytes(new DirectoryInfo("Upload").EnumerateFiles().Sum(f => f.Length));
|
||||
model.TotalSaveDirectorySize = ByteSize.FromBytes(new DirectoryInfo("Save").EnumerateFiles().Sum(f => f.Length));
|
||||
|
||||
model.GameCount = GameService.Get().Count;
|
||||
|
||||
|
|
|
@ -45,11 +45,18 @@ namespace LANCommander.Controllers
|
|||
|
||||
foreach (var user in UserManager.Users)
|
||||
{
|
||||
var savePath = Path.Combine("Save", user.Id.ToString());
|
||||
long saveSize = 0;
|
||||
|
||||
if (Directory.Exists(savePath))
|
||||
saveSize = new DirectoryInfo(savePath).EnumerateFiles().Sum(f => f.Length);
|
||||
|
||||
users.Add(new UserViewModel()
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
Roles = await UserManager.GetRolesAsync(user)
|
||||
Roles = await UserManager.GetRolesAsync(user),
|
||||
SavesSize = saveSize
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,18 @@ namespace LANCommander.Data
|
|||
g => g.HasOne<Company>().WithMany().HasForeignKey("PublisherId"),
|
||||
g => g.HasOne<Game>().WithMany().HasForeignKey("GameId")
|
||||
);
|
||||
|
||||
builder.Entity<User>()
|
||||
.HasMany(u => u.GameSaves)
|
||||
.WithOne(gs => gs.User)
|
||||
.IsRequired(true)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<Game>()
|
||||
.HasMany(g => g.GameSaves)
|
||||
.WithOne(gs => gs.Game)
|
||||
.IsRequired(true)
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
}
|
||||
|
||||
public DbSet<Game>? Games { get; set; }
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
Install,
|
||||
Uninstall,
|
||||
NameChange,
|
||||
KeyChange
|
||||
KeyChange,
|
||||
SaveUpload,
|
||||
SaveDownload
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ namespace LANCommander.Data.Models
|
|||
public virtual ICollection<Company>? Developers { get; set; }
|
||||
public virtual ICollection<Archive>? Archives { get; set; }
|
||||
public virtual ICollection<Script>? Scripts { get; set; }
|
||||
public virtual ICollection<GameSave>? GameSaves { get; set; }
|
||||
|
||||
public string? ValidKeyRegex { get; set; }
|
||||
public virtual ICollection<Key>? Keys { get; set; }
|
||||
|
|
19
LANCommander/Data/Models/GameSave.cs
Normal file
19
LANCommander/Data/Models/GameSave.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LANCommander.Data.Models
|
||||
{
|
||||
public class GameSave : BaseModel
|
||||
{
|
||||
public Guid GameId { get; set; }
|
||||
[JsonIgnore]
|
||||
[ForeignKey(nameof(GameId))]
|
||||
[InverseProperty("GameSaves")]
|
||||
public virtual Game? Game { get; set; }
|
||||
|
||||
public Guid UserId { get; set; }
|
||||
[ForeignKey(nameof(UserId))]
|
||||
[InverseProperty("GameSaves")]
|
||||
public virtual User? User { get; set; }
|
||||
}
|
||||
}
|
|
@ -41,5 +41,8 @@ namespace LANCommander.Data.Models
|
|||
public string? RefreshToken { get; set; }
|
||||
[JsonIgnore]
|
||||
public DateTime RefreshTokenExpiration { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public virtual ICollection<GameSave>? GameSaves { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("GamesId");
|
||||
|
||||
b.ToTable("CategoryGame");
|
||||
b.ToTable("CategoryGame", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GameDeveloper", b =>
|
||||
|
@ -44,7 +44,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("GameId");
|
||||
|
||||
b.ToTable("GameDeveloper");
|
||||
b.ToTable("GameDeveloper", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GameGenre", b =>
|
||||
|
@ -59,7 +59,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("GenresId");
|
||||
|
||||
b.ToTable("GameGenre");
|
||||
b.ToTable("GameGenre", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GamePublisher", b =>
|
||||
|
@ -74,7 +74,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("PublisherId");
|
||||
|
||||
b.ToTable("GamePublisher");
|
||||
b.ToTable("GamePublisher", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GameTag", b =>
|
||||
|
@ -89,7 +89,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("GameTag");
|
||||
b.ToTable("GameTag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Action", b =>
|
||||
|
@ -140,7 +140,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Actions");
|
||||
b.ToTable("Actions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Archive", b =>
|
||||
|
@ -194,7 +194,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Archive");
|
||||
b.ToTable("Archive", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Category", b =>
|
||||
|
@ -230,7 +230,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Categories");
|
||||
b.ToTable("Categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Company", b =>
|
||||
|
@ -261,7 +261,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Companies");
|
||||
b.ToTable("Companies", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Game", b =>
|
||||
|
@ -316,7 +316,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Games");
|
||||
b.ToTable("Games", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Genre", b =>
|
||||
|
@ -347,7 +347,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Genres");
|
||||
b.ToTable("Genres", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Key", b =>
|
||||
|
@ -407,7 +407,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Keys");
|
||||
b.ToTable("Keys", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.MultiplayerMode", b =>
|
||||
|
@ -458,7 +458,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("MultiplayerModes");
|
||||
b.ToTable("MultiplayerModes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Role", b =>
|
||||
|
@ -535,7 +535,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Scripts");
|
||||
b.ToTable("Scripts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.Tag", b =>
|
||||
|
@ -566,7 +566,7 @@ namespace LANCommander.Migrations
|
|||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("Tags");
|
||||
b.ToTable("Tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LANCommander.Data.Models.User", b =>
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace LANCommander.Models
|
|||
public ByteSize TotalAvailableFreeSpace { get; set; }
|
||||
public ByteSize TotalStorageSize { get; set; }
|
||||
public ByteSize TotalUploadDirectorySize { get; set; }
|
||||
public ByteSize TotalSaveDirectorySize { get; set; }
|
||||
public ByteSize TotalOtherSize {
|
||||
get
|
||||
{
|
||||
|
|
8
LANCommander/Models/SaveUpload.cs
Normal file
8
LANCommander/Models/SaveUpload.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace LANCommander.Models
|
||||
{
|
||||
public class SaveUpload
|
||||
{
|
||||
public Guid GameId { get; set; }
|
||||
public IFormFile File { get; set; }
|
||||
}
|
||||
}
|
|
@ -5,5 +5,6 @@
|
|||
public Guid Id { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public IEnumerable<string> Roles { get; set; }
|
||||
public long SavesSize { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,4 +110,7 @@ if (!Directory.Exists("Upload"))
|
|||
if (!Directory.Exists("Icon"))
|
||||
Directory.CreateDirectory("Icon");
|
||||
|
||||
if (!Directory.Exists("Save"))
|
||||
Directory.CreateDirectory("Save");
|
||||
|
||||
app.Run();
|
49
LANCommander/Services/GameSaveService.cs
Normal file
49
LANCommander/Services/GameSaveService.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using LANCommander.Data;
|
||||
using LANCommander.Data.Models;
|
||||
using LANCommander.Helpers;
|
||||
|
||||
namespace LANCommander.Services
|
||||
{
|
||||
public class GameSaveService : BaseDatabaseService<GameSave>
|
||||
{
|
||||
private readonly SettingService SettingService;
|
||||
|
||||
public GameSaveService(DatabaseContext dbContext, IHttpContextAccessor httpContextAccessor, SettingService settingService) : base(dbContext, httpContextAccessor)
|
||||
{
|
||||
SettingService = settingService;
|
||||
}
|
||||
|
||||
public override Task Delete(GameSave entity)
|
||||
{
|
||||
FileHelpers.DeleteIfExists(GetSavePath(entity.Id));
|
||||
|
||||
return base.Delete(entity);
|
||||
}
|
||||
|
||||
public string GetSavePath(Guid gameId, Guid userId)
|
||||
{
|
||||
var save = Get(gs => gs.GameId == gameId && gs.UserId == userId).FirstOrDefault();
|
||||
|
||||
if (save == null)
|
||||
return null;
|
||||
|
||||
return GetSavePath(save.Id);
|
||||
}
|
||||
|
||||
public string GetSavePath(Guid id)
|
||||
{
|
||||
// Use get with predicate to avoid async
|
||||
var save = Get(gs => gs.Id == id).FirstOrDefault();
|
||||
|
||||
if (save == null)
|
||||
return null;;
|
||||
|
||||
return GetSavePath(save);
|
||||
}
|
||||
|
||||
public string GetSavePath(GameSave save)
|
||||
{
|
||||
return Path.Combine("Save", save.UserId.ToString(), $"{save.Id}.zip");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,15 +32,21 @@
|
|||
<p class="mb-3">Storage Used: <strong>@(Model.TotalOtherSize + Model.TotalUploadDirectorySize) of @Model.TotalStorageSize</strong></p>
|
||||
<div class="progress progress-separated mb-3">
|
||||
<div class="progress-bar bg-primary" role="progressbar" style="width: @Math.Round((Model.TotalUploadDirectorySize.Bytes / Model.TotalStorageSize.Bytes) * 100)%;"></div>
|
||||
<div class="progress-bar bg-dark" role="progressbar" style="width: @Math.Round((Model.TotalSaveDirectorySize.Bytes / Model.TotalStorageSize.Bytes) * 100)%;"></div>
|
||||
<div class="progress-bar bg-info" role="progressbar" style="width: @Math.Round((Model.TotalOtherSize.Bytes / Model.TotalStorageSize.Bytes) * 100)%;"></div>
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: @Math.Round((Model.TotalAvailableFreeSpace.Bytes / Model.TotalStorageSize.Bytes) * 100)%;"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-auto d-flex align-items-center pe-2">
|
||||
<span class="legend me-2 bg-primary"></span>
|
||||
<span>Uploads</span>
|
||||
<span>Games</span>
|
||||
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-muted">@Model.TotalUploadDirectorySize</span>
|
||||
</div>
|
||||
<div class="col-auto d-flex align-items-center pe-2">
|
||||
<span class="legend me-2 bg-dark"></span>
|
||||
<span>Saves</span>
|
||||
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-muted">@Model.TotalSaveDirectorySize</span>
|
||||
</div>
|
||||
<div class="col-auto d-flex align-items-center pe-2">
|
||||
<span class="legend me-2 bg-info"></span>
|
||||
<span>Other</span>
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Saves</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -51,6 +52,9 @@
|
|||
<td>
|
||||
@String.Join(", ", item.Roles)
|
||||
</td>
|
||||
<td>
|
||||
@ByteSizeLib.ByteSize.FromBytes(item.SavesSize)
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-list flex-nowrap justify-content-end">
|
||||
@if (!item.Roles.Any(r => r == "Administrator"))
|
||||
|
|
Loading…
Add table
Reference in a new issue