사용자 결정대로 뽑은 폴더 옆에 같은 이름 .zip 을 만든다. 안에 폴더와 안내 파일을 나란히 넣어 풀면 폴더로 뽑은 것과 같은 모양이고, 파일은 바이트 그대로 · 경로는 / · 한글 이름은 UTF-8 표시. .part 에 쓰고 옮겨 반쯤 된 ZIP 을 남기지 않고, 못 만들어도 추출은 성공으로 둔다. UniquePath 는 같은 이름 .zip 이 남아 있어도 그 이름을 쓴 것으로 본다. 하네스 ReportTest +13 (70건, 22벌 1,260건) · 빌드 경고 0 · 오류 0. 아홉 경우를 화면에서 다시 뽑아 ZIP 8/8 을 Windows tar 로 풀어 폴더와 경로 · 해시가 같음, 앱이 만든 ZIP 의 한글 이름 UTF-8 표시 확인. 웹 문서 버전 14. 기획안은 사용자 잠금 대기. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 lines
3.5 KiB
C#
75 lines
3.5 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
|
|
namespace MergeHub.Services
|
|
{
|
|
/// <summary>
|
|
/// 뽑은 변경분을 ZIP 하나로도 묶는다(11차 A-4, 2026-09-22 사용자 결정: <b>폴더에 더해 ZIP 도</b>).
|
|
///
|
|
/// 장비에는 폴더를 넣고, 메일 · 메신저로 보낼 때는 ZIP 을 쓴다. ZIP 안에는 뽑은 폴더와 안내 파일을
|
|
/// <b>나란히</b> 넣는다 - 풀면 폴더로 뽑은 것과 같은 모양이 되고, 안내 파일이 소스 틈에 섞이지 않는다(A-2 결정).
|
|
/// 파일은 바이트 그대로 넣고, 안의 경로는 <c>/</c> 로 나눈다. 한글 이름은 UTF-8 로 적고 그렇다고 표시한다
|
|
/// (.NET 의 <see cref="ZipArchive"/> 가 ASCII 가 아닌 이름에 범용 플래그 bit 11 을 켠다).
|
|
/// </summary>
|
|
public static class ExportZip
|
|
{
|
|
/// <summary>뽑은 폴더 옆, 폴더 이름에 <c>.zip</c>.</summary>
|
|
public static string PathFor(string folder) =>
|
|
folder.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + ".zip";
|
|
|
|
/// <summary>
|
|
/// ZIP 을 만든다. 넣은 파일 수(안내 포함)를 돌려준다.
|
|
/// 이미 같은 이름이 있으면 덮지 않고 예외 - 자리는 <see cref="ExportPlan.UniquePath"/> 가 비워 둔다.
|
|
/// </summary>
|
|
/// <param name="folder">뽑은 폴더. ZIP 안에서 이 폴더 이름이 맨 위 폴더가 된다.</param>
|
|
/// <param name="notePath">안내 파일. 없거나 null 이면 넣지 않는다.</param>
|
|
public static int Create(string folder, string? notePath, string zipPath)
|
|
{
|
|
var root = folder.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
var top = Path.GetFileName(root);
|
|
var count = 0;
|
|
|
|
// 쓰다 막히면 반쯤 된 ZIP 을 남기지 않는다 - 옆 이름에 쓰고 다 되면 옮긴다.
|
|
var temp = zipPath + ".part";
|
|
try
|
|
{
|
|
using (var stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write))
|
|
using (var zip = new ZipArchive(stream, ZipArchiveMode.Create))
|
|
{
|
|
foreach (var file in Directory.GetFiles(root, "*", SearchOption.AllDirectories).OrderBy(f => f, StringComparer.Ordinal))
|
|
{
|
|
var relative = file.Substring(root.Length + 1).Replace(Path.DirectorySeparatorChar, '/');
|
|
Add(zip, file, top + "/" + relative);
|
|
count++;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(notePath) && File.Exists(notePath))
|
|
{
|
|
Add(zip, notePath!, Path.GetFileName(notePath));
|
|
count++;
|
|
}
|
|
}
|
|
|
|
File.Move(temp, zipPath);
|
|
return count;
|
|
}
|
|
catch
|
|
{
|
|
try { if (File.Exists(temp)) File.Delete(temp); } catch { /* 지우지 못해도 원래 오류를 알린다 */ }
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private static void Add(ZipArchive zip, string file, string entryName)
|
|
{
|
|
var entry = zip.CreateEntry(entryName, CompressionLevel.Optimal);
|
|
entry.LastWriteTime = File.GetLastWriteTime(file);
|
|
using var source = File.OpenRead(file);
|
|
using var target = entry.Open();
|
|
source.CopyTo(target);
|
|
}
|
|
}
|
|
}
|