-
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 13 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 |
---|---|---|
|
@@ -16,6 +16,10 @@ enum NetworkError: BacktraceError { | |
|
||
enum HttpError: BacktraceError { | ||
case malformedUrl(URL) | ||
case fileCreationFailed(URL) | ||
case fileWriteFailed | ||
case attachmentError(String) | ||
case multipartFormError(Error) | ||
case unknownError | ||
} | ||
|
||
|
@@ -42,7 +46,7 @@ enum CodingError: BacktraceError { | |
extension HttpError { | ||
var backtraceStatus: BacktraceReportStatus { | ||
switch self { | ||
case .malformedUrl: | ||
case .malformedUrl, .fileCreationFailed, .fileWriteFailed, .attachmentError, .multipartFormError: | ||
return .unknownError | ||
case .unknownError: | ||
return .serverError | ||
|
@@ -62,8 +66,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: | ||
return "File Write Error occurred." | ||
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. Suggestion: I think file is to generic and might be weird from the developer perspective that the file means a temporary file during HTTP submission. My recommendation is to make it more explicit by saying "Temporary submission file" instead. 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. yes, it looks out of place |
||
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 |
---|---|---|
|
@@ -48,41 +48,85 @@ extension MultipartRequest { | |
var multipartRequest = urlRequest | ||
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") | ||
} | ||
// 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") | ||
|
||
// temporary file to stream data | ||
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) | ||
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. what happen to the file if the app is restarted while file is being created? 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. file is created in Temp directory managed by OS, if the app is restarted there is no guarantee the file is persisted but :
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. unlike NSMutableData it's even possible to recover multipart request file! (not sure if necessary but can be a good excuse to consider for |
||
|
||
do { | ||
let fileCreated = FileManager.default.createFile(atPath: tempURL.path, contents: nil, attributes: nil) | ||
if !fileCreated { | ||
throw HttpError.fileCreationFailed(tempURL) | ||
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 won't be captured by the L:111 - which means we will return a generic error rather this this one? |
||
} | ||
|
||
let fileHandle = try FileHandle(forWritingTo: tempURL) | ||
defer { | ||
if #available(iOS 13.0, tvOS 13.0, macOS 11.0, *) { | ||
try? fileHandle.close() | ||
} else { | ||
fileHandle.closeFile() | ||
} | ||
} | ||
|
||
// 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 writeToFile(fileHandle, attributesString) | ||
|
||
// 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 writeToFile(fileHandle, reportString) | ||
fileHandle.write(report.reportData) | ||
try writeToFile(fileHandle, "\r\n") | ||
|
||
// 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 writeToFile(fileHandle, "--\(boundary)\r\n") | ||
try writeToFile(fileHandle, "Content-Disposition: form-data; name=\"\(attachment.filename)\"; filename=\"\(attachment.filename)\"\r\n") | ||
try writeToFile(fileHandle, "Content-Type: \(attachment.mimeType)\r\n\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. I think we should do it with a single write |
||
fileHandle.write(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. you're making an attachment copy in the temporary file. WE cannot stream a file directly from the file available somewhere? 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. possible but requires creating an InputStream for each attachments. |
||
try writeToFile(fileHandle, "\r\n") | ||
} | ||
|
||
// Final boundary | ||
try writeToFile(fileHandle, "--\(boundary)--\r\n") | ||
|
||
// Set Content-Length | ||
let fileSize = try FileManager.default.attributesOfItem(atPath: tempURL.path)[.size] as? Int ?? 0 | ||
multipartRequest.setValue("\(fileSize)", forHTTPHeaderField: "Content-Length") | ||
|
||
// Attach file stream to HTTP body | ||
multipartRequest.httpBodyStream = InputStream(url: tempURL) | ||
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. Instead of creating the file and setting it as I'm not proficient in Swift, but I found this: You can use bound streams to achieve that, it seems. 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, this is the the approach I was referencing:
|
||
|
||
} catch { | ||
BacktraceLogger.error("Error during multipart form creation: \(error.localizedDescription)") | ||
throw HttpError.multipartFormError(error) | ||
} | ||
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 writeToFile(_ fileHandle: FileHandle, _ string: String) throws { | ||
if let data = string.data(using: .utf8) { | ||
fileHandle.write(data) | ||
} else { | ||
throw HttpError.fileWriteFailed | ||
} | ||
} | ||
} | ||
|
||
private extension NSMutableData { | ||
|
Uh oh!
There was an error while loading. Please reload this page.