diff --git a/src/Stripe.net/Infrastructure/FormEncoding/ContentEncoder.cs b/src/Stripe.net/Infrastructure/FormEncoding/ContentEncoder.cs index aeb2453cc8..32733e638c 100644 --- a/src/Stripe.net/Infrastructure/FormEncoding/ContentEncoder.cs +++ b/src/Stripe.net/Infrastructure/FormEncoding/ContentEncoder.cs @@ -145,6 +145,10 @@ private static List> FlattenParamsValue(object valu flatParams = SingleParam(keyPrefix, s); break; + case MultipartFileContent f: + flatParams = SingleParam(keyPrefix, f); + break; + case Stream s: flatParams = SingleParam(keyPrefix, s); break; diff --git a/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs b/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs index 53b2441bae..2c9bca954d 100644 --- a/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs +++ b/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs @@ -116,6 +116,10 @@ private static List> FlattenParamsValue(object valu flatParams = SingleParam(keyPrefix, s); break; + case MultipartFileContent f: + flatParams = SingleParam(keyPrefix, f); + break; + case Stream s: flatParams = SingleParam(keyPrefix, s); break; diff --git a/src/Stripe.net/Infrastructure/FormEncoding/MultipartFormDataContent.cs b/src/Stripe.net/Infrastructure/FormEncoding/MultipartFormDataContent.cs index d7ff9b3427..4c2c46d290 100644 --- a/src/Stripe.net/Infrastructure/FormEncoding/MultipartFormDataContent.cs +++ b/src/Stripe.net/Infrastructure/FormEncoding/MultipartFormDataContent.cs @@ -41,26 +41,29 @@ public MultipartFormDataContent( private static StringContent CreateStringContent(string value) => new StringContent(value, System.Text.Encoding.UTF8); - private static StreamContent CreateStreamContent(Stream value, string name) + private static StreamContent CreateStreamContent(MultipartFileContent value, string name) { - var fileName = "blob"; - var extension = string.Empty; + var fileName = value.Name ?? "blob"; + var extension = Path.GetExtension(fileName); + var stream = value.Data; - FileStream fileStream = value as FileStream; + FileStream fileStream = stream as FileStream; if ((fileStream != null) && (!string.IsNullOrEmpty(fileStream.Name))) { fileName = fileStream.Name; extension = Path.GetExtension(fileName); } - var content = new StreamContent(value); + var type = value.Type ?? MimeTypes.GetMimeType(extension); + + var content = new StreamContent(stream); content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = name, FileName = fileName, FileNameStar = fileName, }; - content.Headers.ContentType = new MediaTypeHeaderValue(MimeTypes.GetMimeType(extension)); + content.Headers.ContentType = new MediaTypeHeaderValue(type); return content; } @@ -79,8 +82,16 @@ private void ProcessParameters(IEnumerable> nameVal this.Add(CreateStringContent(s), QuoteString(kvp.Key)); break; + case MultipartFileContent f: + this.Add(CreateStreamContent(f, QuoteString(kvp.Key))); + break; + case Stream s: - this.Add(CreateStreamContent(s, QuoteString(kvp.Key))); + var fileData = new MultipartFileContent + { + Data = s, + }; + this.Add(CreateStreamContent(fileData, QuoteString(kvp.Key))); break; default: diff --git a/src/Stripe.net/Services/Files/FileCreateOptions.cs b/src/Stripe.net/Services/Files/FileCreateOptions.cs index a252e950b2..093dcefc53 100644 --- a/src/Stripe.net/Services/Files/FileCreateOptions.cs +++ b/src/Stripe.net/Services/Files/FileCreateOptions.cs @@ -1,7 +1,6 @@ // File generated from our OpenAPI spec namespace Stripe { - using System.IO; using Newtonsoft.Json; #if NET6_0_OR_GREATER using STJS = System.Text.Json.Serialization; @@ -17,7 +16,7 @@ public class FileCreateOptions : BaseOptions #if NET6_0_OR_GREATER [STJS.JsonPropertyName("file")] #endif - public Stream File { get; set; } + public MultipartFileContent File { get; set; } /// /// Optional parameters that automatically create a + /// Represents Data and optional Name and Type that will be encoded as multipart form + /// data. Used in e.g. FileService.Create. + /// + /// + public class MultipartFileContent + { + /// + /// The file data to send. If this is a FileStream, the SDK will infer + /// the name and type from the file name and extension. If this is not + /// a FileStream set Name and Type to configure the file upload. + /// + public Stream Data { get; set; } + + /// + /// The optional name to send with this file data. Uses the file name if omitted + /// and Data is a FileStream. + /// + public string Name { get; set; } + + /// + /// The optional mime type to use when sending file data. Uses the type that + /// matches the file extension from Name (or the file name from Data) if omitted. + /// + public string Type { get; set; } + } +} diff --git a/src/StripeTests/Infrastructure/FormEncoding/MultipartFormDataContentTest.cs b/src/StripeTests/Infrastructure/FormEncoding/MultipartFormDataContentTest.cs index 30f2bf6c40..fcaa77faad 100644 --- a/src/StripeTests/Infrastructure/FormEncoding/MultipartFormDataContentTest.cs +++ b/src/StripeTests/Infrastructure/FormEncoding/MultipartFormDataContentTest.cs @@ -5,6 +5,7 @@ namespace StripeTests using System.IO; using System.Text; using System.Threading.Tasks; + using Stripe; using Stripe.Infrastructure.FormEncoding; using Xunit; @@ -71,6 +72,132 @@ public async Task Ctor_OneStreamEntry_Success() result); } + [Fact] + public async Task Ctor_OneMultipartFileContentEntry_Success() + { + var source = new Dictionary + { + { "key", new MultipartFileContent { Data = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")) } }, + }; + var content = new MultipartFormDataContent(source, "test-boundary"); + + var stream = await content.ReadAsStreamAsync(); + Assert.Equal(174, stream.Length); + var result = new StreamReader(stream).ReadToEnd(); + Assert.Equal( + "--test-boundary\r\n" + + "Content-Disposition: form-data; name=\"key\"; filename=blob; filename*=utf-8''blob\r\n" + + "Content-Type: application/octet-stream\r\n\r\nHello World!\r\n" + + "--test-boundary--\r\n", + result); + } + + [Fact] + public async Task Ctor_OneMultipartFileContentWithNameEntry_Success() + { + var source = new Dictionary + { + { + "key", new MultipartFileContent + { + Data = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")), + Name = "file", + } + }, + }; + var content = new MultipartFormDataContent(source, "test-boundary"); + + var stream = await content.ReadAsStreamAsync(); + Assert.Equal(174, stream.Length); + var result = new StreamReader(stream).ReadToEnd(); + Assert.Equal( + "--test-boundary\r\n" + + "Content-Disposition: form-data; name=\"key\"; filename=file; filename*=utf-8''file\r\n" + + "Content-Type: application/octet-stream\r\n\r\nHello World!\r\n" + + "--test-boundary--\r\n", + result); + } + + [Fact] + public async Task Ctor_OneMultipartFileContentWithNameAndExtEntry_Success() + { + var source = new Dictionary + { + { + "key", new MultipartFileContent + { + Data = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")), + Name = "file.csv", + } + }, + }; + var content = new MultipartFormDataContent(source, "test-boundary"); + + var stream = await content.ReadAsStreamAsync(); + Assert.Equal(166, stream.Length); + var result = new StreamReader(stream).ReadToEnd(); + Assert.Equal( + "--test-boundary\r\n" + + "Content-Disposition: form-data; name=\"key\"; filename=file.csv; filename*=utf-8''file.csv\r\n" + + "Content-Type: text/csv\r\n\r\nHello World!\r\n" + + "--test-boundary--\r\n", + result); + } + + [Fact] + public async Task Ctor_OneMultipartFileContentWithNameAndTypeEntry_Success() + { + var source = new Dictionary + { + { + "key", new MultipartFileContent + { + Data = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")), + Name = "file", + Type = "application/json", + } + }, + }; + var content = new MultipartFormDataContent(source, "test-boundary"); + + var stream = await content.ReadAsStreamAsync(); + Assert.Equal(166, stream.Length); + var result = new StreamReader(stream).ReadToEnd(); + Assert.Equal( + "--test-boundary\r\n" + + "Content-Disposition: form-data; name=\"key\"; filename=file; filename*=utf-8''file\r\n" + + "Content-Type: application/json\r\n\r\nHello World!\r\n" + + "--test-boundary--\r\n", + result); + } + + [Fact] + public async Task Ctor_OneMultipartFileContentWithNameAndExtAndTypeEntry_Success() + { + var source = new Dictionary + { + { + "key", new MultipartFileContent + { + Data = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")), + Name = "file.json", + Type = "application/octet-stream", + } + }, + }; + var content = new MultipartFormDataContent(source, "test-boundary"); + + var stream = await content.ReadAsStreamAsync(); + Assert.Equal(184, stream.Length); + var result = new StreamReader(stream).ReadToEnd(); + Assert.Equal( + "--test-boundary\r\n" + + "Content-Disposition: form-data; name=\"key\"; filename=file.json; filename*=utf-8''file.json\r\n" + + "Content-Type: application/octet-stream\r\n\r\nHello World!\r\n" + + "--test-boundary--\r\n", + result); + } + [Fact] public async Task Ctor_TwoEntries_Success() { diff --git a/src/StripeTests/Services/Files/FileServiceTest.cs b/src/StripeTests/Services/Files/FileServiceTest.cs index 9a0bd84032..bdf88084cd 100644 --- a/src/StripeTests/Services/Files/FileServiceTest.cs +++ b/src/StripeTests/Services/Files/FileServiceTest.cs @@ -28,9 +28,13 @@ public FileServiceTest( { this.service = new FileService(this.StripeClient); + var resourceStream = typeof(FileServiceTest).GetTypeInfo().Assembly.GetManifestResourceStream(FileName); this.createOptions = new FileCreateOptions { - File = typeof(FileServiceTest).GetTypeInfo().Assembly.GetManifestResourceStream(FileName), + File = new MultipartFileContent + { + Data = resourceStream, + }, FileLinkData = new FileFileLinkDataOptions { Create = true, @@ -44,7 +48,10 @@ public FileServiceTest( this.base64Options = new FileCreateOptions { - File = new MemoryStream(Convert.FromBase64String("c3RyaXBlLWRvdG5ldA==")), + File = new MultipartFileContent + { + Data = new MemoryStream(Convert.FromBase64String("c3RyaXBlLWRvdG5ldA==")), + }, Purpose = FilePurpose.BusinessLogo, }; diff --git a/src/StripeTests/Services/GeneratedExamplesTest.cs b/src/StripeTests/Services/GeneratedExamplesTest.cs index a91e363ab4..ee1712c6c8 100644 --- a/src/StripeTests/Services/GeneratedExamplesTest.cs +++ b/src/StripeTests/Services/GeneratedExamplesTest.cs @@ -1330,8 +1330,11 @@ public void TestFilesPost() var options = new FileCreateOptions { Purpose = "account_requirement", - File = new System.IO.MemoryStream( - System.Text.Encoding.UTF8.GetBytes("File contents")), + File = new Stripe.MultipartFileContent + { + Data = new System.IO.MemoryStream( + System.Text.Encoding.UTF8.GetBytes("File contents")), + }, }; var service = new FileService(this.StripeClient); File file = service.Create(options);