Skip to content

Commit

Permalink
fix: handle locked attachments (#70)
Browse files Browse the repository at this point in the history
* fix: don't throw if attachment is locked

* fix: upload both default and override attachments

* chore: update README

* chore: add note about files being ignored

* chore: fix test cleanup
  • Loading branch information
bobbyg603 authored Sep 16, 2024
1 parent f2a534c commit 256fcd2
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 102 deletions.
87 changes: 81 additions & 6 deletions BugSplatDotNetStandard.Test/Api/CrashPostClient.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BugSplatDotNetStandard;
Expand All @@ -11,7 +11,6 @@
using Moq;
using Moq.Protected;
using NUnit.Framework;
using static Tests.HttpContentVerifiers;
using static Tests.StackTraceFactory;

namespace Tests
Expand All @@ -23,6 +22,30 @@ public class CrashPostClientTest
const string application = "my-net-crasher";
const string version = "1.0";

FileInfo lockedFile;
FileInfo minidumpFile;
FileStream lockedFileWriter;

[OneTimeSetUp]
public void SetUp()
{
var bytesToWrite = Encoding.UTF8.GetBytes("This file is locked");
lockedFile = new FileInfo("lockedFile.txt");
lockedFileWriter = File.Open(lockedFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
lockedFileWriter.Write(bytesToWrite, 0, bytesToWrite.Length);
lockedFileWriter.Flush();
minidumpFile = new FileInfo("minidump.dmp");
File.Create(minidumpFile.FullName).Close();
}

[OneTimeTearDown]
public void TearDown()
{
lockedFileWriter.Close();
lockedFile.Delete();
minidumpFile.Delete();
}

[Test]
public void CrashPostClient_Constructor_ShouldThrowIfHttpClientFactoryIsNull()
{
Expand All @@ -46,7 +69,7 @@ public async Task CrashPostClient_PostException_ShouldReturn200()
var httpClient = new HttpClient(mockHttp.Object);
var httpClientFactory = new FakeHttpClientFactory(httpClient);
var mockS3ClientFactory = FakeS3ClientFactory.CreateMockS3ClientFactory();

var sut = new CrashPostClient(httpClientFactory, mockS3ClientFactory);

var postResult = await sut.PostException(
Expand All @@ -70,7 +93,7 @@ public async Task CrashPostClient_PostMinidump_ShouldReturn200()
var httpClient = new HttpClient(mockHttp.Object);
var httpClientFactory = new FakeHttpClientFactory(httpClient);
var mockS3ClientFactory = FakeS3ClientFactory.CreateMockS3ClientFactory();

var sut = new CrashPostClient(httpClientFactory, mockS3ClientFactory);

var postResult = await sut.PostMinidump(
Expand All @@ -84,7 +107,59 @@ public async Task CrashPostClient_PostMinidump_ShouldReturn200()
Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode);
}

private Mock<HttpMessageHandler> CreateMockHttpClientForExceptionPost(string crashUploadUrl)
[Test]
public void CrashPostClient_PostException_ShouldNotThrowIfAttachmentLocked()
{
var stackTrace = CreateStackTrace();
var bugsplat = new BugSplat(database, application, version);
bugsplat.Attachments.Add(lockedFile);
var getCrashUrl = "https://fake.url.com";
var mockHttp = CreateMockHttpClientForExceptionPost(getCrashUrl);
var httpClient = new HttpClient(mockHttp.Object);
var httpClientFactory = new FakeHttpClientFactory(httpClient);
var mockS3ClientFactory = FakeS3ClientFactory.CreateMockS3ClientFactory();

var sut = new CrashPostClient(httpClientFactory, mockS3ClientFactory);

Assert.DoesNotThrowAsync(async () =>
{
await sut.PostException(
database,
application,
version,
stackTrace,
ExceptionPostOptions.Create(bugsplat)
);
});
}

[Test]
public void CrashPostClient_PostCrashFile_ShouldNotThrowIfAttachmentLocked()
{
var stackTrace = CreateStackTrace();
var bugsplat = new BugSplat(database, application, version);
bugsplat.Attachments.Add(lockedFile);
var getCrashUrl = "https://fake.url.com";
var mockHttp = CreateMockHttpClientForExceptionPost(getCrashUrl);
var httpClient = new HttpClient(mockHttp.Object);
var httpClientFactory = new FakeHttpClientFactory(httpClient);
var mockS3ClientFactory = FakeS3ClientFactory.CreateMockS3ClientFactory();

var sut = new CrashPostClient(httpClientFactory, mockS3ClientFactory);

Assert.DoesNotThrowAsync(async () =>
{
await sut.PostCrashFile(
database,
application,
version,
new FileInfo("minidump.dmp"),
MinidumpPostOptions.Create(bugsplat)
);
});
}

private Mock<HttpMessageHandler> CreateMockHttpClientForExceptionPost(string crashUploadUrl)
{
var getCrashUploadUrlResponse = new HttpResponseMessage();
getCrashUploadUrlResponse.StatusCode = HttpStatusCode.OK;
Expand All @@ -103,7 +178,7 @@ private Mock<HttpMessageHandler> CreateMockHttpClientForExceptionPost(string cra
)
.ReturnsAsync(getCrashUploadUrlResponse)
.ReturnsAsync(commitCrashUploadUrlReponse);

return handlerMock;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down
137 changes: 44 additions & 93 deletions BugSplatDotNetStandard/Api/CrashPostClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ public async Task<HttpResponseMessage> PostException(
{
overridePostOptions = overridePostOptions ?? new ExceptionPostOptions();

var files = overridePostOptions.Attachments
.Select(attachment => InMemoryFile.FromFileInfo(attachment))
var files = CombineListsWithDuplicatesRemoved(defaultPostOptions.Attachments, overridePostOptions.Attachments)
.Select(attachment => TryCreateInMemoryFileFromFileInfo(attachment))
.Where(file => file != null)
.ToList();

var additionalFormDataFiles = overridePostOptions.FormDataParams
.Where(file => !string.IsNullOrEmpty(file.FileName) && file.Content != null)
.Select(file => new InMemoryFile() { FileName = file.FileName, Content = file.Content.ReadAsByteArrayAsync().Result })
Expand Down Expand Up @@ -82,9 +83,7 @@ public async Task<HttpResponseMessage> PostException(
version,
md5,
s3Key,
crashTypeId,
defaultPostOptions,
overridePostOptions
crashTypeId
);

ThrowIfHttpRequestFailed(commitS3CrashResponse);
Expand Down Expand Up @@ -143,8 +142,9 @@ public async Task<HttpResponseMessage> PostCrashFile(
{
overridePostOptions = overridePostOptions ?? new MinidumpPostOptions();

var files = overridePostOptions.Attachments
.Select(attachment => InMemoryFile.FromFileInfo(attachment))
var files = CombineListsWithDuplicatesRemoved(defaultPostOptions.Attachments, overridePostOptions.Attachments)
.Select(attachment => TryCreateInMemoryFileFromFileInfo(attachment))
.Where(file => file != null)
.ToList();

files.Add(new InMemoryFile() { FileName = crashFileInfo.Name, Content = File.ReadAllBytes(crashFileInfo.FullName) });
Expand Down Expand Up @@ -176,9 +176,7 @@ public async Task<HttpResponseMessage> PostCrashFile(
version,
md5,
s3Key,
crashTypeId,
defaultPostOptions,
overridePostOptions
crashTypeId
);

ThrowIfHttpRequestFailed(commitS3CrashResponse);
Expand All @@ -194,104 +192,45 @@ public void Dispose()
this.s3Client.Dispose();
}

private List<FileInfo> CombineListsWithDuplicatesRemoved(
List<FileInfo> defaultList,
List<FileInfo> overrideList
)
{
return defaultList
.Concat(overrideList)
.GroupBy(file => file.FullName)
.Select(group => group.First())
.ToList();
}

private async Task<HttpResponseMessage> CommitS3CrashUpload(
string database,
string application,
string version,
string md5,
string s3Key,
int crashTypeId,
IBugSplatPostOptions defaultPostOptions,
IBugSplatPostOptions overridePostOptions
int crashTypeId
)
{
var baseUrl = this.CreateBaseUrlFromDatabase(database);
var route = $"{baseUrl}/api/commitS3CrashUpload";
var body = CreateMultiPartFormDataContent(
database,
application,
version,
defaultPostOptions,
overridePostOptions
);
body.Add(new StringContent($"{crashTypeId}"), "crashTypeId");
body.Add(new StringContent(s3Key), "s3Key");
body.Add(new StringContent(md5), "md5");

return await httpClient.PostAsync(route, body);
}

private string CreateBaseUrlFromDatabase(string database)
{
return $"https://{database}.bugsplat.com";
}

private MultipartFormDataContent CreateMultiPartFormDataContent(
string database,
string application,
string version,
IBugSplatPostOptions defaultOptions,
IBugSplatPostOptions overrideOptions = null
)
{
var description = GetStringValueOrDefault(overrideOptions?.Description, defaultOptions.Description);
var email = GetStringValueOrDefault(overrideOptions?.Email, defaultOptions.Email);
var key = GetStringValueOrDefault(overrideOptions?.Key, defaultOptions.Key);
var notes = GetStringValueOrDefault(overrideOptions?.Notes, defaultOptions.Notes);
var user = GetStringValueOrDefault(overrideOptions?.User, defaultOptions.User);

var body = new MultipartFormDataContent
var body = new MultipartFormDataContent()
{
{ new StringContent(database), "database" },
{ new StringContent(application), "appName" },
{ new StringContent(version), "appVersion" },
{ new StringContent(description), "description" },
{ new StringContent(email), "email" },
{ new StringContent(key), "appKey" },
{ new StringContent(notes), "notes" },
{ new StringContent(user), "user" },
{ new StringContent(crashTypeId.ToString()), "crashTypeId" },
{ new StringContent(s3Key), "s3Key" },
{ new StringContent(md5), "md5" }
};

var formDataParams = overrideOptions?.FormDataParams ?? new List<IFormDataParam>();
formDataParams.AddRange(defaultOptions.FormDataParams);
foreach (var param in formDataParams)
{
if (!string.IsNullOrEmpty(param.FileName))
{
body.Add(param.Content, param.Name, param.FileName);
continue;
}

body.Add(param.Content, param.Name);
}

var attachments = new List<FileInfo>();
attachments.AddRange(defaultOptions.Attachments);
if (overrideOptions != null)
{
attachments.AddRange(overrideOptions.Attachments);
}

for (var i = 0; i < attachments.Count; i++)
{
byte[] bytes = null;
using (var fileStream = File.Open(attachments[i].FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var memoryStream = new MemoryStream())
{
fileStream.CopyTo(memoryStream);
bytes = memoryStream.ToArray();
}
}

if (bytes != null)
{
var name = attachments[i].Name;
body.Add(new ByteArrayContent(bytes), name, name);
}
}
return await httpClient.PostAsync(route, body);
}

return body;
private string CreateBaseUrlFromDatabase(string database)
{
return $"https://{database}.bugsplat.com";
}

private async Task<HttpResponseMessage> GetCrashUploadUrl(
Expand All @@ -305,7 +244,7 @@ int crashPostSize
var path = $"{baseUrl}/api/getCrashUploadUrl";
var route = $"{path}?database={database}&appName={application}&appVersion={version}&crashPostSize={crashPostSize}";


return await httpClient.GetAsync(route);
}

Expand All @@ -332,5 +271,17 @@ private async Task<Uri> GetPresignedUrlFromResponse(HttpResponseMessage response
throw new Exception("Failed to parse crash upload url", ex);
}
}

private InMemoryFile TryCreateInMemoryFileFromFileInfo(FileInfo fileInfo)
{
try
{
return InMemoryFile.FromFileInfo(fileInfo);
}
catch
{
return null;
}
}
}
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Additionally, `Post` can be used to upload minidumps to BugSplat.
await bugsplat.Post(new FileInfo("/path/to/minidump.dmp"));
```

The default values for description, email, key and user can be overridden in the call to Post. Additional attachments can also be specified in the call to Post. Please note that the total size of the Post body and all attachments is limited to **20MB** by default.
The default values for description, email, key and user can be overridden in the call to Post. Additional attachments can also be specified in the call to Post. If BugSplat can't read an attachment (e.g. the file is in use), it will be skipped. Please note that the total size of the Post body and all attachments is limited to **20MB** by default.

```cs
var options = new ExceptionPostOptions()
Expand All @@ -72,7 +72,7 @@ var options = new ExceptionPostOptions()
User = "Fred",
Key = "the key!"
};
options.AdditionalAttachments.Add(new FileInfo("/path/to/attachment2.txt"));
options.Attachments.Add(new FileInfo("/path/to/attachment2.txt"));

await bugsplat.Post(ex, options);
```
Expand Down

0 comments on commit 256fcd2

Please sign in to comment.