175 lines
7.1 KiB
C#
175 lines
7.1 KiB
C#
using System.Net;
|
|
using Paca.Core.Config;
|
|
using Paca.Core.Hls;
|
|
using Paca.Core.Platform;
|
|
using Xunit;
|
|
|
|
namespace Paca.Tests.Core;
|
|
|
|
public class DownloadFolderCleanlinessAndPacaIsolationGateTests
|
|
{
|
|
[Fact]
|
|
public void Gate_DownloadMetaPathResolver_CreatesHiddenPacaFolder_AndSubdirectories()
|
|
{
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "paca_clean_test_" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
try
|
|
{
|
|
var outputBase = Path.Combine(tempRoot, "test_video");
|
|
var partsDir = DownloadMetaPathResolver.GetPartsDirectory(outputBase);
|
|
var tempPartFile = DownloadMetaPathResolver.GetTempPartFilePath(outputBase);
|
|
var pacaDir = DownloadMetaPathResolver.GetPacaDirectory(tempRoot);
|
|
|
|
Assert.True(Directory.Exists(pacaDir), ".paca 폴더가 생성되어야 한다.");
|
|
var di = new DirectoryInfo(pacaDir);
|
|
Assert.True((di.Attributes & FileAttributes.Hidden) != 0, ".paca 폴더는 Hidden 속성을 가져야 한다.");
|
|
|
|
// partsDir은 .paca/parts 하위에 존재해야 함
|
|
Assert.Contains(Path.Combine(".paca", "parts"), partsDir);
|
|
Assert.DoesNotContain(tempRoot + Path.DirectorySeparatorChar + "test_video.parts", partsDir);
|
|
|
|
// tempPartFile은 .paca/temp 하위에 존재해야 함
|
|
Assert.Contains(Path.Combine(".paca", "temp"), tempPartFile);
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(tempRoot))
|
|
{
|
|
try { Directory.Delete(tempRoot, recursive: true); } catch { }
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Gate_DownloadFolder_HlsDownloader_StoresPartsInsidePacaHiddenFolder()
|
|
{
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "paca_hls_clean_" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
|
|
// Fake HTTP Listener for sample m3u8
|
|
var listener = new HttpListener();
|
|
var port = 58921;
|
|
listener.Prefixes.Add($"http://127.0.0.1:{port}/");
|
|
listener.Start();
|
|
|
|
var serverTask = Task.Run(async () =>
|
|
{
|
|
while (listener.IsListening)
|
|
{
|
|
try
|
|
{
|
|
var ctx = await listener.GetContextAsync();
|
|
var path = ctx.Request.Url?.AbsolutePath ?? "";
|
|
if (path.EndsWith("playlist.m3u8"))
|
|
{
|
|
var m3u8 = "#EXTM3U\n#EXT-X-TARGETDURATION:2\n#EXTINF:2.0,\nseg0.ts\n#EXT-X-ENDLIST\n";
|
|
var bytes = System.Text.Encoding.UTF8.GetBytes(m3u8);
|
|
ctx.Response.ContentType = "application/vnd.apple.mpegurl";
|
|
ctx.Response.OutputStream.Write(bytes);
|
|
ctx.Response.Close();
|
|
}
|
|
else if (path.EndsWith("seg0.ts"))
|
|
{
|
|
var seg = new byte[1024];
|
|
new Random(42).NextBytes(seg);
|
|
ctx.Response.ContentType = "video/MP2T";
|
|
ctx.Response.OutputStream.Write(seg);
|
|
ctx.Response.Close();
|
|
}
|
|
}
|
|
catch { break; }
|
|
}
|
|
});
|
|
|
|
try
|
|
{
|
|
using var http = new HttpClient();
|
|
var config = new AppConfig { Concurrency = 1, OutputFolder = tempRoot };
|
|
var fakeMuxer = new FakeMediaMuxer();
|
|
var hls = new HlsDownloader(http, config, fakeMuxer);
|
|
|
|
var outputBase = Path.Combine(tempRoot, "my_video");
|
|
var res = await hls.DownloadAsync(new Uri($"http://127.0.0.1:{port}/playlist.m3u8"), outputBase);
|
|
|
|
// 1. 루트 폴더에 .parts 폴더나 .json 파일이 절대 없어야 한다!
|
|
var rootDirs = Directory.GetDirectories(tempRoot).Select(Path.GetFileName).ToList();
|
|
Assert.DoesNotContain("my_video.parts", rootDirs);
|
|
|
|
var rootFiles = Directory.GetFiles(tempRoot).Select(Path.GetFileName).ToList();
|
|
Assert.DoesNotContain(".chunkcache.json", rootFiles);
|
|
Assert.DoesNotContain("chunkcache.json", rootFiles);
|
|
|
|
// 2. 최종 mp4 파일은 루트에 존재해야 한다
|
|
Assert.True(File.Exists(outputBase + ".mp4"), "최종 mp4는 루트에 있어야 함");
|
|
}
|
|
finally
|
|
{
|
|
listener.Stop();
|
|
try { await serverTask; } catch { }
|
|
if (Directory.Exists(tempRoot))
|
|
{
|
|
try { Directory.Delete(tempRoot, recursive: true); } catch { }
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Gate_DownloadFolder_DirectDownloader_IsolatesPartFileInPacaHiddenDir()
|
|
{
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "paca_direct_clean_" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
|
|
try
|
|
{
|
|
var fakeHandler = new SimpleMockHandler(new byte[512]);
|
|
using var client = new HttpClient(fakeHandler);
|
|
var config = new AppConfig { OutputFolder = tempRoot };
|
|
var downloader = new DirectDownloader(client, config);
|
|
|
|
var outputBase = Path.Combine(tempRoot, "direct_file");
|
|
var final = await downloader.DownloadAsync(new Uri("https://test.paca/video.mp4"), outputBase);
|
|
|
|
Assert.True(File.Exists(final));
|
|
// 루트 폴더에 .part 파일이 없어야 함
|
|
Assert.False(File.Exists(outputBase + ".part"), "루트 폴더에 .part 파일이 존재해서는 안 됨");
|
|
|
|
var rootFiles = Directory.GetFiles(tempRoot).Select(Path.GetFileName).ToList();
|
|
Assert.DoesNotContain("direct_file.part", rootFiles);
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(tempRoot))
|
|
{
|
|
try { Directory.Delete(tempRoot, recursive: true); } catch { }
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class SimpleMockHandler : HttpMessageHandler
|
|
{
|
|
private readonly byte[] _data;
|
|
public SimpleMockHandler(byte[] data) => _data = data;
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
var resp = new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new ByteArrayContent(_data)
|
|
};
|
|
resp.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("video/mp4");
|
|
return Task.FromResult(resp);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeMediaMuxer : IMediaMuxer
|
|
{
|
|
public Task RemuxAsync(string ffmpeg, string input, string output, CancellationToken ct)
|
|
{
|
|
File.Copy(input, output, overwrite: true);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task DownloadDashAsync(string ffmpeg, Uri mpdUrl, string output, double durationSec, HttpClient http, IProgress<DownloadProgress>? progress, CancellationToken ct) => Task.CompletedTask;
|
|
|
|
public string? ResolveFfmpeg(AppConfig config) => "fake-ffmpeg";
|
|
}
|
|
}
|