105 lines
5 KiB
C#
105 lines
5 KiB
C#
using System.IO.Compression;
|
|
using System.Runtime.InteropServices;
|
|
using VideoDownloader.Core.Config;
|
|
|
|
namespace VideoDownloader.Core.Social;
|
|
|
|
/// <summary>
|
|
/// Deno(JS 런타임) 해결(설정 오버라이드 → 번들 → PATH → 자동 다운로드) +
|
|
/// 안전한 다운로드(GitHub Releases zip → deno.exe 추출). yt-dlp 의 EJS(n 챌린지 해석) 실행에 필요.
|
|
/// YtDlpRunner.RunAsync 가 자식 프로세스 PATH 에 ToolsDir 를 추가하므로, 번들 deno.exe 는 yt-dlp 가 자동 발견.
|
|
/// </summary>
|
|
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) };
|
|
|
|
/// <summary>Deno 는 Windows ARM 네이티브 빌드가 없어 x64 에뮬레이션. macOS/Linux 는 아키텍처별 에셋.</summary>
|
|
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}";
|
|
|
|
/// <summary>Deno 경로 해결. 못 찾으면 null.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Deno 가 없고 AutoInstallDeno 면 번들 디렉터리로 자동 다운로드. 경로 반환(실패 시 null).</summary>
|
|
public static async Task<string?> EnsureInstalledAsync(AppConfig config, Action<string>? log, CancellationToken ct)
|
|
{
|
|
var found = Resolve(config);
|
|
if (found != null) return found;
|
|
if (!config.AutoInstallDeno) return null;
|
|
return await DownloadAsync(config, log, ct);
|
|
}
|
|
|
|
/// <summary>Deno 를 강제로(재)다운로드해 번들 디렉터리에 설치.</summary>
|
|
public static async Task<string?> DownloadAsync(AppConfig config, Action<string>? 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;
|
|
}
|
|
}
|