/// <summary> /// Maps the given <see cref="IdeaPlugin"/> to a <see cref="Plugin"/>. /// </summary> /// <param name="this">The IdeaPlugin.</param> /// <param name="dbCategory">The plugin category.</param> /// <returns>The mapped plugin.</returns> public static Plugin ToEntity ( this IdeaPlugin @this, PluginCategory dbCategory ) { DateTime ParseDateFromMilliseconds(string value) { var millis = long.Parse(value); return(DateTimeOffset.FromUnixTimeMilliseconds(millis).UtcDateTime); } var result = new Plugin ( @this.Name, dbCategory, @this.ID, @this.Description, @this.Vendor.ToEntity() ); result.Tags = @this.Tags?.Split(';').ToList() ?? new List <string>(); result.Rating = @this.Rating; result.ProjectURL = @this.ProjectURL ?? string.Empty; result.UpdatedAt = ParseDateFromMilliseconds ( @this.UpdateDate ?? @this.UploadDate ?? throw new InvalidOperationException() ); return(result); }
public async Task <HttpResponseMessage> DownloadIconAsync ( IdeaPlugin plugin, string?theme, CancellationToken ct ) { var query = HttpUtility.ParseQueryString(string.Empty); query[Endpoints.API.Icon.Parameters.PluginID] = plugin.ID; if (!(theme is null)) { query[Endpoints.API.Icon.Parameters.Theme] = theme.ToUpperInvariant(); } var uriBuilder = new UriBuilder("https", _baseURL) { Path = Endpoints.API.Icon.BasePath, Query = query.ToString() ?? string.Empty }; // Resolve moved requests. var data = await _httpClient.GetAsync(uriBuilder.Uri, HttpCompletionOption.ResponseHeadersRead, ct); while (!data.IsSuccessStatusCode && (data.StatusCode == HttpStatusCode.MovedPermanently || data.StatusCode == HttpStatusCode.Found)) { var newData = await _httpClient.GetAsync(data.Headers.Location, HttpCompletionOption.ResponseHeadersRead, ct); data.Dispose(); data = newData; } return(data); }
private static async Task <bool> ImportPluginReleaseAsync ( PluginsDatabaseContext db, IdeaPlugin pluginRelease ) { var dbPlugin = await db.Plugins.FirstOrDefaultAsync(p => p.PluginID == pluginRelease.ID); var dbRelease = dbPlugin.Releases.FirstOrDefault(r => r.Version == pluginRelease.Version); if (dbRelease is null) { dbRelease = pluginRelease.ToReleaseEntity(dbPlugin); dbPlugin.Releases.Add(dbRelease); } else { var newValues = pluginRelease.ToReleaseEntity(dbPlugin); newValues.ID = dbRelease.ID; var existingEntry = db.Entry(dbRelease); existingEntry.CurrentValues.SetValues(newValues); if (existingEntry.State == EntityState.Unchanged) { return(false); } } return(true); }
public static DownloadResult FromError(IdeaPlugin plugin, IResult <DownloadError> result) { if (result.IsSuccess) { throw new InvalidOperationException("The original result was successful."); } return(FromError(plugin, result.Error !.Value, result.ErrorReason)); }
public static DownloadResult FromError ( IdeaPlugin plugin, DownloadError error, string?reason, Exception?exception = null ) { return(new DownloadResult(plugin, error, reason, exception)); }
private static async Task <bool> ImportPluginAsync ( PluginsDatabaseContext db, IdeaPlugin plugin, IdeaPluginCategory category ) { var dbCategory = await db.Categories.FirstOrDefaultAsync(c => c.Name == category.Name); if (dbCategory is null) { return(false); } var dbPlugin = await db.Plugins.FirstOrDefaultAsync(p => p.PluginID == plugin.ID); if (dbPlugin is null) { dbPlugin = plugin.ToEntity(dbCategory); db.Plugins.Add(dbPlugin); } else { var newValues = plugin.ToEntity(dbCategory); newValues.ID = dbPlugin.ID; var existingEntry = db.Entry(dbPlugin); existingEntry.CurrentValues.SetValues(newValues); if (existingEntry.State == EntityState.Unchanged) { return(false); } } return(true); }
/// <summary> /// Downloads the given plugin. /// </summary> /// <param name="plugin">The plugin.</param> /// <param name="ct">The cancellation token to use.</param> /// <returns>The response from the server.</returns> public Task <HttpResponseMessage> DownloadAsync(IdeaPlugin plugin, CancellationToken ct) => DownloadSpecificAsync(plugin.ID, plugin.Version, ct);
/// <summary> /// Serves GET requests to this controller. /// </summary> /// <param name="build">The build to list plugins for.</param> /// <returns>The plugins compatible with the given IDE version.</returns> public ActionResult <IdeaPluginRepository> Get(string build) { if (!IDEVersion.TryParse(build, out var ideVersion)) { return(BadRequest()); } var categories = _database.Categories.ToList(); var ideaCategories = categories.Select(c => new IdeaPluginCategory(c.Name)).ToList(); foreach (var category in _database.Categories) { var ideaCategory = ideaCategories.First(c => c.Name == category.Name); foreach (var plugin in category.Plugins) { var compatibleRelease = plugin.Releases .OrderBy(r => r.UploadedAt) .FirstOrDefault(r => r.CompatibleWith.IsInRange(ideVersion)); if (compatibleRelease is null) { continue; } var ideaPlugin = new IdeaPlugin ( plugin.Name, plugin.PluginID, plugin.Description, compatibleRelease.Version, new IdeaVendor { Email = plugin.Vendor.Email, Name = plugin.Vendor.Name, URL = plugin.Vendor.URL }, new IdeaVersion { UntilBuild = compatibleRelease.CompatibleWith.UntilBuild.IsValid ? compatibleRelease.CompatibleWith.UntilBuild.ToString() : null, SinceBuild = compatibleRelease.CompatibleWith.SinceBuild.IsValid ? compatibleRelease.CompatibleWith.SinceBuild.ToString() : null, }, compatibleRelease.ChangeNotes ) { ProjectURL = plugin.ProjectURL, Tags = plugin.Tags.Aggregate((a, b) => a + ";" + b), Depends = compatibleRelease.Dependencies, Downloads = compatibleRelease.Downloads, Size = compatibleRelease.Size, Rating = plugin.Rating, UploadDate = (compatibleRelease.UploadedAt.ToFileTimeUtc() / 10000).ToString(), UpdateDate = (plugin.Releases.Select(r => r.UploadedAt).OrderBy(d => d).First().ToFileTimeUtc() / 10000).ToString() }; ideaCategory.Plugins.Add(ideaPlugin); } } var ideaRepository = new IdeaPluginRepository(); ideaRepository.Categories = ideaCategories; return(new ActionResult <IdeaPluginRepository>(ideaRepository)); }
/// <summary> /// Maps the given <see cref="IdeaPlugin"/> to a <see cref="PluginRelease"/>. /// </summary> /// <param name="this">The IdeaPlugin.</param> /// <param name="dbPlugin">The plugin that the release belongs to.</param> /// <returns>The mapped release.</returns> public static PluginRelease ToReleaseEntity ( this IdeaPlugin @this, Plugin dbPlugin ) { string?SelectBestSinceValue(IdeaVersion version) { if (version.SinceBuild is null || version.SinceBuild == "n/a") { return(version.Min); } return(version.SinceBuild); } string?SelectBestUntilValue(IdeaVersion version) { if (version.UntilBuild is null || version.UntilBuild == "n/a") { return(version.Max); } return(version.UntilBuild); } DateTime ParseDateFromMilliseconds(string value) { var millis = long.Parse(value); return(DateTimeOffset.FromUnixTimeMilliseconds(millis).UtcDateTime); } var sinceBuildValue = SelectBestSinceValue(@this.IdeaVersion); IDEVersion?sinceBuild = null; if (!(sinceBuildValue is null)) { if (!IDEVersion.TryParse(sinceBuildValue, out sinceBuild)) { throw new InvalidDataException("Bad version string."); } } var untilBuildValue = SelectBestUntilValue(@this.IdeaVersion); IDEVersion?untilBuild = null; if (!(untilBuildValue is null)) { if (!IDEVersion.TryParse(untilBuildValue, out untilBuild)) { throw new InvalidDataException("Bad version string."); } } var versionRange = new IDEVersionRange ( sinceBuild ?? IDEVersion.Invalid, untilBuild ?? IDEVersion.Invalid ); // Get the file size and hash // TODO: refactor var basePath = Program.Options.InputFolder; var pluginFolder = Path.Combine ( basePath, "plugins", dbPlugin.Category.Name.GenerateSlug(), dbPlugin.Name.GenerateSlug(), @this.Version ); var pluginFile = Directory.EnumerateFiles(pluginFolder).FirstOrDefault(); if (pluginFile is null) { throw new FileNotFoundException("Couldn't find the released plugin file. Missing data?"); } string hash; using (var md5 = MD5.Create()) { using var file = File.OpenRead(pluginFile); var md5Sum = md5.ComputeHash(file); hash = BitConverter.ToString(md5Sum).Replace("-", string.Empty).ToLowerInvariant(); } var fileInfo = new FileInfo(pluginFile); var size = fileInfo.Length; var result = new PluginRelease ( dbPlugin, @this.ChangeNotes, size, ParseDateFromMilliseconds ( @this.UpdateDate ?? @this.UploadDate ?? throw new InvalidOperationException() ), hash, @this.Version, versionRange, @this.Depends ); result.Downloads = @this.Downloads; return(result); }
private async Task <DownloadResult> DownloadPluginAsync ( string targetDirectory, IdeaPlugin plugin, CancellationToken ct ) { // Try downloading the icon variants var iconTargetDirectory = Path.Combine(Program.Options.OutputFolder, "icons"); await DownloadIconAsync(iconTargetDirectory, plugin, null, ct); await DownloadIconAsync(iconTargetDirectory, plugin, "DARCULA", ct); // First, let's perform a quick existing file check against the values in the reported plugin var sluggedPluginName = plugin.Name.GenerateSlug(); var version = plugin.Version; var saveDirectory = Path.Combine ( targetDirectory, sluggedPluginName, version ); if (Directory.Exists(saveDirectory)) { var existingFile = Directory.GetFiles(saveDirectory).FirstOrDefault(); if (!(existingFile is null)) { if (new FileInfo(existingFile).Length == plugin.Size) { // Looks like we already have this one return(DownloadResult.FromSuccess(plugin, DownloadAction.Skipped)); } } } try { using var data = await _api.DownloadAsync(plugin, ct); if (!data.IsSuccessStatusCode) { return(DownloadResult.FromError(plugin, DownloadError.Unknown, data.ReasonPhrase)); } string?filename = null; if (data.Content.Headers?.ContentDisposition?.FileName is null) { // Try an alternate way var alternatePath = data.RequestMessage?.RequestUri?.AbsolutePath; if (!(alternatePath is null)) { if (Path.HasExtension(alternatePath)) { filename = Path.GetFileName(alternatePath); } } } else { filename = data.Content.Headers.ContentDisposition.FileName; } if (filename is null) { return(DownloadResult.FromError ( plugin, DownloadError.Unknown, "Failed to retrieve file information from the download headers." )); } Directory.CreateDirectory(saveDirectory); var savePath = Path.Combine(saveDirectory, filename.Replace("\"", string.Empty)); if (File.Exists(savePath)) { var expectedSize = data.Content.Headers?.ContentLength ?? plugin.Size; if (new FileInfo(savePath).Length == expectedSize) { // Looks like we already have this one return(DownloadResult.FromSuccess(plugin, DownloadAction.Skipped)); } // It's crap, so delete it and download again File.Delete(savePath); } // Download to a temporary file first var tempFile = Path.GetTempFileName(); await using (var tempOutput = File.Create(tempFile)) { await using var contentStream = await data.Content.ReadAsStreamAsync(); await contentStream.CopyToAsync(tempOutput, ct); } // And then move it over to the final save location File.Move(tempFile, savePath); return(DownloadResult.FromSuccess(plugin, DownloadAction.Downloaded)); } catch (TimeoutException tex) { return(DownloadResult.FromError(plugin, DownloadError.Timeout, tex.Message)); } catch (Exception ex) { return(DownloadResult.FromError(plugin, DownloadError.Exception, ex.Message, ex)); } }
/// <summary> /// Creates a successful result. /// </summary> /// <param name="plugin">The plugin that was successfully downloaded.</param> /// <param name="action">The action that was performed.</param> /// <returns>The result.</returns> public static DownloadResult FromSuccess(IdeaPlugin plugin, DownloadAction action) { return(new DownloadResult(plugin, action)); }
/// <summary> /// Initializes a new instance of the <see cref="DownloadResult"/> class. /// </summary> /// <param name="plugin">The plugin that was downloaded.</param> /// <param name="action">The action that was performed.</param> private DownloadResult(IdeaPlugin plugin, DownloadAction action) { this.Plugin = plugin; this.Action = action; }
public static DownloadResult FromError(IdeaPlugin plugin, Exception exception, string reason) { return(FromError(plugin, DownloadError.Exception, reason, exception)); }
public static DownloadResult FromError(IdeaPlugin plugin, Exception exception) { return(FromError(plugin, DownloadError.Exception, exception.Message, exception)); }