using System.IO.Compression;
using System.Runtime.InteropServices;
using VideoDownloader.Core.Config;
namespace VideoDownloader.Core.Social;
///
/// Deno(JS 런타임) 해결(설정 오버라이드 → 번들 → PATH → 자동 다운로드) +
/// 안전한 다운로드(GitHub Releases zip → deno.exe 추출). yt-dlp 의 EJS(n 챌린지 해석) 실행에 필요.
/// YtDlpRunner.RunAsync 가 자식 프로세스 PATH 에 ToolsDir 를 추가하므로, 번들 deno.exe 는 yt-dlp 가 자동 발견.
///
public static class DenoRunner
{
// yt-dlp 와 동일한 tools 디렉터리를 공유 → PATH 주입 한 번으로 yt-dlp/ffmpeg/deno 모두 발견.
public static string ToolsDir => YtDlpRunner.ToolsDir;
private static string BundledExe => OperatingSystem.IsWindows() ? "deno.exe" : "deno";
public static string BundledPath => Path.Combine(ToolsDir, BundledExe);
private static readonly HttpClient Downloader = new(new HttpClientHandler { AllowAutoRedirect = true })
{ Timeout = TimeSpan.FromMinutes(5) };
/// Deno 는 Windows ARM 네이티브 빌드가 없어 x64 에뮬레이션. macOS/Linux 는 아키텍처별 에셋.
private static string AssetName
{
get
{
bool arm = RuntimeInformation.ProcessArchitecture is Architecture.Arm64;
if (OperatingSystem.IsWindows()) return "deno-x86_64-pc-windows-msvc.zip";
if (OperatingSystem.IsMacOS()) return arm ? "deno-aarch64-apple-darwin.zip" : "deno-x86_64-apple-darwin.zip";
return arm ? "deno-aarch64-unknown-linux-gnu.zip" : "deno-x86_64-unknown-linux-gnu.zip";
}
}
private static string ReleaseUrl => $"https://github.com/denoland/deno/releases/latest/download/{AssetName}";
/// Deno 경로 해결. 못 찾으면 null.
public static string? Resolve(AppConfig config)
{
if (!string.IsNullOrWhiteSpace(config.DenoPath) && File.Exists(config.DenoPath))
return config.DenoPath;
try { if (File.Exists(BundledPath)) return BundledPath; } catch { }
return FindOnPath(OperatingSystem.IsWindows() ? "deno.exe" : "deno");
}
/// Deno 가 없고 AutoInstallDeno 면 번들 디렉터리로 자동 다운로드. 경로 반환(실패 시 null).
public static async Task EnsureInstalledAsync(AppConfig config, Action? log, CancellationToken ct)
{
var found = Resolve(config);
if (found != null) return found;
if (!config.AutoInstallDeno) return null;
return await DownloadAsync(config, log, ct);
}
/// Deno 를 강제로(재)다운로드해 번들 디렉터리에 설치.
public static async Task DownloadAsync(AppConfig config, Action? log, CancellationToken ct)
{
var tmpZip = BundledPath + ".zip";
try
{
Directory.CreateDirectory(ToolsDir);
log?.Invoke("Deno 다운로드 중...");
using var resp = await Downloader.GetAsync(ReleaseUrl, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();
await using (var src = await resp.Content.ReadAsStreamAsync(ct))
await using (var dst = new FileStream(tmpZip, FileMode.Create, FileAccess.Write, FileShare.None, 65536, useAsync: true))
{
await src.CopyToAsync(dst, 65536, ct);
await dst.FlushAsync(ct);
}
// zip 에서 deno 실행파일 엔트리만 추출(압축 풀면 deno/deno.exe 단일 파일).
try { if (File.Exists(BundledPath)) File.Delete(BundledPath); } catch { }
using var archive = new ZipArchive(new FileStream(tmpZip, FileMode.Open, FileAccess.Read, FileShare.None, 65536, useAsync: true), ZipArchiveMode.Read);
var entry = archive.Entries.FirstOrDefault(e => e.Name.Equals(BundledExe, StringComparison.OrdinalIgnoreCase));
if (entry == null) { log?.Invoke("Deno 추출 실패: zip 에 실행파일이 없음"); return null; }
await using var es = entry.Open();
await using var fs = new FileStream(BundledPath, FileMode.Create, FileAccess.Write, FileShare.None, 65536, useAsync: true);
await es.CopyToAsync(fs, 65536, ct);
await fs.FlushAsync(ct);
log?.Invoke($"Deno 설치 완료: {BundledPath}");
return BundledPath;
}
catch (Exception e)
{
log?.Invoke($"Deno 다운로드 실패: {e.Message}");
return null;
}
finally
{
try { if (File.Exists(tmpZip)) File.Delete(tmpZip); } catch { }
}
}
private static string? FindOnPath(string name)
{
foreach (var d in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator))
{
var p = Path.Combine(d.Trim('"'), name);
if (File.Exists(p)) return p;
}
return null;
}
}