diff --git a/.github/workflows/UploadFile_build_and_test_on_main.yml b/.github/workflows/UploadFile_build_and_test_on_main.yml
new file mode 100644
index 0000000..3e5b6c8
--- /dev/null
+++ b/.github/workflows/UploadFile_build_and_test_on_main.yml
@@ -0,0 +1,17 @@
+name: UploadFile build_main
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - "Frends.HTTP.UploadFile/**"
+ workflow_dispatch:
+
+jobs:
+ build:
+ uses: FrendsPlatform/FrendsTasks/.github/workflows/build_main.yml@main
+ with:
+ workdir: Frends.HTTP.UploadFile
+ secrets:
+ badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }}
diff --git a/.github/workflows/UploadFile_build_and_test_on_push.yml b/.github/workflows/UploadFile_build_and_test_on_push.yml
new file mode 100644
index 0000000..0e4cbf3
--- /dev/null
+++ b/.github/workflows/UploadFile_build_and_test_on_push.yml
@@ -0,0 +1,18 @@
+name: UploadFile push
+
+on:
+ push:
+ branches-ignore:
+ - main
+ paths:
+ - "Frends.HTTP.UploadFile/**"
+ workflow_dispatch:
+
+jobs:
+ build:
+ uses: FrendsPlatform/FrendsTasks/.github/workflows/build_test.yml@main
+ with:
+ workdir: Frends.HTTP.UploadFile
+ secrets:
+ badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }}
+ test_feed_api_key: ${{ secrets.TASKS_TEST_FEED_API_KEY }}
diff --git a/.github/workflows/UploadFile_release.yml b/.github/workflows/UploadFile_release.yml
new file mode 100644
index 0000000..2efb48b
--- /dev/null
+++ b/.github/workflows/UploadFile_release.yml
@@ -0,0 +1,12 @@
+name: UploadFile release
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build:
+ uses: FrendsPlatform/FrendsTasks/.github/workflows/release.yml@main
+ with:
+ workdir: Frends.HTTP.UploadFile
+ secrets:
+ feed_api_key: ${{ secrets.TASKS_FEED_API_KEY }}
diff --git a/Frends.HTTP.UploadFile/CHANGELOG.md b/Frends.HTTP.UploadFile/CHANGELOG.md
new file mode 100644
index 0000000..17139d1
--- /dev/null
+++ b/Frends.HTTP.UploadFile/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## [1.0.0] - 2025-01-09
+### Added
+- Initial implementation.
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/CertificateHandler.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/CertificateHandler.cs
new file mode 100644
index 0000000..d68f711
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/CertificateHandler.cs
@@ -0,0 +1,57 @@
+using Pluralsight.Crypto;
+using System;
+using System.IO;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Frends.HTTP.UploadFile.Tests;
+
+public static class CertificateHandler
+{
+ public static string Handle(string path, string password, bool cleanUp, string thumbPrint)
+ {
+ try
+ {
+ if (!cleanUp)
+ {
+ using CryptContext ctx = new();
+ ctx.Open();
+
+ X509Certificate2 cert = ctx.CreateSelfSignedCertificate(
+ new SelfSignedCertProperties
+ {
+ IsPrivateKeyExportable = true,
+ KeyBitLength = 4096,
+ Name = new X500DistinguishedName("cn=localhost"),
+ ValidFrom = DateTime.Today.AddDays(-1),
+ ValidTo = DateTime.Today.AddMinutes(1),
+ });
+
+ byte[] certData = cert.Export(X509ContentType.Pfx, password);
+
+ using (X509Store store = new(StoreName.My, StoreLocation.CurrentUser))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(cert);
+ }
+
+ File.WriteAllBytes(path, certData);
+ return cert.Thumbprint;
+ }
+ else
+ {
+ using X509Store store = new(StoreName.My, StoreLocation.CurrentUser);
+ store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived);
+ X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false);
+
+ foreach (var cert in col)
+ store.Remove(cert);
+
+ return null;
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new Exception(ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj
new file mode 100644
index 0000000..b7d0f03
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net6.0
+
+ false
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs
new file mode 100644
index 0000000..61bb65f
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs
@@ -0,0 +1,188 @@
+using Frends.HTTP.UploadFile.Definitions;
+using HttpMock;
+using HttpMock.Verify.NUnit;
+using NUnit.Framework;
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Frends.HTTP.UploadFile.Tests;
+
+[TestFixture]
+public class IntegrationTest
+{
+ private IHttpServer _stubHttp;
+ private static readonly string testFilePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt"));
+ private static readonly string testCodePageName = "iso-8859-1";
+ private static readonly string expectedContentType = $"text/plain; charset={testCodePageName}";
+ private static readonly Header contentTypeHeader = new() { Name = "cONtENT-tYpE", Value = expectedContentType };
+
+ private static readonly Input defaultInput = new()
+ {
+ Method = Method.POST,
+ Url = "http://localhost:9191/endpoint",
+ Headers = new Header[1] { contentTypeHeader },
+ FilePath = testFilePath
+ };
+
+
+ [SetUp]
+ public void Setup()
+ {
+ _stubHttp = HttpMockRepository.At("http://localhost:9191");
+ _stubHttp.Stub(x => x.Post("/endpoint"))
+ .AsContentType($"text/plain; charset={testCodePageName}")
+ .Return("foo ���")
+ .OK();
+ _stubHttp.Stub(x => x.Post("/wrong-endpoint"))
+ .AsContentType($"text/plain")
+ .Return("error occured")
+ .NotFound();
+ }
+
+ [Test]
+ public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase()
+ {
+ var utf8ByteArray = File.ReadAllBytes(testFilePath);
+
+ var result = await UploadFileTask.UploadFile(defaultInput, new Options(), CancellationToken.None);
+
+ var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
+ var requestHead = request.RequestHead;
+ var requestBodyByteArray = Encoding.GetEncoding(testCodePageName).GetBytes(request.Body);
+ var requestContentType = requestHead.Headers["cONTENT-tYpE"];
+
+ Assert.That(requestContentType, Is.EqualTo(expectedContentType));
+ Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray));
+ }
+
+ [Test]
+ public void ThrowOnErrorResponse()
+ {
+ var input = defaultInput;
+ input.Url = "http://localhost:9191/wrong-endpoint";
+ var options = new Options
+ {
+ ThrowExceptionOnErrorResponse = true
+ };
+
+ Assert.ThrowsAsync(() => UploadFileTask.UploadFile(input, options, CancellationToken.None));
+ }
+
+ [Test]
+ public async Task AddBasicAuthHeader()
+ {
+ var options = new Options
+ {
+ Authentication = Authentication.Basic,
+ Username = "foo",
+ Password = "bar",
+ };
+ var encodedValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"));
+
+ var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);
+ var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
+ Assert.That(request.RequestHead.Headers["Authorization"], Is.EqualTo($"Basic {encodedValue}"));
+ }
+
+ [Test]
+ public async Task AddOAuthHeader()
+ {
+ var options = new Options
+ {
+ Authentication = Authentication.OAuth,
+ Token = "foobar"
+ };
+
+ var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);
+
+ var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
+ Assert.That(request.RequestHead.Headers["Authorization"], Is.EqualTo($"Bearer foobar"));
+ }
+
+ [Test]
+ public async Task AddInvalidHeader()
+ {
+ Header invalidHeader = new() { Name = string.Empty, Value = "bar" };
+ var input = defaultInput;
+ input.Headers = new Header[2] { contentTypeHeader, invalidHeader };
+
+ var result = await UploadFileTask.UploadFile(input, new Options(), CancellationToken.None);
+ var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
+ var isInvalidAdded = request.RequestHead.Headers.Any(x => x.Key == string.Empty);
+ Assert.IsFalse(isInvalidAdded);
+ }
+
+ [Test]
+ public async Task AddWindowsIntegratedSecurity()
+ {
+ var options = new Options
+ {
+ Authentication = Authentication.WindowsIntegratedSecurity
+ };
+
+ var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);
+
+ var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
+ }
+
+ [Test]
+ public async Task AddWindowsAuthentication()
+ {
+ var options = new Options
+ {
+ Authentication = Authentication.WindowsAuthentication,
+ Username = "user\\domain"
+ };
+
+ var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);
+
+ var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
+ }
+
+ [Test]
+ public void ThrowWhenAddWindowsAuthenticationWithInvalidUsername()
+ {
+ var options = new Options
+ {
+ Authentication = Authentication.WindowsAuthentication,
+ Username = "userWithoutDomain"
+ };
+
+ Assert.ThrowsAsync(() => UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None));
+ }
+
+ [Test]
+ public async Task AddClientCertiface()
+ {
+ string certPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "certwithpk.pfx"));
+ string certPassword = "password";
+ var thumprint = CertificateHandler.Handle(certPath, certPassword, false, null);
+
+ var options = new Options
+ {
+ Authentication = Authentication.ClientCertificate,
+ CertificateThumbprint = thumprint
+ };
+
+ var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);
+
+ var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
+ }
+
+ [Test]
+ public void ThrowWhenInvalidClientCertificate()
+ {
+ var options = new Options
+ {
+ Authentication = Authentication.ClientCertificate,
+ CertificateThumbprint = "thumbprint"
+ };
+
+ Assert.ThrowsAsync(() => UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None));
+ }
+}
\ No newline at end of file
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt
new file mode 100644
index 0000000..863df82
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt
@@ -0,0 +1,2 @@
+Lorem lipsum
+Sumpil merol
\ No newline at end of file
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln
new file mode 100644
index 0000000..8cbfa93
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33213.308
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile", "Frends.HTTP.UploadFile\Frends.HTTP.UploadFile.csproj", "{35C305C0-8108-4A98-BB1D-AFE5C926239E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile.Tests", "Frends.HTTP.UploadFile.Tests\Frends.HTTP.UploadFile.Tests.csproj", "{8CA92187-8E4F-4414-803B-EC899479022E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{500C38D9-EBDF-49FE-ACFB-D22A5191B8BB}"
+ ProjectSection(SolutionItems) = preProject
+ CHANGELOG.md = CHANGELOG.md
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {55BC6629-85C9-48D8-8CA2-B0046AF1AF4B}
+ EndGlobalSection
+EndGlobal
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs
new file mode 100644
index 0000000..0999dc4
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs
@@ -0,0 +1,37 @@
+namespace Frends.HTTP.UploadFile.Definitions;
+
+///
+/// Represents the authentication method to be used with the request.
+///
+public enum Authentication
+{
+ ///
+ /// No authentication is used.
+ ///
+ None,
+
+ ///
+ /// Basic authentication is used, where the username and password are sent in plain text.
+ ///
+ Basic,
+
+ ///
+ /// Windows authentication is used, where the user's Windows login credentials are used to authenticate the request.
+ ///
+ WindowsAuthentication,
+
+ ///
+ /// Windows integrated security is used, where the current Windows user's credentials are used to authenticate the request.
+ ///
+ WindowsIntegratedSecurity,
+
+ ///
+ /// OAuth token-based authentication is used, where a token is obtained from an authentication server and used to authenticate the request.
+ ///
+ OAuth,
+
+ ///
+ /// Client certificate-based authentication is used, where a client certificate is used to authenticate the request.
+ ///
+ ClientCertificate
+}
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs
new file mode 100644
index 0000000..513a54d
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs
@@ -0,0 +1,19 @@
+namespace Frends.HTTP.UploadFile.Definitions;
+///
+/// Represents an HTTP header, which consists of a name-value pair.
+///
+public class Header
+{
+ ///
+ /// The name of the header.
+ ///
+ /// Example Name
+ public string Name { get; set; }
+
+ ///
+ /// The value of the header.
+ ///
+ /// Example Value
+ public string Value { get; set; }
+
+}
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs
new file mode 100644
index 0000000..d39db1e
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs
@@ -0,0 +1,37 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel;
+
+namespace Frends.HTTP.UploadFile.Definitions;
+
+///
+/// Represents the input data for the HTTP file upload operation.
+///
+public class Input
+{
+ ///
+ /// The HTTP Method to be used with the request.
+ ///
+ /// GET
+ public Method Method { get; set; }
+
+ ///
+ /// The URL with protocol and path. You can include query parameters directly in the url.
+ ///
+ /// https://example.org/path/to
+ [DefaultValue("https://example.org/path/to")]
+ [DisplayFormat(DataFormatString = "Text")]
+ public string Url { get; set; }
+
+ ///
+ /// The file path to be posted
+ ///
+ /// C:\Users
+ public string FilePath { get; set; }
+
+ ///
+ /// List of HTTP headers to be added to the request.
+ ///
+ /// Name: Header, Value: HeaderValue
+ public Header[] Headers { get; set; }
+}
+
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs
new file mode 100644
index 0000000..b723e56
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs
@@ -0,0 +1,17 @@
+namespace Frends.HTTP.UploadFile.Definitions;
+
+
+///
+/// Represents the HTTP method to be used with the request.
+///
+public enum Method
+{
+ ///
+ /// The HTTP POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server.
+ ///
+ POST,
+ ///
+ /// The HTTP PUT method is used to replace or update a current resource with new content.
+ ///
+ PUT
+}
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs
new file mode 100644
index 0000000..1251ae4
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs
@@ -0,0 +1,78 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel;
+
+namespace Frends.HTTP.UploadFile.Definitions;
+
+///
+/// Options for the HTTP request.
+///
+public class Options
+{
+ ///
+ /// Method of authenticating request
+ ///
+ /// Basic
+ public Authentication Authentication { get; set; }
+
+ ///
+ /// If WindowsAuthentication is selected you should use domain\username
+ ///
+ /// Domain\User
+ [UIHint(nameof(Definitions.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)]
+ public string Username { get; set; }
+
+ ///
+ /// Password for the user.
+ ///
+ /// Example Password
+ [PasswordPropertyText]
+ [UIHint(nameof(Definitions.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)]
+ public string Password { get; set; }
+
+ ///
+ /// Bearer token to be used for request. Token will be added as Authorization header.
+ ///
+ /// exampleOAuthToken
+ [PasswordPropertyText]
+ [UIHint(nameof(Definitions.Authentication), "", Authentication.OAuth)]
+ public string Token { get; set; }
+
+ ///
+ /// Thumbprint for using client certificate authentication.
+ ///
+ /// exampleCertificateThumbprint
+ [UIHint(nameof(Definitions.Authentication), "", Authentication.ClientCertificate)]
+ public string CertificateThumbprint { get; set; }
+
+ ///
+ /// Timeout in seconds to be used for the connection and operation.
+ ///
+ /// 30
+ [DefaultValue(30)]
+ public int ConnectionTimeoutSeconds { get; set; }
+
+ ///
+ /// If FollowRedirects is set to false, all responses with an HTTP status code from 300 to 399 is returned to the application.
+ ///
+ /// true
+ [DefaultValue(true)]
+ public bool FollowRedirects { get; set; }
+
+ ///
+ /// Do not throw an exception on certificate error.
+ ///
+ /// true
+ public bool AllowInvalidCertificate { get; set; }
+
+ ///
+ /// Some API's return faulty content-type charset header. This setting overrides the returned charset.
+ ///
+ /// false
+ public bool AllowInvalidResponseContentTypeCharSet { get; set; }
+
+ ///
+ /// Throw exception if return code of request is not successfull
+ ///
+ /// true
+ public bool ThrowExceptionOnErrorResponse { get; set; }
+}
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs
new file mode 100644
index 0000000..bb38da6
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+
+namespace Frends.HTTP.UploadFile.Definitions;
+
+///
+/// Represents the response received from the HTTP server after sending a request.
+///
+public class Response
+{
+ ///
+ /// The body of the response.
+ ///
+ ///
+ ///
+ ///
+ public string Body { get; set; }
+
+ ///
+ /// The headers of the response, as a dictionary of key-value pairs.
+ ///
+ ///
+ ///
+ ///
+ public Dictionary Headers { get; set; }
+
+ ///
+ /// The status code of the response, as an integer value.
+ ///
+ /// 200
+ public int StatusCode { get; set; }
+}
\ No newline at end of file
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs
new file mode 100644
index 0000000..b1221db
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Frends.HTTP.UploadFile.Definitions;
+
+namespace Frends.HTTP.UploadFile;
+
+///
+/// Represents a task that posts a file to a web API endpoint.
+///
+public static class UploadFileTask
+{
+ ///
+ /// Send file using StreamContent, the file data is read from a Stream and sent as the content of an HTTP request.
+ /// StreamContent is a class provided by the .NET framework that allows you to send content from a Stream in an HTTP request
+ ///
+ /// The input parameters specifying the file to be sent.
+ /// The optional parameters controlling the file upload behavior.
+ /// The cancellation token that can be used to cancel the upload operation.
+ /// Object { string Body, Dictionary[string, string] Headers, int StatusCode }
+ public static async Task UploadFile([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken)
+ {
+ using var handler = new HttpClientHandler();
+ handler.SetHandleSettingsBasedOnOptions(options);
+
+ using var httpClient = new HttpClient(handler);
+ var responseMessage = await GetHttpRequestResponseAsync(httpClient, input, options, cancellationToken).ConfigureAwait(false);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ string body = await responseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ HttpHeaders contentHeaders = responseMessage.Content.Headers;
+ HttpHeaders headers = responseMessage.Headers;
+
+ var responseHeaders = CombineHeaders(new HttpHeaders[] { headers, contentHeaders });
+
+ var response = new Response
+ {
+ Body = body,
+ StatusCode = (int)responseMessage.StatusCode,
+ Headers = responseHeaders
+ };
+
+ if (!responseMessage.IsSuccessStatusCode && options.ThrowExceptionOnErrorResponse)
+ {
+ throw new WebException($"Request to '{input.Url}' failed with status code {(int)responseMessage.StatusCode}. Response body: {response.Body}");
+ }
+
+ return response;
+ }
+
+ //Combine http headears collections into one dictionary
+ private static Dictionary CombineHeaders(HttpHeaders[] headers)
+ {
+ var result = headers
+ .SelectMany(dict => dict)
+ .ToDictionary(pair => pair.Key, pair => string.Join(";", pair.Value));
+ return result;
+ }
+
+ private static async Task GetHttpRequestResponseAsync(HttpClient httpClient, Input input, Options options, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ if (options.Authentication == Authentication.Basic || options.Authentication == Authentication.OAuth)
+ {
+ switch (options.Authentication)
+ {
+ case Authentication.Basic:
+ httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
+ Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}")));
+ break;
+ case Authentication.OAuth:
+ httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",
+ options.Token);
+ break;
+ }
+ }
+
+ using MemoryStream reader = new(File.ReadAllBytes(input.FilePath));
+ using HttpContent content = new StreamContent(reader);
+ var headerDict = input.Headers.ToDictionary(key => key.Name, value => value.Value, StringComparer.InvariantCultureIgnoreCase);
+
+ foreach (var header in headerDict)
+ {
+ var requestHeaderAddedSuccessfully = httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
+ if (!requestHeaderAddedSuccessfully)
+ {
+ // Could not add to request headers, try to add to content headers
+ var contentHeaderAddedSuccessfully = content.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ if (!contentHeaderAddedSuccessfully)
+ {
+ Trace.TraceWarning($"Could not add header {header.Key}:{header.Value}");
+ }
+ }
+ }
+
+ var request = new HttpRequestMessage(new HttpMethod(input.Method.ToString()), new Uri(input.Url))
+ {
+ Content = content
+ };
+
+ var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj
new file mode 100644
index 0000000..a64fa71
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net6.0
+ Frends
+ Frends
+ MIT
+ https://github.com/FrendsPlatform/Frends.HTTP
+ true
+ Frends
+ true
+ 1.0.0
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json
new file mode 100644
index 0000000..34dfe4d
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json
@@ -0,0 +1,7 @@
+{
+ "Tasks": [
+ {
+ "TaskMethod": "Frends.HTTP.UploadFile.UploadFileTask.UploadFile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/HttpClientHandlerExtensions.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/HttpClientHandlerExtensions.cs
new file mode 100644
index 0000000..c654bbd
--- /dev/null
+++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/HttpClientHandlerExtensions.cs
@@ -0,0 +1,64 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
+using System.Text.RegularExpressions;
+using Frends.HTTP.UploadFile.Definitions;
+
+namespace Frends.HTTP.UploadFile;
+
+///
+/// Provides extension methods for various types, allowing for additional functionality to be added to existing types.
+///
+internal static class HttpClientHandlerExtensions
+{
+ internal static void SetHandleSettingsBasedOnOptions(this HttpClientHandler handler, Options options)
+ {
+ switch (options.Authentication)
+ {
+ case Authentication.WindowsIntegratedSecurity:
+ handler.UseDefaultCredentials = true;
+ break;
+ case Authentication.WindowsAuthentication:
+ var domainAndUserName = options.Username.Split('\\');
+ if (domainAndUserName.Length != 2)
+ {
+ throw new ArgumentException($@"Username needs to be 'domain\username' now it was '{options.Username}'");
+ }
+ handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]);
+ break;
+ case Authentication.ClientCertificate:
+ handler.ClientCertificates.Add(GetCertificate(options.CertificateThumbprint));
+ break;
+ }
+
+ handler.AllowAutoRedirect = options.FollowRedirects;
+
+ if (options.AllowInvalidCertificate)
+ {
+ handler.ServerCertificateCustomValidationCallback = (a, b, c, d) => true;
+ }
+ }
+
+ internal static X509Certificate2 GetCertificate(string thumbprint)
+ {
+ thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper();
+ var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
+ try
+ {
+ store.Open(OpenFlags.ReadOnly);
+ var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
+ if (signingCert.Count == 0)
+ {
+ throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in current user cert store.");
+ }
+
+ return signingCert[0];
+ }
+ finally
+ {
+ store.Close();
+ }
+ }
+}
diff --git a/Frends.HTTP.UploadFile/README.md b/Frends.HTTP.UploadFile/README.md
new file mode 100644
index 0000000..e065fa8
--- /dev/null
+++ b/Frends.HTTP.UploadFile/README.md
@@ -0,0 +1,28 @@
+# Frends.HTTP.UploadFile
+Frends Task for Executing a HTTP UploadFile.
+
+[](https://opensource.org/licenses/MIT)
+[](https://github.com/FrendsPlatform/Frends.HTTP/actions)
+
+
+
+Returns the object of body, headers and statuscode.
+
+## Installing
+
+You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed
+https://www.myget.org/F/frends-tasks/api/v2.
+
+## Building
+
+Rebuild the project
+
+`dotnet build`
+
+Run tests
+
+`dotnet test`
+
+Create a NuGet package
+
+`dotnet pack --configuration Release`
diff --git a/README.md b/README.md
index f38fe2c..e5f9b90 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ Frends tasks for HTTP operations.
- [Frends.HTTP.SendBytes](Frends.HTTP.SendBytes/README.md)
- [Frends.HTTP.SendAndReceiveBytes](Frends.HTTP.SendAndReceiveBytes/README.md)
- [Frends.HTTP.DownloadFile](Frends.HTTP.DownloadFile/README.md)
+- [Frends.HTTP.UploadFile](Frends.HTTP.UploadFile/README.md)
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change.
@@ -19,4 +20,4 @@ When contributing to this repository, please first discuss the change you wish t
4. Push your work back up to your fork
5. Submit a Pull request so that we can review your changes
-NOTE: Be sure to merge the latest from "upstream" before making a pull request!
\ No newline at end of file
+NOTE: Be sure to merge the latest from "upstream" before making a pull request!