Skip to content

Commit 47f921c

Browse files
.Net: Updates to WebFileDownloadPlugin (#11415)
### Motivation and Context 1. `AllowedDomains` - Provide control over which domains can be downloaded from 2. `AllowedFolders` - Provide control over which folders can be written to 3. `DisableFileOverwrite` - Disable overwriting existing files 4. `MaximumDownloadSize` - Set a maximum size of a download that will be written ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄
1 parent 97fb2c3 commit 47f921c

File tree

7 files changed

+380
-7
lines changed

7 files changed

+380
-7
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.SemanticKernel.Plugins.Web;
4+
5+
namespace Plugins;
6+
7+
/// <summary>
8+
/// Sample showing how to use the Semantic Kernel web plugins correctly.
9+
/// </summary>
10+
public sealed class WebPlugins(ITestOutputHelper output) : BaseTest(output)
11+
{
12+
/// <summary>
13+
/// Shows how to download to a temporary directory on the local machine.
14+
/// </summary>
15+
[Fact]
16+
public async Task DownloadSKLogoAsync()
17+
{
18+
var uri = new Uri("https://raw.githubusercontent.com/microsoft/semantic-kernel/refs/heads/main/docs/images/sk_logo.png");
19+
var folderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
20+
var filePath = Path.Combine(folderPath, "sk_logo.png");
21+
22+
try
23+
{
24+
Directory.CreateDirectory(folderPath);
25+
26+
var webFileDownload = new WebFileDownloadPlugin(this.LoggerFactory)
27+
{
28+
AllowedDomains = ["raw.githubusercontent.com"],
29+
AllowedFolders = [folderPath]
30+
};
31+
32+
await webFileDownload.DownloadToFileAsync(uri, filePath);
33+
34+
if (Path.Exists(filePath))
35+
{
36+
Output.WriteLine($"Successfully downloaded to {filePath}");
37+
}
38+
}
39+
finally
40+
{
41+
if (Path.Exists(folderPath))
42+
{
43+
Directory.Delete(folderPath, true);
44+
}
45+
}
46+
}
47+
}

dotnet/samples/Concepts/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom
183183
- [ImportPluginFromGrpc](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/ImportPluginFromGrpc.cs)
184184
- [TransformPlugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/TransformPlugin.cs)
185185
- [CopilotAgentBasedPlugins](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CopilotAgentBasedPlugins.cs)
186+
- [WebPlaugins](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/WebPlugins.cs)
186187

187188
### PromptTemplates - Using [`Templates`](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/IPromptTemplate.cs) with parametrization for `Prompt` rendering
188189

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.IO;
5+
using System.Threading.Tasks;
6+
using Microsoft.SemanticKernel.Plugins.Web;
7+
using Xunit;
8+
9+
namespace SemanticKernel.IntegrationTests.Plugins.Web;
10+
11+
/// <summary>
12+
/// Integration tests for <see cref="WebFileDownloadPlugin"/>.
13+
/// </summary>
14+
public sealed class WebFileDownloadPluginTests : BaseIntegrationTest
15+
{
16+
/// <summary>
17+
/// Verify downloading to a temporary directory on the local machine.
18+
/// </summary>
19+
[Fact]
20+
public async Task VerifyDownloadToFileAsync()
21+
{
22+
var uri = new Uri("https://raw.githubusercontent.com/microsoft/semantic-kernel/refs/heads/main/docs/images/sk_logo.png");
23+
var folderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
24+
var filePath = Path.Combine(folderPath, "sk_logo.png");
25+
26+
try
27+
{
28+
Directory.CreateDirectory(folderPath);
29+
30+
var webFileDownload = new WebFileDownloadPlugin()
31+
{
32+
AllowedDomains = ["raw.githubusercontent.com"],
33+
AllowedFolders = [folderPath]
34+
};
35+
36+
await webFileDownload.DownloadToFileAsync(uri, filePath);
37+
38+
Assert.True(Path.Exists(filePath));
39+
}
40+
finally
41+
{
42+
if (Path.Exists(folderPath))
43+
{
44+
Directory.Delete(folderPath, true);
45+
}
46+
}
47+
}
48+
}

dotnet/src/InternalUtilities/test/MultipleHttpMessageHandlerStub.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ internal void AddJsonResponse(string json)
3737
});
3838
}
3939

40+
internal void AddImageResponse(byte[] image)
41+
{
42+
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
43+
{
44+
Content = new ByteArrayContent(image)
45+
};
46+
response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
47+
this.ResponsesToReturn.Add(response);
48+
}
49+
4050
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
4151
{
4252
this._callIteration++;
76.1 KB
Loading
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.IO;
5+
using System.Net.Http;
6+
using System.Threading.Tasks;
7+
using Microsoft.SemanticKernel.Plugins.Web;
8+
using Xunit;
9+
10+
namespace SemanticKernel.Plugins.UnitTests.Web;
11+
12+
/// <summary>
13+
/// Unit tests for <see cref="WebFileDownloadPlugin"/>
14+
/// </summary>
15+
public sealed class WebFileDownloadPluginTests : IDisposable
16+
{
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="WebFileDownloadPluginTests"/> class.
19+
/// </summary>
20+
public WebFileDownloadPluginTests()
21+
{
22+
this._messageHandlerStub = new MultipleHttpMessageHandlerStub();
23+
this._httpClient = new HttpClient(this._messageHandlerStub, disposeHandler: false);
24+
}
25+
26+
[Fact]
27+
public async Task DownloadToFileSucceedsAsync()
28+
{
29+
// Arrange
30+
this._messageHandlerStub.AddImageResponse(File.ReadAllBytes(SKLogoPng));
31+
var uri = new Uri("https://raw.githubusercontent.com/microsoft/semantic-kernel/refs/heads/main/docs/images/sk_logo.png");
32+
var folderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
33+
var filePath = Path.Combine(folderPath, "sk_logo.png");
34+
Directory.CreateDirectory(folderPath);
35+
36+
var webFileDownload = new WebFileDownloadPlugin()
37+
{
38+
AllowedDomains = ["raw.githubusercontent.com"],
39+
AllowedFolders = [folderPath]
40+
};
41+
42+
try
43+
{
44+
// Act
45+
await webFileDownload.DownloadToFileAsync(uri, filePath);
46+
47+
// Assert
48+
Assert.True(Path.Exists(filePath));
49+
}
50+
finally
51+
{
52+
if (Path.Exists(folderPath))
53+
{
54+
Directory.Delete(folderPath, true);
55+
}
56+
}
57+
}
58+
59+
[Fact]
60+
public async Task DownloadToFileFailsForInvalidDomainAsync()
61+
{
62+
// Arrange
63+
this._messageHandlerStub.AddImageResponse(File.ReadAllBytes(SKLogoPng));
64+
var uri = new Uri("https://raw.githubfakecontent.com/microsoft/semantic-kernel/refs/heads/main/docs/images/sk_logo.png");
65+
var folderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
66+
var filePath = Path.Combine(folderPath, "sk_logo.png");
67+
Directory.CreateDirectory(folderPath);
68+
69+
var webFileDownload = new WebFileDownloadPlugin()
70+
{
71+
AllowedDomains = ["raw.githubusercontent.com"],
72+
AllowedFolders = [folderPath]
73+
};
74+
75+
// Act & Assert
76+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await webFileDownload.DownloadToFileAsync(uri, filePath));
77+
}
78+
79+
[Fact]
80+
public void ValidatePluginProperties()
81+
{
82+
// Arrange
83+
var folderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
84+
var filePath = Path.Combine(Path.GetTempPath(), "sk_logo.png");
85+
86+
// Act
87+
var webFileDownload = new WebFileDownloadPlugin()
88+
{
89+
AllowedDomains = ["raw.githubusercontent.com"],
90+
AllowedFolders = [folderPath],
91+
MaximumDownloadSize = 100,
92+
DisableFileOverwrite = true
93+
};
94+
95+
// Act & Assert
96+
Assert.Equal(["raw.githubusercontent.com"], webFileDownload.AllowedDomains);
97+
Assert.Equal([folderPath], webFileDownload.AllowedFolders);
98+
Assert.Equal(100, webFileDownload.MaximumDownloadSize);
99+
Assert.True(webFileDownload.DisableFileOverwrite);
100+
}
101+
102+
[Fact]
103+
public async Task DownloadToFileFailsForInvalidParametersAsync()
104+
{
105+
// Arrange
106+
this._messageHandlerStub.AddImageResponse(File.ReadAllBytes(SKLogoPng));
107+
var validUri = new Uri("https://raw.githubusercontent.com/microsoft/semantic-kernel/refs/heads/main/docs/images/sk_logo.png");
108+
var invalidUri = new Uri("https://raw.githubfakecontent.com/microsoft/semantic-kernel/refs/heads/main/docs/images/sk_logo.png");
109+
var folderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
110+
var validFilePath = Path.Combine(folderPath, "sk_logo.png");
111+
var invalidFilePath = Path.Combine(Path.GetTempPath(), "sk_logo.png");
112+
Directory.CreateDirectory(folderPath);
113+
114+
var webFileDownload = new WebFileDownloadPlugin()
115+
{
116+
AllowedDomains = ["raw.githubusercontent.com"],
117+
AllowedFolders = [folderPath],
118+
MaximumDownloadSize = 100
119+
};
120+
121+
// Act & Assert
122+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await webFileDownload.DownloadToFileAsync(validUri, validFilePath));
123+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await webFileDownload.DownloadToFileAsync(invalidUri, validFilePath));
124+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await webFileDownload.DownloadToFileAsync(validUri, invalidFilePath));
125+
await Assert.ThrowsAsync<ArgumentException>(async () => await webFileDownload.DownloadToFileAsync(validUri, "\\\\UNC\\server\\folder\\myfile.txt"));
126+
await Assert.ThrowsAsync<ArgumentException>(async () => await webFileDownload.DownloadToFileAsync(validUri, ""));
127+
await Assert.ThrowsAsync<ArgumentException>(async () => await webFileDownload.DownloadToFileAsync(validUri, "myfile.txt"));
128+
}
129+
130+
/// <inheritdoc/>
131+
public void Dispose()
132+
{
133+
this._messageHandlerStub.Dispose();
134+
this._httpClient.Dispose();
135+
GC.SuppressFinalize(this);
136+
}
137+
138+
#region private
139+
private const string SKLogoPng = "./TestData/sk_logo.png";
140+
141+
private readonly MultipleHttpMessageHandlerStub _messageHandlerStub;
142+
private readonly HttpClient _httpClient;
143+
#endregion
144+
}

0 commit comments

Comments
 (0)