Skip to content

Commit 393eaa1

Browse files
Adds the preset get command. Closes #438 (#441)
1 parent 129fdde commit 393eaa1

File tree

2 files changed

+254
-1
lines changed

2 files changed

+254
-1
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.DevProxy.Abstractions;
5+
using System.Net.Http.Headers;
6+
using System.Text.Json;
7+
using System.Text.Json.Serialization;
8+
9+
namespace Microsoft.DevProxy;
10+
11+
class ProxyPresetInfo
12+
{
13+
public IList<string> ConfigFiles { get; set; } = new List<string>();
14+
public IList<string> MockFiles { get; set; } = new List<string>();
15+
}
16+
17+
class GitHubTreeResponse
18+
{
19+
[JsonPropertyName("tree")]
20+
public GitHubTreeItem[] Tree { get; set; } = Array.Empty<GitHubTreeItem>();
21+
[JsonPropertyName("truncated")]
22+
public bool Truncated { get; set; }
23+
}
24+
25+
class GitHubTreeItem
26+
{
27+
[JsonPropertyName("path")]
28+
public string Path { get; set; } = string.Empty;
29+
[JsonPropertyName("type")]
30+
public string Type { get; set; } = string.Empty;
31+
}
32+
33+
public static class PresetGetCommandHandler
34+
{
35+
public static async Task DownloadPreset(string presetId, ILogger logger)
36+
{
37+
try
38+
{
39+
var appFolder = ProxyUtils.AppFolder;
40+
if (string.IsNullOrEmpty(appFolder) || !Directory.Exists(appFolder))
41+
{
42+
logger.LogError("App folder not found");
43+
return;
44+
}
45+
46+
var presetsFolderPath = Path.Combine(appFolder, "presets");
47+
logger.LogDebug($"Checking if presets folder {presetsFolderPath} exists...");
48+
if (!Directory.Exists(presetsFolderPath))
49+
{
50+
logger.LogDebug("Presets folder not found, creating it...");
51+
Directory.CreateDirectory(presetsFolderPath);
52+
logger.LogDebug("Presets folder created");
53+
}
54+
55+
logger.LogDebug($"Getting target folder path for preset {presetId}...");
56+
var targetFolderPath = GetTargetFolderPath(appFolder, presetId);
57+
logger.LogDebug($"Creating target folder {targetFolderPath}...");
58+
Directory.CreateDirectory(targetFolderPath);
59+
60+
logger.LogInfo($"Downloading preset {presetId}...");
61+
62+
var sampleFiles = await GetFilesToDownload(presetId, logger);
63+
foreach (var sampleFile in sampleFiles)
64+
{
65+
await DownloadFile(sampleFile, targetFolderPath, presetId, logger);
66+
}
67+
68+
logger.LogInfo($"Preset saved in {targetFolderPath}");
69+
var presetInfo = GetPresetInfo(targetFolderPath, logger);
70+
if (!presetInfo.ConfigFiles.Any() && !presetInfo.MockFiles.Any())
71+
{
72+
return;
73+
}
74+
75+
logger.LogInfo("");
76+
if (presetInfo.ConfigFiles.Any())
77+
{
78+
logger.LogInfo("To start Dev Proxy with the preset, run:");
79+
foreach (var configFile in presetInfo.ConfigFiles)
80+
{
81+
logger.LogInfo($" devproxy --config-file \"{configFile.Replace(appFolder, "~appFolder")}\"");
82+
}
83+
}
84+
else
85+
{
86+
logger.LogInfo("To start Dev Proxy with the mock file, enable the MockResponsePlugin or GraphMockResponsePlugin and run:");
87+
foreach (var mockFile in presetInfo.MockFiles)
88+
{
89+
logger.LogInfo($" devproxy --mock-file \"{mockFile.Replace(appFolder, "~appFolder")}\"");
90+
}
91+
}
92+
}
93+
catch (Exception ex)
94+
{
95+
logger.LogError(ex.Message);
96+
}
97+
}
98+
99+
/// <summary>
100+
/// Returns the list of files that can be used as entry points for the preset
101+
/// </summary>
102+
/// <remarks>
103+
/// A sample in the gallery can have multiple entry points. It can
104+
/// contain multiple config files or no config files and a multiple
105+
/// mock files. This method returns the list of files that Dev Proxy
106+
/// can use as entry points.
107+
/// If there's one or more config files, it'll return an array of
108+
/// these file names. If there are no proxy configs, it'll return
109+
/// an array of all the mock files. If there are no mocks, it'll return
110+
/// an empty array indicating that there's no entry point.
111+
/// </remarks>
112+
/// <param name="presetFolder">Full path to the folder with preset files</param>
113+
/// <returns>Array of files that can be used to start proxy with</returns>
114+
private static ProxyPresetInfo GetPresetInfo(string presetFolder, ILogger logger)
115+
{
116+
var presetInfo = new ProxyPresetInfo();
117+
118+
logger.LogDebug($"Getting list of JSON files in {presetFolder}...");
119+
var jsonFiles = Directory.GetFiles(presetFolder, "*.json");
120+
if (!jsonFiles.Any())
121+
{
122+
logger.LogDebug("No JSON files found");
123+
return presetInfo;
124+
}
125+
126+
foreach (var jsonFile in jsonFiles)
127+
{
128+
logger.LogDebug($"Reading file {jsonFile}...");
129+
130+
var fileContents = File.ReadAllText(jsonFile);
131+
if (fileContents.Contains("\"plugins\":"))
132+
{
133+
logger.LogDebug($"File {jsonFile} contains proxy config");
134+
presetInfo.ConfigFiles.Add(jsonFile);
135+
continue;
136+
}
137+
138+
if (fileContents.Contains("\"responses\":"))
139+
{
140+
logger.LogDebug($"File {jsonFile} contains mock data");
141+
presetInfo.MockFiles.Add(jsonFile);
142+
continue;
143+
}
144+
145+
logger.LogDebug($"File {jsonFile} is not a proxy config or mock data");
146+
}
147+
148+
if (presetInfo.ConfigFiles.Any())
149+
{
150+
logger.LogDebug($"Found {presetInfo.ConfigFiles.Count} proxy config files. Clearing mocks...");
151+
presetInfo.MockFiles.Clear();
152+
}
153+
154+
return presetInfo;
155+
}
156+
157+
private static string GetTargetFolderPath(string appFolder, string presetId)
158+
{
159+
var baseFolder = Path.Combine(appFolder, "presets", presetId);
160+
var newFolder = baseFolder;
161+
var i = 1;
162+
while (Directory.Exists(newFolder))
163+
{
164+
newFolder = baseFolder + i++;
165+
}
166+
167+
return newFolder;
168+
}
169+
170+
private static async Task<string[]> GetFilesToDownload(string sampleFolderName, ILogger logger)
171+
{
172+
logger.LogDebug($"Getting list of files in Dev Proxy samples repo...");
173+
var url = $"https://api.github.com/repos/pnp/proxy-samples/git/trees/main?recursive=1";
174+
using (var client = new HttpClient())
175+
{
176+
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("dev-proxy", ProxyUtils.ProductVersion));
177+
var response = await client.GetAsync(url);
178+
179+
if (response.IsSuccessStatusCode)
180+
{
181+
var content = await response.Content.ReadAsStringAsync();
182+
var tree = JsonSerializer.Deserialize<GitHubTreeResponse>(content);
183+
if (tree is null)
184+
{
185+
throw new Exception("Failed to get list of files from GitHub");
186+
}
187+
188+
var samplePath = $"samples/{sampleFolderName}";
189+
190+
var filesToDownload = tree.Tree
191+
.Where(f => f.Path.StartsWith(samplePath, StringComparison.OrdinalIgnoreCase) && f.Type == "blob")
192+
.Select(f => f.Path)
193+
.ToArray();
194+
195+
foreach (var file in filesToDownload)
196+
{
197+
logger.LogDebug($"Found file {file}");
198+
}
199+
200+
return filesToDownload;
201+
}
202+
else
203+
{
204+
throw new Exception($"Failed to get list of files from GitHub. Status code: {response.StatusCode}");
205+
}
206+
}
207+
}
208+
209+
private static async Task DownloadFile(string filePath, string targetFolderPath, string presetId, ILogger logger)
210+
{
211+
var url = $"https://raw.githubusercontent.com/pnp/proxy-samples/main/{filePath.Replace("#", "%23")}";
212+
logger.LogDebug($"Downloading file {filePath}...");
213+
214+
using (var client = new HttpClient())
215+
{
216+
var response = await client.GetAsync(url);
217+
218+
if (response.IsSuccessStatusCode)
219+
{
220+
var contentStream = await response.Content.ReadAsStreamAsync();
221+
var filePathInsideSample = Path.GetRelativePath($"samples/{presetId}", filePath);
222+
var directoryNameInsideSample = Path.GetDirectoryName(filePathInsideSample);
223+
if (directoryNameInsideSample is not null)
224+
{
225+
Directory.CreateDirectory(Path.Combine(targetFolderPath, directoryNameInsideSample));
226+
}
227+
var localFilePath = Path.Combine(targetFolderPath, filePathInsideSample);
228+
229+
using (var fileStream = new FileStream(localFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
230+
{
231+
await contentStream.CopyToAsync(fileStream);
232+
}
233+
234+
logger.LogDebug($"File downloaded successfully to {localFilePath}");
235+
}
236+
else
237+
{
238+
throw new Exception($"Failed to download file {url}. Status code: {response.StatusCode}");
239+
}
240+
}
241+
}
242+
}

dev-proxy/ProxyHost.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.DevProxy.Abstractions;
55
using System.CommandLine;
6+
using System.CommandLine.Invocation;
67
using System.Net;
78

89
namespace Microsoft.DevProxy;
@@ -209,9 +210,19 @@ public RootCommand GetRootCommand(ILogger logger)
209210
};
210211
command.Add(msGraphDbCommand);
211212

213+
var presetCommand = new Command("preset", "Manage Dev Proxy presets");
214+
215+
var presetGetCommand = new Command("get", "Download the specified preset from the Sample Solution Gallery");
216+
var presetIdArgument = new Argument<string>("preset-id", "The ID of the preset to download");
217+
presetGetCommand.AddArgument(presetIdArgument);
218+
presetGetCommand.SetHandler(async presetId => await PresetGetCommandHandler.DownloadPreset(presetId, logger), presetIdArgument);
219+
presetCommand.Add(presetGetCommand);
220+
221+
command.Add(presetCommand);
222+
212223
return command;
213224
}
214225

215-
public ProxyCommandHandler GetCommandHandler(PluginEvents pluginEvents, ISet<UrlToWatch> urlsToWatch, ILogger logger) => new ProxyCommandHandler(_portOption, _ipAddressOption, _logLevelOption, _recordOption, _watchPidsOption, _watchProcessNamesOption, _rateOption, _noFirstRunOption, pluginEvents, urlsToWatch, logger);
226+
public ProxyCommandHandler GetCommandHandler(PluginEvents pluginEvents, ISet<UrlToWatch> urlsToWatch, ILogger logger) => new ProxyCommandHandler(_portOption, _ipAddressOption, _logLevelOption!, _recordOption, _watchPidsOption, _watchProcessNamesOption, _rateOption, _noFirstRunOption, pluginEvents, urlsToWatch, logger);
216227
}
217228

0 commit comments

Comments
 (0)