171 lines
7.1 KiB
C#
171 lines
7.1 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.Extensions.Configuration;
|
||
using TinfoilVibeServer.Models;
|
||
using TinfoilVibeServer.Services;
|
||
|
||
namespace TinfoilVibeServer.Controllers;
|
||
|
||
[ApiController]
|
||
[Route("/")]
|
||
public sealed class IndexController : ControllerBase
|
||
{
|
||
private readonly ISnapshotService _snapshotService;
|
||
private readonly TitleDatabaseService _titleDb;
|
||
private readonly IConfiguration _configuration;
|
||
private readonly IndexBuilderService _indexBuilderService;
|
||
|
||
public IndexController(ISnapshotService snapshotService,
|
||
TitleDatabaseService titleDb,
|
||
IConfiguration configuration, IndexBuilderService indexBuilderService)
|
||
{
|
||
_snapshotService = snapshotService;
|
||
_titleDb = titleDb;
|
||
_configuration = configuration;
|
||
_indexBuilderService = indexBuilderService;
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// GET /
|
||
// ------------------------------------------------------------
|
||
/// <summary>
|
||
/// Return the “index snapshot” – e.g. a JSON file that lists
|
||
/// every available resource. The snapshot could also be a
|
||
/// rendered Razor view – simply change the return type.
|
||
/// </summary>
|
||
public IActionResult Index()
|
||
{
|
||
var index = _indexBuilderService.Build();
|
||
|
||
return Ok(index);
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// GET /{*path}
|
||
// ------------------------------------------------------------
|
||
/// <summary>
|
||
/// Catch‑all action. Any URL that is **not** “/” is routed
|
||
/// here. The value of *path* is the relative location you
|
||
/// want to stream back to the client.
|
||
/// </summary>
|
||
/// <param name="path">The relative file path requested.</param>
|
||
[HttpGet("{*path}")]
|
||
public IActionResult Download(string path)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
return BadRequest("Missing url query parameter.");
|
||
|
||
// ---- 1️⃣ Parse the brackets --------------------------------
|
||
// Expected format: [name][TitleId][v][patchOrApp].nsp
|
||
var match = System.Text.RegularExpressions.Regex.Match(path,
|
||
@"\[(?<name>.*?)\]\[(?<id>[0-9a-fA-F]{8}[0-9a-fA-F]{8})\]\[(?<v>[0-9a-fA-F]+)\]\[(?<app>Base|Update)\]\.nsp",
|
||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||
|
||
if (!match.Success)
|
||
return BadRequest("Url does not match the expected pattern.");
|
||
|
||
var titleId = match.Groups["id"].Value.ToUpperInvariant();
|
||
|
||
// ---- 2️⃣ Find the file that contains this TitleId ------------
|
||
var entry = _snapshotService.GetSnapshot()
|
||
.FirstOrDefault(e => e.Title?.TitleId == titleId);
|
||
|
||
if (entry == null)
|
||
return NotFound("No file with that TitleId found.");
|
||
|
||
// ---- 3️⃣ If the file is a normal NSP → send it ----------------
|
||
if (Path.GetExtension(entry.Path).Equals(".nsp", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// Check if it is inside an archive.
|
||
// If the path contains a slash that is not the root separator
|
||
// it might be an entry inside an archive; we simply stream it.
|
||
// - For normal files, we can use SendFileAsync.
|
||
// - For archives, we stream the entry using ArchiveHandler.
|
||
|
||
if (IsInsideArchive(entry.Path))
|
||
{
|
||
// Example: file is inside an archive – use ArchiveHandler
|
||
var archivePath = Path.GetDirectoryName(entry.Path);
|
||
var innerFileName = Path.GetFileName(entry.Path);
|
||
var stream = StreamFromArchive(archivePath, innerFileName);
|
||
|
||
if (stream == null)
|
||
return NotFound("Could not stream entry from archive.");
|
||
|
||
return File(stream, "application/octet-stream",
|
||
Path.GetFileName(innerFileName));
|
||
}
|
||
else
|
||
{
|
||
// Regular file – just serve it.
|
||
return PhysicalFile(entry.Path, "application/octet-stream",
|
||
Path.GetFileName(entry.Path));
|
||
}
|
||
}
|
||
|
||
return NotFound("Requested URL does not reference an NSP file.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Very light‑weight helper – decides whether the file path
|
||
/// represents a file inside an archive.
|
||
/// </summary>
|
||
private bool IsInsideArchive(string path) =>
|
||
// If the path contains a separator that is not a root separator
|
||
// (e.g. "Games/MyGame.nsp" is a regular file; "archive.7z/mygame.nsp"
|
||
// would be inside an archive). For simplicity we only check
|
||
// for common archive extensions.
|
||
path.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ||
|
||
path.EndsWith(".7z", StringComparison.OrdinalIgnoreCase) ||
|
||
path.EndsWith(".rar", StringComparison.OrdinalIgnoreCase);
|
||
|
||
/// <summary>
|
||
/// If the NSP is inside an archive, this method opens the archive
|
||
/// and returns the entry stream. It is deliberately minimal –
|
||
/// if the archive can’t be opened we return null.
|
||
/// </summary>
|
||
private Stream? StreamFromArchive(string archivePath, string innerFileName)
|
||
{
|
||
// Use SharpCompress to open the archive and find the entry.
|
||
// Only the 3 archive types we support are handled.
|
||
try
|
||
{
|
||
// Check which archive type
|
||
if (archivePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
using var zip = SharpCompress.Archives.Zip.ZipArchive.Open(archivePath);
|
||
var entry = zip.Entries
|
||
.FirstOrDefault(e => e.Key.Equals(innerFileName,
|
||
StringComparison.OrdinalIgnoreCase));
|
||
if (entry != null)
|
||
return entry.OpenEntryStream();
|
||
}
|
||
else if (archivePath.EndsWith(".7z", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
using var sevenZip = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(archivePath);
|
||
var entry = sevenZip.Entries
|
||
.FirstOrDefault(e => e.Key.Equals(innerFileName,
|
||
StringComparison.OrdinalIgnoreCase));
|
||
if (entry != null)
|
||
return entry.OpenEntryStream();
|
||
}
|
||
else if (archivePath.EndsWith(".rar", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
using var rar = SharpCompress.Archives.Rar.RarArchive.Open(archivePath);
|
||
var entry = rar.Entries
|
||
.FirstOrDefault(e => e.Key.Equals(innerFileName,
|
||
StringComparison.OrdinalIgnoreCase));
|
||
if (entry != null)
|
||
return entry.OpenEntryStream();
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore – we will just return null and the controller
|
||
// will respond with 404.
|
||
}
|
||
return null;
|
||
}
|
||
} |