-
Notifications
You must be signed in to change notification settings - Fork 17
Improvement/attachments and requests #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
951c3a8
b422cea
ec3f888
63f19f0
7983155
b447b73
84619eb
c2fb53a
ef88290
e7e659e
e5cbedb
bb3f7f6
e1cd517
4c2fbde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,5 +38,4 @@ - (IBAction) crashAction: (id) sender { | |
array[1]; | ||
} | ||
|
||
|
||
@end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,8 +14,15 @@ enum NetworkError: BacktraceError { | |
case connectionError(Error) | ||
} | ||
|
||
//TODO: Create and update stream error category | ||
enum HttpError: BacktraceError { | ||
case malformedUrl(URL) | ||
case fileCreationFailed(URL) | ||
case fileWriteFailed | ||
case attachmentError(String) | ||
case multipartFormError(Error) | ||
case streamReadFailed | ||
case streamWriteFailed | ||
case unknownError | ||
} | ||
|
||
|
@@ -42,7 +49,7 @@ enum CodingError: BacktraceError { | |
extension HttpError { | ||
var backtraceStatus: BacktraceReportStatus { | ||
switch self { | ||
case .malformedUrl: | ||
case .malformedUrl, .fileCreationFailed, .fileWriteFailed, .attachmentError, .multipartFormError, .streamReadFailed, .streamWriteFailed: | ||
return .unknownError | ||
case .unknownError: | ||
return .serverError | ||
|
@@ -62,8 +69,18 @@ extension NetworkError { | |
extension HttpError { | ||
var localizedDescription: String { | ||
switch self { | ||
case .malformedUrl(let url): return "Provided URL cannot be parsed: \(url)." | ||
case .unknownError: return "Unknown error occurred." | ||
case .malformedUrl(let url): | ||
return "Provided URL cannot be parsed: \(url)." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small nit: can we either get rid of trailing dots or put the URL in quotes? |
||
case .fileCreationFailed(let url): | ||
return "File Error occurred: \(url)." | ||
case .fileWriteFailed, .streamReadFailed, .streamWriteFailed: | ||
return "File Write Error occurred." | ||
case .attachmentError(let string): | ||
return "Attachment Error occurred: \(string)." | ||
case .multipartFormError(let error): | ||
return "Multipart Form Error occurred: \(error.localizedDescription)." | ||
case .unknownError: | ||
return "Unknown error occurred." | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,46 +49,117 @@ extension MultipartRequest { | |
let boundary = generateBoundaryString() | ||
multipartRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") | ||
|
||
let boundaryPrefix = "--\(boundary)\r\n" | ||
let body = NSMutableData() | ||
// attributes | ||
for attribute in report.attributes { | ||
body.appendString(boundaryPrefix) | ||
body.appendString("Content-Disposition: form-data; name=\"\(attribute.key)\"\r\n\r\n") | ||
body.appendString("\(attribute.value)\r\n") | ||
// output stream | ||
// TODO: bind input & output streams | ||
let outputStream = OutputStream.toMemory() | ||
outputStream.open() | ||
defer { outputStream.close() } | ||
|
||
let writeLock = DispatchQueue(label: "backtrace.multipartRequest.writeLock") | ||
|
||
do { | ||
// attributes | ||
var attributesString = "" | ||
for attribute in report.attributes { | ||
attributesString += "--\(boundary)\r\n" | ||
attributesString += "Content-Disposition: form-data; name=\"\(attribute.key)\"\r\n\r\n" | ||
attributesString += "\(attribute.value)\r\n" | ||
} | ||
try writeToStream(outputStream, attributesString, writeLock: writeLock) | ||
|
||
// report | ||
var reportString = "--\(boundary)\r\n" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better if we start with report rather than with attributes - if the application stops during submission part, we can still submit a valid report. |
||
reportString += "Content-Disposition: form-data; name=\"upload_file\"; filename=\"upload_file\"\r\n" | ||
reportString += "Content-Type: application/octet-stream\r\n\r\n" | ||
try writeToStream(outputStream, reportString, writeLock: writeLock) | ||
try writeLock.sync { | ||
let data = report.reportData | ||
let bytesWritten = data.withUnsafeBytes { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need to execute this part of the code in the sync context? You won't submit two identical reports. |
||
guard let baseAddress = $0.bindMemory(to: UInt8.self).baseAddress else { | ||
return -1 | ||
} | ||
return outputStream.write(baseAddress, maxLength: data.count) | ||
} | ||
if bytesWritten < 0 { | ||
throw HttpError.streamWriteFailed | ||
} | ||
} | ||
try writeToStream(outputStream, "\r\n", writeLock: writeLock) | ||
|
||
// attachments | ||
for attachmentPath in Set(report.attachmentPaths) { | ||
guard let attachment = Attachment(filePath: attachmentPath) else { | ||
BacktraceLogger.error("Failed to create attachment for path: \(attachmentPath)") | ||
continue | ||
} | ||
|
||
try writeToStream(outputStream, "--\(boundary)\r\n", writeLock: writeLock) | ||
try writeToStream(outputStream, "Content-Disposition: form-data; name=\"\(attachment.filename)\"; filename=\"\(attachment.filename)\"\r\n", writeLock: writeLock) | ||
try writeToStream(outputStream, "Content-Type: \(attachment.mimeType)\r\n\r\n", writeLock: writeLock) | ||
try writeLock.sync { | ||
let data = attachment.data | ||
let bytesWritten = data.withUnsafeBytes { | ||
guard let baseAddress = $0.bindMemory(to: UInt8.self).baseAddress else { | ||
return -1 | ||
} | ||
return outputStream.write(baseAddress, maxLength: data.count) | ||
} | ||
if bytesWritten < 0 { | ||
throw HttpError.streamWriteFailed | ||
} | ||
} | ||
try writeToStream(outputStream, "\r\n", writeLock: writeLock) | ||
} | ||
|
||
// Final boundary | ||
try writeToStream(outputStream, "--\(boundary)--\r\n", writeLock: writeLock) | ||
|
||
// Data from Output Stream | ||
guard let data = outputStream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we read data from the output stream here? |
||
throw HttpError.streamReadFailed | ||
} | ||
// Set Content-Length | ||
multipartRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length") | ||
// Attach file stream to HTTP body | ||
multipartRequest.httpBodyStream = InputStream(data: data) | ||
} catch { | ||
BacktraceLogger.error("Error during multipart form creation: \(error.localizedDescription)") | ||
throw HttpError.multipartFormError(error) | ||
} | ||
// report file | ||
body.appendString(boundaryPrefix) | ||
body.appendString("Content-Disposition: form-data; name=\"upload_file\"; filename=\"upload_file\"\r\n") | ||
body.appendString("Content-Type: application/octet-stream\r\n\r\n") | ||
body.append(report.reportData) | ||
body.appendString("\r\n") | ||
|
||
// attachments | ||
for attachment in Set(report.attachmentPaths).compactMap(Attachment.init(filePath:)) { | ||
body.appendString(boundaryPrefix) | ||
body.appendString("Content-Disposition: form-data; name=\"\(attachment.filename)\"; filename=\"\(attachment.filename)\"\r\n") | ||
body.appendString("Content-Type: \(attachment.mimeType)\r\n\r\n") | ||
body.append(attachment.data) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we stream data instead of reading it to memory? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can use a Stream. Benefits:
Caviats:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comment is on original not the updated code |
||
body.appendString("\r\n") | ||
} | ||
body.appendString("\(boundaryPrefix)--") | ||
multipartRequest.httpBody = body as Data | ||
|
||
multipartRequest.setValue("\(body.length)", forHTTPHeaderField: "Content-Length") | ||
|
||
return multipartRequest | ||
} | ||
|
||
private static func generateBoundaryString() -> String { | ||
return "Boundary-\(NSUUID().uuidString)" | ||
} | ||
|
||
private static func writeToStream(_ stream: OutputStream, _ string: String, writeLock: DispatchQueue) throws { | ||
guard let data = string.data(using: .utf8) else { | ||
BacktraceLogger.error("Failed to convert string to UTF-8 data: \(string)") | ||
throw HttpError.fileWriteFailed | ||
} | ||
try writeLock.sync { | ||
let bytesWritten = data.withUnsafeBytes { | ||
guard let baseAddress = $0.bindMemory(to: UInt8.self).baseAddress else { | ||
return -1 | ||
} | ||
return stream.write(baseAddress, maxLength: data.count) | ||
} | ||
if bytesWritten < 0 { | ||
throw HttpError.streamWriteFailed | ||
} | ||
} | ||
} | ||
} | ||
|
||
private extension NSMutableData { | ||
|
||
func appendString(_ string: String) { | ||
guard let data = string.data(using: String.Encoding.utf8, allowLossyConversion: false) else { return } | ||
guard let data = string.data(using: String.Encoding.utf8, allowLossyConversion: false) else { | ||
BacktraceLogger.error("Failed to append string as UTF-8 data: \(string)") | ||
return | ||
} | ||
append(data) | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.