|
| 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 | +} |
0 commit comments