Initial commit to new repository
This commit is contained in:
167
src/AxCopilot/Services/Agent/ZipTool.cs
Normal file
167
src/AxCopilot/Services/Agent/ZipTool.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// 파일 압축(zip) / 해제 도구.
|
||||
/// </summary>
|
||||
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<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
|
||||
{
|
||||
var action = args.GetProperty("action").GetString() ?? "";
|
||||
var zipPath = args.GetProperty("zip_path").GetString() ?? "";
|
||||
|
||||
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.TryGetProperty("source_path", out var sp))
|
||||
return ToolResult.Fail("'source_path' is required for compress action");
|
||||
|
||||
var sourcePath = sp.GetString() ?? "";
|
||||
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.TryGetProperty("dest_path", out var dp)
|
||||
? dp.GetString() ?? "" : "";
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user