diff --git a/GreenshotJiraPlugin/AsyncMemoryCache.cs b/GreenshotJiraPlugin/AsyncMemoryCache.cs new file mode 100644 index 000000000..f7d029750 --- /dev/null +++ b/GreenshotJiraPlugin/AsyncMemoryCache.cs @@ -0,0 +1,246 @@ +#region Dapplo 2016 - GNU Lesser General Public License + +// Dapplo - building blocks for .NET applications +// Copyright (C) 2016 Dapplo +// +// For more information see: http://dapplo.net/ +// Dapplo repositories are hosted on GitHub: https://github.com/dapplo +// +// This file is part of Dapplo.Utils +// +// Dapplo.Utils is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Dapplo.Utils is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have a copy of the GNU Lesser General Public License +// along with Dapplo.Utils. If not, see . + +#endregion + +#region Usings + +using System; +using System.Runtime.Caching; +using System.Threading; +using System.Threading.Tasks; +using Dapplo.Log.Facade; + +#endregion + + +namespace GreenshotJiraPlugin +{ + /// + /// This abstract class builds a base for a simple async memory cache. + /// + /// Type for the key + /// Type for the stored value + public abstract class AsyncMemoryCache where TResult : class + { + private static readonly Task EmptyValueTask = Task.FromResult(null); + private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); + private readonly MemoryCache _cache = new MemoryCache(Guid.NewGuid().ToString()); + private readonly LogSource _log = new LogSource(); + + /// + /// Set the timespan for items to expire. + /// + public TimeSpan? ExpireTimeSpan { get; set; } + + /// + /// Set the timespan for items to slide. + /// + public TimeSpan? SlidingTimeSpan { get; set; } + + /// + /// Specifies if the RemovedCallback needs to be called + /// If this is active, ActivateUpdateCallback should be false + /// + protected bool ActivateRemovedCallback { get; set; } = true; + + /// + /// Specifies if the UpdateCallback needs to be called. + /// If this is active, ActivateRemovedCallback should be false + /// + protected bool ActivateUpdateCallback { get; set; } = false; + + /// + /// Implement this method, it should create an instance of TResult via the supplied TKey. + /// + /// TKey + /// CancellationToken + /// TResult + protected abstract Task CreateAsync(TKey key, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Creates a key under which the object is stored or retrieved, default is a toString on the object. + /// + /// TKey + /// string + protected virtual string CreateKey(TKey keyObject) + { + return keyObject.ToString(); + } + + /// + /// Get an element from the cache, if this is not available call the create function. + /// + /// object for the key + /// CancellationToken + /// TResult + public async Task DeleteAsync(TKey keyObject, CancellationToken cancellationToken = default(CancellationToken)) + { + var key = CreateKey(keyObject); + await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + _cache.Remove(key); + } + finally + { + _semaphoreSlim.Release(); + } + } + + /// + /// Get a task element from the cache, if this is not available return null. + /// You probably want to call GetOrCreateAsync + /// + /// object for the key + /// Task with TResult, null if no value + public Task GetAsync(TKey keyObject) + { + var key = CreateKey(keyObject); + return _cache.Get(key) as Task ?? EmptyValueTask; + } + + /// + /// Get a task element from the cache, if this is not available call the create function. + /// + /// object for the key + /// CancellationToken + /// Task with TResult + public Task GetOrCreateAsync(TKey keyObject, CancellationToken cancellationToken = default(CancellationToken)) + { + var key = CreateKey(keyObject); + return _cache.Get(key) as Task ?? GetOrCreateInternalAsync(keyObject, null, cancellationToken); + } + + /// + /// Get a task element from the cache, if this is not available call the create function. + /// + /// object for the key + /// CacheItemPolicy for when you want more control over the item + /// CancellationToken + /// Task with TResult + public Task GetOrCreateAsync(TKey keyObject, CacheItemPolicy cacheItemPolicy, CancellationToken cancellationToken = default(CancellationToken)) + { + var key = CreateKey(keyObject); + return _cache.Get(key) as Task ?? GetOrCreateInternalAsync(keyObject, cacheItemPolicy, cancellationToken); + } + + /// + /// This takes care of the real async part of the code. + /// + /// + /// CacheItemPolicy for when you want more control over the item + /// CancellationToken + /// TResult + private async Task GetOrCreateInternalAsync(TKey keyObject, CacheItemPolicy cacheItemPolicy = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var key = CreateKey(keyObject); + var completionSource = new TaskCompletionSource(); + + if (cacheItemPolicy == null) + { + cacheItemPolicy = new CacheItemPolicy + { + AbsoluteExpiration = ExpireTimeSpan.HasValue ? DateTimeOffset.Now.Add(ExpireTimeSpan.Value) : ObjectCache.InfiniteAbsoluteExpiration, + SlidingExpiration = SlidingTimeSpan ?? ObjectCache.NoSlidingExpiration + }; + if (ActivateUpdateCallback) + { + cacheItemPolicy.UpdateCallback = UpdateCallback; + } + if (ActivateRemovedCallback) + { + cacheItemPolicy.RemovedCallback = RemovedCallback; + } + } + + var result = _cache.AddOrGetExisting(key, completionSource.Task, cacheItemPolicy) as Task; + // Test if we got an existing object or our own + if (result != null && !completionSource.Task.Equals(result)) + { + return await result.ConfigureAwait(false); + } + + await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + result = _cache.AddOrGetExisting(key, completionSource.Task, cacheItemPolicy) as Task; + if (result != null && !completionSource.Task.Equals(result)) + { + return await result.ConfigureAwait(false); + } + + // Now, start the background task, which will set the completionSource with the correct response + // ReSharper disable once MethodSupportsCancellation + // ReSharper disable once UnusedVariable + var ignoreBackgroundTask = Task.Run(async () => + { + try + { + var backgroundResult = await CreateAsync(keyObject, cancellationToken).ConfigureAwait(false); + completionSource.TrySetResult(backgroundResult); + } + catch (TaskCanceledException) + { + completionSource.TrySetCanceled(); + } + catch (Exception ex) + { + completionSource.TrySetException(ex); + } + }); + } + finally + { + _semaphoreSlim.Release(); + } + + return await completionSource.Task.ConfigureAwait(false); + } + + /// + /// Override to know when an item is removed, make sure to configure ActivateUpdateCallback / ActivateRemovedCallback + /// + /// CacheEntryRemovedArguments + protected virtual void RemovedCallback(CacheEntryRemovedArguments cacheEntryRemovedArguments) + { + _log.Verbose().WriteLine("Item {0} removed due to {1}.", cacheEntryRemovedArguments.CacheItem.Key, cacheEntryRemovedArguments.RemovedReason); + var disposable = cacheEntryRemovedArguments.CacheItem.Value as IDisposable; + if (disposable != null) + { + _log.Debug().WriteLine("Disposed cached item."); + disposable.Dispose(); + } + } + + /// + /// Override to modify the cache behaviour when an item is about to be removed, make sure to configure + /// ActivateUpdateCallback / ActivateRemovedCallback + /// + /// CacheEntryUpdateArguments + protected virtual void UpdateCallback(CacheEntryUpdateArguments cacheEntryUpdateArguments) + { + _log.Verbose().WriteLine("Update request for {0} due to {1}.", cacheEntryUpdateArguments.Key, cacheEntryUpdateArguments.RemovedReason); + } + } +} diff --git a/GreenshotJiraPlugin/Forms/JiraForm.cs b/GreenshotJiraPlugin/Forms/JiraForm.cs index b4c99e49b..19c8f3edd 100644 --- a/GreenshotJiraPlugin/Forms/JiraForm.cs +++ b/GreenshotJiraPlugin/Forms/JiraForm.cs @@ -118,13 +118,9 @@ namespace GreenshotJiraPlugin.Forms { public async Task UploadAsync(IBinaryContainer attachment) { _config.LastUsedJira = _selectedIssue.Key; - using (var memoryStream = new MemoryStream()) - { - attachment.WriteToStream(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - await _jiraConnector.AttachAsync(_selectedIssue.Key, memoryStream, jiraFilenameBox.Text, attachment.ContentType); - } - + attachment.Filename = jiraFilenameBox.Text; + await _jiraConnector.AttachAsync(_selectedIssue.Key, attachment); + if (!string.IsNullOrEmpty(jiraCommentBox.Text)) { await _jiraConnector.AddCommentAsync(_selectedIssue.Key, jiraCommentBox.Text); } @@ -141,8 +137,7 @@ namespace GreenshotJiraPlugin.Forms { IList issues = null; try { - var searchResult = await _jiraConnector.SearchAsync(filter.Jql, fields: new [] { "summary", "reporter", "assignee", "created" }); - issues = searchResult.Issues; + issues = await _jiraConnector.SearchAsync(filter); } catch (Exception ex) { diff --git a/GreenshotJiraPlugin/GreenshotJiraPlugin.csproj b/GreenshotJiraPlugin/GreenshotJiraPlugin.csproj index 78ffaad6b..10f91ea8e 100644 --- a/GreenshotJiraPlugin/GreenshotJiraPlugin.csproj +++ b/GreenshotJiraPlugin/GreenshotJiraPlugin.csproj @@ -38,8 +38,8 @@ ..\packages\Dapplo.HttpExtensions.0.5.31\lib\net45\Dapplo.HttpExtensions.dll True - - ..\packages\Dapplo.Jira.0.1.57\lib\net45\Dapplo.Jira.dll + + ..\packages\Dapplo.Jira.0.1.58\lib\net45\Dapplo.Jira.dll True @@ -51,11 +51,13 @@ + + Form @@ -71,6 +73,7 @@ SettingsForm.cs + diff --git a/GreenshotJiraPlugin/IssueTypeBitmapCache.cs b/GreenshotJiraPlugin/IssueTypeBitmapCache.cs new file mode 100644 index 000000000..019468944 --- /dev/null +++ b/GreenshotJiraPlugin/IssueTypeBitmapCache.cs @@ -0,0 +1,50 @@ +/* + * Greenshot - a free and open source screenshot tool + * Copyright (C) 2007-2016 Thomas Braun, Jens Klingen, Robin Krom + * + * For more information see: http://getgreenshot.org/ + * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Drawing; +using System.Threading; +using System.Threading.Tasks; +using Dapplo.Jira; +using Dapplo.Jira.Entities; + +namespace GreenshotJiraPlugin +{ + /// + /// This is the bach for the IssueType bitmaps + /// + public class IssueTypeBitmapCache : AsyncMemoryCache + { + private readonly JiraApi _jiraApi; + + public IssueTypeBitmapCache(JiraApi jiraApi) + { + _jiraApi = jiraApi; + // Set the expire timeout to an hour + ExpireTimeSpan = TimeSpan.FromHours(1); + } + + protected override async Task CreateAsync(Issue issue, CancellationToken cancellationToken = new CancellationToken()) + { + return await _jiraApi.GetUriContentAsync(issue.Fields.IssueType.IconUri, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/GreenshotJiraPlugin/JiraConnector.cs b/GreenshotJiraPlugin/JiraConnector.cs index ba772e735..197d65996 100644 --- a/GreenshotJiraPlugin/JiraConnector.cs +++ b/GreenshotJiraPlugin/JiraConnector.cs @@ -22,6 +22,8 @@ using System; using System.Collections.Generic; +using System.Drawing; +using System.IO; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; @@ -31,15 +33,21 @@ using Greenshot.IniFile; using GreenshotPlugin.Core; namespace GreenshotJiraPlugin { + /// + /// This encapsulates the JiraApi to make it possible to change as less old Greenshot code as needed + /// public class JiraConnector : IDisposable { private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(JiraConnector)); private static readonly JiraConfiguration Config = IniConfig.GetIniSection(); + // Used to remove the wsdl information from the old SOAP Uri public const string DefaultPostfix = "/rpc/soap/jirasoapservice-v2?wsdl"; - private DateTime _loggedInTime = DateTime.Now; + private DateTimeOffset _loggedInTime = DateTimeOffset.MinValue; private bool _loggedIn; private readonly int _timeout; + private readonly object _lock = new object(); private string _url; private JiraApi _jiraApi; + private IssueTypeBitmapCache _issueTypeBitmapCache; public void Dispose() { if (_jiraApi != null) @@ -51,7 +59,6 @@ namespace GreenshotJiraPlugin { public JiraConnector() { _url = Config.Url.Replace(DefaultPostfix, ""); _timeout = Config.Timeout; - _jiraApi = new JiraApi(new Uri(_url)); } /// @@ -60,11 +67,18 @@ namespace GreenshotJiraPlugin { /// true if login was done sucessfully private async Task DoLogin(string user, string password) { - if (_url.EndsWith("wsdl")) + lock (_lock) { - _url = _url.Replace(DefaultPostfix, ""); - // recreate the service with the new url - _jiraApi = new JiraApi(new Uri(_url)); + if (_url.EndsWith("wsdl")) + { + _url = _url.Replace(DefaultPostfix, ""); + } + if (_jiraApi == null) + { + // recreate the service with the new url + _jiraApi = new JiraApi(new Uri(_url)); + _issueTypeBitmapCache = new IssueTypeBitmapCache(_jiraApi); + } } LoginInfo loginInfo; @@ -118,14 +132,22 @@ namespace GreenshotJiraPlugin { } + /// + /// End the session, if there was one + /// public async Task Logout() { - if (_jiraApi != null) + if (_jiraApi != null && _loggedIn) { await _jiraApi.EndSessionAsync(); _loggedIn = false; } } + /// + /// check the login credentials, to prevent timeouts of the session, or makes a login + /// Do not use ConfigureAwait to call this, as it will move await from the UI thread. + /// + /// private async Task CheckCredentials() { if (_loggedIn) { if (_loggedInTime.AddMinutes(_timeout-1).CompareTo(DateTime.Now) < 0) { @@ -137,32 +159,81 @@ namespace GreenshotJiraPlugin { } } + /// + /// Get the favourite filters + /// + /// List with filters public async Task> GetFavoriteFiltersAsync() { await CheckCredentials(); return await _jiraApi.GetFavoriteFiltersAsync().ConfigureAwait(false); } + /// + /// Get the issue for a key + /// + /// Jira issue key + /// Issue public async Task GetIssueAsync(string issueKey) { await CheckCredentials(); return await _jiraApi.GetIssueAsync(issueKey).ConfigureAwait(false); } - public async Task AttachAsync(string issueKey, TContent content, string filename, string contentType = null, CancellationToken cancellationToken = default(CancellationToken)) where TContent : class + + /// + /// Attach the content to the jira + /// + /// + /// IBinaryContainer + /// + /// + /// + public async Task AttachAsync(string issueKey, IBinaryContainer content, CancellationToken cancellationToken = default(CancellationToken)) { - await CheckCredentials().ConfigureAwait(false); - return await _jiraApi.AttachAsync(issueKey, content, filename, contentType, cancellationToken).ConfigureAwait(false); + await CheckCredentials(); + using (var memoryStream = new MemoryStream()) + { + content.WriteToStream(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + await _jiraApi.AttachAsync(issueKey, memoryStream, content.Filename, content.ContentType, cancellationToken).ConfigureAwait(false); + } } + /// + /// Add a comment to the supplied issue + /// + /// Jira issue key + /// text + /// the visibility role + /// CancellationToken public async Task AddCommentAsync(string issueKey, string body, string visibility = null, CancellationToken cancellationToken = default(CancellationToken)) { await CheckCredentials(); await _jiraApi.AddCommentAsync(issueKey, body, visibility, cancellationToken).ConfigureAwait(false); } - public async Task SearchAsync(string jql, int maxResults = 20, IList fields = null, CancellationToken cancellationToken = default(CancellationToken)) + + /// + /// Get the search results for the specified filter + /// + /// Filter + /// + /// + public async Task> SearchAsync(Filter filter, CancellationToken cancellationToken = default(CancellationToken)) { await CheckCredentials(); - return await _jiraApi.SearchAsync(jql, maxResults, fields, cancellationToken).ConfigureAwait(false); + var searchResult = await _jiraApi.SearchAsync(filter.Jql, 20, new[] { "summary", "reporter", "assignee", "created" }, cancellationToken).ConfigureAwait(false); + return searchResult.Issues; + } + + /// + /// Get the bitmap representing the issue type of an issue, from cache. + /// + /// Issue + /// CancellationToken + /// Bitmap + public async Task GetIssueTypeBitmapAsync(Issue issue, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _issueTypeBitmapCache.GetOrCreateAsync(issue, cancellationToken).ConfigureAwait(false); } public Uri JiraBaseUri => _jiraApi.JiraBaseUri; diff --git a/GreenshotJiraPlugin/JiraDestination.cs b/GreenshotJiraPlugin/JiraDestination.cs index 6a30f4fd7..6d472535a 100644 --- a/GreenshotJiraPlugin/JiraDestination.cs +++ b/GreenshotJiraPlugin/JiraDestination.cs @@ -41,30 +41,27 @@ namespace GreenshotJiraPlugin { private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(JiraDestination)); private static readonly JiraConfiguration Config = IniConfig.GetIniSection(); private readonly JiraPlugin _jiraPlugin; - private readonly Issue _jira; + private readonly Issue _jiraIssue; public JiraDestination(JiraPlugin jiraPlugin) { _jiraPlugin = jiraPlugin; } - public JiraDestination(JiraPlugin jiraPlugin, Issue jira) { + public JiraDestination(JiraPlugin jiraPlugin, Issue jiraIssue) { _jiraPlugin = jiraPlugin; - _jira = jira; + _jiraIssue = jiraIssue; } public override string Designation => "Jira"; - private string FormatUpload(Issue jira) { - return Designation + " - " + jira.Key + ": " + jira.Fields.Summary.Substring(0, Math.Min(20, jira.Fields.Summary.Length)); - } - public override string Description { get { - if (_jira == null) { + if (_jiraIssue?.Fields?.Summary == null) { return Language.GetString("jira", LangKey.upload_menu_item); } - return FormatUpload(_jira); + // Format the title of this destination + return Designation + " - " + _jiraIssue.Key + ": " + _jiraIssue.Fields.Summary.Substring(0, Math.Min(20, _jiraIssue.Fields.Summary.Length)); } } @@ -73,9 +70,19 @@ namespace GreenshotJiraPlugin { public override bool isDynamic => true; public override Image DisplayIcon { - get { - var resources = new ComponentResourceManager(typeof(JiraPlugin)); - return (Image)resources.GetObject("Jira"); + get + { + Image displayIcon = null; + if (_jiraIssue != null && JiraPlugin.Instance.CurrentJiraConnector != null) + { + displayIcon = JiraPlugin.Instance.CurrentJiraConnector.GetIssueTypeBitmapAsync(_jiraIssue).Result; + } + if (displayIcon == null) + { + var resources = new ComponentResourceManager(typeof(JiraPlugin)); + displayIcon = (Image)resources.GetObject("Jira"); + } + return displayIcon; } } @@ -95,23 +102,18 @@ namespace GreenshotJiraPlugin { ExportInformation exportInformation = new ExportInformation(Designation, Description); string filename = Path.GetFileName(FilenameHelper.GetFilename(Config.UploadFormat, captureDetails)); SurfaceOutputSettings outputSettings = new SurfaceOutputSettings(Config.UploadFormat, Config.UploadJpegQuality, Config.UploadReduceColors); - if (_jira != null) { + if (_jiraIssue != null) { try { // Run upload in the background new PleaseWaitForm().ShowAndWait(Description, Language.GetString("jira", LangKey.communication_wait), async () => { var surfaceContainer = new SurfaceContainer(surfaceToUpload, outputSettings, filename); - using (var memoryStream = new MemoryStream()) - { - surfaceContainer.WriteToStream(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - await _jiraPlugin.JiraConnector.AttachAsync(_jira.Key, memoryStream, filename, surfaceContainer.ContentType); - } - surfaceToUpload.UploadURL = _jiraPlugin.JiraConnector.JiraBaseUri.AppendSegments("browse", _jira.Key).AbsoluteUri; + await _jiraPlugin.JiraConnector.AttachAsync(_jiraIssue.Key, surfaceContainer); + surfaceToUpload.UploadURL = _jiraPlugin.JiraConnector.JiraBaseUri.AppendSegments("browse", _jiraIssue.Key).AbsoluteUri; } ); - Log.DebugFormat("Uploaded to Jira {0}", _jira.Key); + Log.DebugFormat("Uploaded to Jira {0}", _jiraIssue.Key); exportInformation.ExportMade = true; exportInformation.Uri = surfaceToUpload.UploadURL; } catch (Exception e) { diff --git a/GreenshotJiraPlugin/packages.config b/GreenshotJiraPlugin/packages.config index 16bfb9d03..2ac586eeb 100644 --- a/GreenshotJiraPlugin/packages.config +++ b/GreenshotJiraPlugin/packages.config @@ -1,7 +1,7 @@  - + \ No newline at end of file diff --git a/GreenshotPlugin/Core/NetworkHelper.cs b/GreenshotPlugin/Core/NetworkHelper.cs index 776fb9f5d..e1d90c7fc 100644 --- a/GreenshotPlugin/Core/NetworkHelper.cs +++ b/GreenshotPlugin/Core/NetworkHelper.cs @@ -557,6 +557,7 @@ namespace GreenshotPlugin.Core { void Upload(HttpWebRequest webRequest); string ContentType { get; } + string Filename { get; set; } } /// @@ -564,7 +565,6 @@ namespace GreenshotPlugin.Core { /// public class ByteContainer : IBinaryContainer { private readonly byte[] _file; - private readonly string _fileName; private readonly string _contentType; private readonly int _fileSize; public ByteContainer(byte[] file) : this(file, null) { @@ -575,7 +575,7 @@ namespace GreenshotPlugin.Core { } public ByteContainer(byte[] file, string filename, string contenttype, int filesize) { _file = file; - _fileName = filename; + Filename = filename; _contentType = contenttype; _fileSize = filesize == 0 ? file.Length : filesize; } @@ -607,7 +607,7 @@ namespace GreenshotPlugin.Core { string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n", boundary, name, - _fileName ?? name, + Filename ?? name, _contentType ?? "application/octet-stream"); formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, Encoding.UTF8.GetByteCount(header)); @@ -638,6 +638,7 @@ namespace GreenshotPlugin.Core { } public string ContentType => _contentType; + public string Filename { get; set; } } /// @@ -646,12 +647,11 @@ namespace GreenshotPlugin.Core { public class BitmapContainer : IBinaryContainer { private readonly Bitmap _bitmap; private readonly SurfaceOutputSettings _outputSettings; - private readonly string _fileName; public BitmapContainer(Bitmap bitmap, SurfaceOutputSettings outputSettings, string filename) { _bitmap = bitmap; _outputSettings = outputSettings; - _fileName = filename; + Filename = filename; } /// @@ -689,7 +689,7 @@ namespace GreenshotPlugin.Core { string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n", boundary, name, - _fileName ?? name, + Filename ?? name, ContentType); formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, Encoding.UTF8.GetByteCount(header)); @@ -717,6 +717,8 @@ namespace GreenshotPlugin.Core { } public string ContentType => "image/" + _outputSettings.Format; + + public string Filename { get; set; } } /// @@ -725,12 +727,11 @@ namespace GreenshotPlugin.Core { public class SurfaceContainer : IBinaryContainer { private readonly ISurface _surface; private readonly SurfaceOutputSettings _outputSettings; - private readonly string _fileName; public SurfaceContainer(ISurface surface, SurfaceOutputSettings outputSettings, string filename) { _surface = surface; _outputSettings = outputSettings; - _fileName = filename; + Filename = filename; } /// @@ -768,7 +769,7 @@ namespace GreenshotPlugin.Core { string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n", boundary, name, - _fileName ?? name, + Filename ?? name, ContentType); formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, Encoding.UTF8.GetByteCount(header)); @@ -796,5 +797,6 @@ namespace GreenshotPlugin.Core { } public string ContentType => "image/" + _outputSettings.Format; + public string Filename { get; set; } } }