Files
TinfoilVibeServer/TinfoilVibeServer/Controllers/IndexController.cs
T
2025-11-04 07:40:27 +10:30

171 lines
7.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
/// Catchall 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 lightweight 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 cant 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;
}
}