using System.IO; using System.IO.Compression; using System.Text.Json; namespace AxCopilot.Services.Agent; /// /// 파일 압축(zip) / 해제 도구. /// public class ZipTool : IAgentTool { public string Name => "zip_tool"; public string Description => "Compress or extract zip archives. Actions: " + "'compress' — create a zip file from files/folders; " + "'extract' — extract a zip file to a directory; " + "'list' — list contents of a zip file without extracting."; public ToolParameterSchema Parameters => new() { Properties = new() { ["action"] = new() { Type = "string", Description = "Action to perform", Enum = ["compress", "extract", "list"], }, ["zip_path"] = new() { Type = "string", Description = "Path to the zip file (to create or extract)", }, ["source_path"] = new() { Type = "string", Description = "Source file or directory path (for compress action)", }, ["dest_path"] = new() { Type = "string", Description = "Destination directory (for extract action)", }, }, Required = ["action", "zip_path"], }; private const long MaxExtractSize = 500 * 1024 * 1024; // 500MB 제한 public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { var action = args.GetProperty("action").SafeGetString() ?? ""; var zipPath = args.GetProperty("zip_path").SafeGetString() ?? ""; if (!Path.IsPathRooted(zipPath)) zipPath = Path.Combine(context.WorkFolder, zipPath); try { return Task.FromResult(action switch { "compress" => Compress(args, zipPath, context), "extract" => Extract(args, zipPath, context), "list" => ListContents(zipPath), _ => ToolResult.Fail($"Unknown action: {action}"), }); } catch (Exception ex) { return Task.FromResult(ToolResult.Fail($"Zip 오류: {ex.Message}")); } } private static ToolResult Compress(JsonElement args, string zipPath, AgentContext context) { if (!args.SafeTryGetProperty("source_path", out var sp)) return ToolResult.Fail("'source_path' is required for compress action"); var sourcePath = sp.SafeGetString() ?? ""; if (!Path.IsPathRooted(sourcePath)) sourcePath = Path.Combine(context.WorkFolder, sourcePath); if (File.Exists(zipPath)) return ToolResult.Fail($"Zip file already exists: {zipPath}"); if (Directory.Exists(sourcePath)) { ZipFile.CreateFromDirectory(sourcePath, zipPath, CompressionLevel.Optimal, includeBaseDirectory: false); var info = new FileInfo(zipPath); return ToolResult.Ok($"✓ Created {zipPath} ({info.Length / 1024}KB)", zipPath); } else if (File.Exists(sourcePath)) { using var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create); zip.CreateEntryFromFile(sourcePath, Path.GetFileName(sourcePath), CompressionLevel.Optimal); var info = new FileInfo(zipPath); return ToolResult.Ok($"✓ Created {zipPath} ({info.Length / 1024}KB)", zipPath); } else { return ToolResult.Fail($"Source not found: {sourcePath}"); } } private static ToolResult Extract(JsonElement args, string zipPath, AgentContext context) { if (!File.Exists(zipPath)) return ToolResult.Fail($"Zip file not found: {zipPath}"); var destPath = args.SafeTryGetProperty("dest_path", out var dp) ? dp.SafeGetString() ?? "" : ""; if (string.IsNullOrEmpty(destPath)) destPath = Path.Combine(Path.GetDirectoryName(zipPath) ?? context.WorkFolder, Path.GetFileNameWithoutExtension(zipPath)); if (!Path.IsPathRooted(destPath)) destPath = Path.Combine(context.WorkFolder, destPath); // 사이즈 체크 using (var check = ZipFile.OpenRead(zipPath)) { var totalSize = check.Entries.Sum(e => e.Length); if (totalSize > MaxExtractSize) return ToolResult.Fail($"Uncompressed size ({totalSize / 1024 / 1024}MB) exceeds 500MB limit"); // 보안: 상위 경로 이탈 방지 foreach (var entry in check.Entries) { var fullPath = Path.GetFullPath(Path.Combine(destPath, entry.FullName)); if (!fullPath.StartsWith(Path.GetFullPath(destPath), StringComparison.OrdinalIgnoreCase)) return ToolResult.Fail($"Security: entry '{entry.FullName}' escapes destination directory"); } } Directory.CreateDirectory(destPath); ZipFile.ExtractToDirectory(zipPath, destPath, overwriteFiles: true); var fileCount = Directory.GetFiles(destPath, "*", SearchOption.AllDirectories).Length; return ToolResult.Ok($"✓ Extracted {fileCount} files to {destPath}"); } private static ToolResult ListContents(string zipPath) { if (!File.Exists(zipPath)) return ToolResult.Fail($"Zip file not found: {zipPath}"); using var zip = ZipFile.OpenRead(zipPath); var sb = new System.Text.StringBuilder(); sb.AppendLine($"Archive: {Path.GetFileName(zipPath)} ({new FileInfo(zipPath).Length / 1024}KB)"); sb.AppendLine($"Entries: {zip.Entries.Count}"); sb.AppendLine(); sb.AppendLine($"{"Size",10} {"Compressed",10} {"Name"}"); sb.AppendLine(new string('-', 60)); var limit = Math.Min(zip.Entries.Count, 200); foreach (var entry in zip.Entries.Take(limit)) { sb.AppendLine($"{entry.Length,10} {entry.CompressedLength,10} {entry.FullName}"); } if (zip.Entries.Count > limit) sb.AppendLine($"\n... and {zip.Entries.Count - limit} more entries"); var totalUncompressed = zip.Entries.Sum(e => e.Length); sb.AppendLine($"\nTotal uncompressed: {totalUncompressed / 1024}KB"); return ToolResult.Ok(sb.ToString()); } }