using System.Security.Cryptography; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using QMax.Api.Configuration; using QMax.Api.Contracts; namespace QMax.Api.Controllers; [ApiController] [Route("api/app-updates")] public sealed class AppUpdatesController(IOptions options) : ControllerBase { private readonly QMaxOptions _options = options.Value; [AllowAnonymous] [HttpGet("android/latest")] public async Task> Latest(CancellationToken cancellationToken) { var manifestPath = Path.Combine(_options.ReleasesPath, "android", "latest.json"); if (System.IO.File.Exists(manifestPath)) { return PhysicalFile(manifestPath, "application/json"); } var androidReleases = new DirectoryInfo(Path.Combine(_options.ReleasesPath, "android")); if (!androidReleases.Exists) { return NotFound(); } var apk = androidReleases .GetFiles("*.apk") .OrderByDescending(x => x.LastWriteTimeUtc) .FirstOrDefault(); if (apk is null) { return NotFound(); } var manifest = new AppUpdateManifestDto( "qmax", "QMAX", "0.1.0", 1, "stable", "android", "apk", $"/api/app-updates/android/download/{Uri.EscapeDataString(apk.Name)}", apk.Length, await Sha256Async(apk.FullName, cancellationToken), "QMAX local release", apk.LastWriteTimeUtc); return manifest; } [AllowAnonymous] [HttpGet("android/download/{fileName}")] public IActionResult Download(string fileName) { var safeName = Path.GetFileName(fileName); var path = Path.Combine(_options.ReleasesPath, "android", safeName); if (!System.IO.File.Exists(path) || !safeName.EndsWith(".apk", StringComparison.OrdinalIgnoreCase)) { return NotFound(); } return PhysicalFile(path, "application/vnd.android.package-archive", safeName, enableRangeProcessing: true); } private static async Task Sha256Async(string path, CancellationToken cancellationToken) { await using var stream = System.IO.File.OpenRead(path); var hash = await SHA256.HashDataAsync(stream, cancellationToken); return Convert.ToHexString(hash).ToLowerInvariant(); } }