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; } }