From f12c3c20d74d18eeda100fdfd9f7511003d744ee Mon Sep 17 00:00:00 2001 From: kevinzhow Date: Mon, 30 Mar 2015 07:32:11 +0800 Subject: [PATCH 1/4] Add Token API --- Yep.xcodeproj/project.pbxproj | 4 +++ Yep/Services/YepStorageService.swift | 28 +++++++++++++++++++ .../ConversationViewController.swift | 8 ++++++ Yep/Views/MessageToolbar/MessageToolbar.swift | 9 ++++++ 4 files changed, 49 insertions(+) create mode 100644 Yep/Services/YepStorageService.swift diff --git a/Yep.xcodeproj/project.pbxproj b/Yep.xcodeproj/project.pbxproj index 27fa8d1a..4c0fe2a8 100644 --- a/Yep.xcodeproj/project.pbxproj +++ b/Yep.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 0A8F54B81AB67D11004AD60E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A8F54B71AB67D11004AD60E /* Images.xcassets */; }; 0A8F54BB1AB67D11004AD60E /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0A8F54B91AB67D11004AD60E /* LaunchScreen.xib */; }; 0A8F54C71AB67D11004AD60E /* YepTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8F54C61AB67D11004AD60E /* YepTests.swift */; }; + 0A944C251AC8BB2F00037A06 /* YepStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A944C241AC8BB2F00037A06 /* YepStorageService.swift */; }; 0AD7EA5E1AC3ED1200617758 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA5D1AC3ED1200617758 /* CFNetwork.framework */; }; 0AD7EA601AC3ED1900617758 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA5F1AC3ED1900617758 /* CoreFoundation.framework */; }; 0AD7EA621AC3ED2100617758 /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA611AC3ED2100617758 /* CoreTelephony.framework */; }; @@ -99,6 +100,7 @@ 0A8F54C01AB67D11004AD60E /* YepTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = YepTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0A8F54C51AB67D11004AD60E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0A8F54C61AB67D11004AD60E /* YepTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YepTests.swift; sourceTree = ""; }; + 0A944C241AC8BB2F00037A06 /* YepStorageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = YepStorageService.swift; path = Services/YepStorageService.swift; sourceTree = ""; }; 0AD7EA511AC3EA6F00617758 /* JPushSDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JPushSDK.framework; path = "Pods/../build/Debug-iphoneos/Pods/JPushSDK.framework"; sourceTree = ""; }; 0AD7EA581AC3EAF300617758 /* Yep-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Yep-Bridging-Header.h"; sourceTree = ""; }; 0AD7EA5B1AC3ECD700617758 /* libPushSDK-1.8.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPushSDK-1.8.3.a"; path = "Yep/lib/libPushSDK-1.8.3.a"; sourceTree = ""; }; @@ -431,6 +433,7 @@ 502AE5281AB81076005BD199 /* YepService.swift */, 50E114F81ABC137A00F13000 /* YepServiceSync.swift */, 5076C5691AC46B9F00B22952 /* FayeService.swift */, + 0A944C241AC8BB2F00037A06 /* YepStorageService.swift */, ); name = Services; sourceTree = ""; @@ -832,6 +835,7 @@ 503315A01AC126100008A209 /* MessageToolbar.swift in Sources */, 502AE5531AB93215005BD199 /* ProfileFooterCell.swift in Sources */, 0A5D83991AC5D91E000045BF /* ContactsSearchTableViewController.swift in Sources */, + 0A944C251AC8BB2F00037A06 /* YepStorageService.swift in Sources */, 5023DE521AB6C51300B3EE96 /* CGRect+Yep.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Yep/Services/YepStorageService.swift b/Yep/Services/YepStorageService.swift new file mode 100644 index 00000000..886c6423 --- /dev/null +++ b/Yep/Services/YepStorageService.swift @@ -0,0 +1,28 @@ +// +// YepStorageService.swift +// Yep +// +// Created by kevinzhow on 15/3/30. +// Copyright (c) 2015年 Catch Inc. All rights reserved. +// + +import Foundation + +func uploadFileToAWSS3(fileUrl: NSURL) -> String { + return "" +} + +func requestAWSS3UploadForm(#failureHandler: ((Reason, String?) -> ())?, #completion: JSONDictionary -> Void) { + + let parse: JSONDictionary -> JSONDictionary? = { data in + return data + } + + let resource = authJsonResource(path: "/api/v1/attachments/s3_upload_public_form_fields", method: .GET, requestParameters:[:], parse: parse) + + if let failureHandler = failureHandler { + apiRequest({_ in}, baseURL, resource, failureHandler, completion) + } else { + apiRequest({_ in}, baseURL, resource, defaultFailureHandler, completion) + } +} diff --git a/Yep/ViewControllers/Conversation/ConversationViewController.swift b/Yep/ViewControllers/Conversation/ConversationViewController.swift index 40813584..2d76e359 100644 --- a/Yep/ViewControllers/Conversation/ConversationViewController.swift +++ b/Yep/ViewControllers/Conversation/ConversationViewController.swift @@ -137,6 +137,14 @@ class ConversationViewController: UIViewController { }) } } + + messageToolbar.voiceSendAction = { messageToolbar in + requestAWSS3UploadForm(failureHandler: { (reason, error) in + println("\(error)") + }, completion: { token in + println("Got S3 token is \(token)") + }) + } } override func viewDidLayoutSubviews() { diff --git a/Yep/Views/MessageToolbar/MessageToolbar.swift b/Yep/Views/MessageToolbar/MessageToolbar.swift index 2678c148..6abe3328 100644 --- a/Yep/Views/MessageToolbar/MessageToolbar.swift +++ b/Yep/Views/MessageToolbar/MessageToolbar.swift @@ -31,6 +31,8 @@ class MessageToolbar: UIToolbar { } var textSendAction: ((messageToolBar: MessageToolbar) -> ())? + + var voiceSendAction: ((messageToolBar: MessageToolbar) -> ())? lazy var cameraButton: UIButton = { let button = UIButton() @@ -51,6 +53,7 @@ class MessageToolbar: UIToolbar { let button = UIButton() button.setImage(UIImage(named: "item_mic"), forState: .Normal) button.tintColor = UIColor.yepTintColor() + button.addTarget(self, action: "trySendVoiceMessage", forControlEvents: UIControlEvents.TouchUpInside) return button }() @@ -127,6 +130,12 @@ class MessageToolbar: UIToolbar { textSendAction(messageToolBar: self) } } + + func trySendVoiceMessage() { + if let textSendAction = voiceSendAction { + textSendAction(messageToolBar: self) + } + } } // MARK: UITextFieldDelegate From d4c1a7a4edf2fcd4fbf41ac5b47c4d445d2b1cf9 Mon Sep 17 00:00:00 2001 From: kevinzhow Date: Mon, 30 Mar 2015 08:52:15 +0800 Subject: [PATCH 2/4] Add S3 Upload Support --- Podfile | 1 - Podfile.lock | 8 +- Yep.xcodeproj/project.pbxproj | 12 +++ Yep/Services/YepNetworking.swift | 82 ++++++++++++++ Yep/Services/YepStorageService.swift | 100 +++++++++++++++++- .../ConversationViewController.swift | 5 + Yep/icon-1024.png | Bin 0 -> 54960 bytes 7 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 Yep/icon-1024.png diff --git a/Podfile b/Podfile index e15a7121..72a0d185 100644 --- a/Podfile +++ b/Podfile @@ -3,6 +3,5 @@ platform :ios, '8.0' use_frameworks! pod 'MZFayeClient', :path => '../CatchLib-iOS/MZFayeClient/' -#pod 'MZFayeClient' pod 'JPushSDK' pod 'Realm' diff --git a/Podfile.lock b/Podfile.lock index 8bcf84f1..3118e9fe 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -19,10 +19,10 @@ EXTERNAL SOURCES: :path: "../CatchLib-iOS/MZFayeClient/" SPEC CHECKSUMS: - Base64: c6d74f31f150543b246a07e180ba813c88e59311 - JPushSDK: 30b632d813610d80f27c18eb2a7d52ceb9eeaee7 + Base64: 4924bf3ca6fa559a5161ef717291bd450eb7bd1a + JPushSDK: c68dd04c595a5c93aa003f212974010790410d8e MZFayeClient: 19df756a4c86c8f414668e6edc54fccfee54adbb - Realm: 81adfc7f43f9301f40bbc46d5e8a10aaf71b9504 - SocketRocket: 79a68ae186063c65e1fe6f638b1ae2927aa5f304 + Realm: b9c1fd9defa26aa7276ce938a8df52ec81f24528 + SocketRocket: 7284ab9370a06c99aba92b2fe3a32aedd0f9a6fa COCOAPODS: 0.36.1 diff --git a/Yep.xcodeproj/project.pbxproj b/Yep.xcodeproj/project.pbxproj index 4c0fe2a8..ebe60131 100644 --- a/Yep.xcodeproj/project.pbxproj +++ b/Yep.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 0A8F54BB1AB67D11004AD60E /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0A8F54B91AB67D11004AD60E /* LaunchScreen.xib */; }; 0A8F54C71AB67D11004AD60E /* YepTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8F54C61AB67D11004AD60E /* YepTests.swift */; }; 0A944C251AC8BB2F00037A06 /* YepStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A944C241AC8BB2F00037A06 /* YepStorageService.swift */; }; + 0AAFCFF91AC8CA5B00EB0EF8 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AAFCFF81AC8CA5B00EB0EF8 /* MobileCoreServices.framework */; }; + 0AAFCFFB1AC8CDFE00EB0EF8 /* icon-1024.png in Resources */ = {isa = PBXBuildFile; fileRef = 0AAFCFFA1AC8CDFE00EB0EF8 /* icon-1024.png */; }; 0AD7EA5E1AC3ED1200617758 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA5D1AC3ED1200617758 /* CFNetwork.framework */; }; 0AD7EA601AC3ED1900617758 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA5F1AC3ED1900617758 /* CoreFoundation.framework */; }; 0AD7EA621AC3ED2100617758 /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA611AC3ED2100617758 /* CoreTelephony.framework */; }; @@ -101,6 +103,9 @@ 0A8F54C51AB67D11004AD60E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0A8F54C61AB67D11004AD60E /* YepTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YepTests.swift; sourceTree = ""; }; 0A944C241AC8BB2F00037A06 /* YepStorageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = YepStorageService.swift; path = Services/YepStorageService.swift; sourceTree = ""; }; + 0AAFCFF61AC8C75900EB0EF8 /* AWSS3.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AWSS3.framework; path = "Pods/../build/Debug-iphoneos/Pods/AWSS3.framework"; sourceTree = ""; }; + 0AAFCFF81AC8CA5B00EB0EF8 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 0AAFCFFA1AC8CDFE00EB0EF8 /* icon-1024.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon-1024.png"; sourceTree = ""; }; 0AD7EA511AC3EA6F00617758 /* JPushSDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JPushSDK.framework; path = "Pods/../build/Debug-iphoneos/Pods/JPushSDK.framework"; sourceTree = ""; }; 0AD7EA581AC3EAF300617758 /* Yep-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Yep-Bridging-Header.h"; sourceTree = ""; }; 0AD7EA5B1AC3ECD700617758 /* libPushSDK-1.8.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPushSDK-1.8.3.a"; path = "Yep/lib/libPushSDK-1.8.3.a"; sourceTree = ""; }; @@ -175,6 +180,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0AAFCFF91AC8CA5B00EB0EF8 /* MobileCoreServices.framework in Frameworks */, 0AD7EA741AC3EE9600617758 /* libPushSDK-1.8.3.a in Frameworks */, 0AD7EA6E1AC3ED4A00617758 /* libz.dylib in Frameworks */, 0AD7EA6C1AC3ED4300617758 /* Security.framework in Frameworks */, @@ -239,6 +245,7 @@ 0A8F54AD1AB67D11004AD60E /* Yep */ = { isa = PBXGroup; children = ( + 0AAFCFFA1AC8CDFE00EB0EF8 /* icon-1024.png */, 0AFE75AE1AC43DE0005AA33E /* Crashlytics.framework */, 0A8F54B01AB67D11004AD60E /* AppDelegate.swift */, 0AD7EA581AC3EAF300617758 /* Yep-Bridging-Header.h */, @@ -597,6 +604,8 @@ 67B708B60AAD1DEA82A6A682 /* Frameworks */ = { isa = PBXGroup; children = ( + 0AAFCFF81AC8CA5B00EB0EF8 /* MobileCoreServices.framework */, + 0AAFCFF61AC8C75900EB0EF8 /* AWSS3.framework */, 0AD7EA6D1AC3ED4A00617758 /* libz.dylib */, 0AD7EA6B1AC3ED4300617758 /* Security.framework */, 0AD7EA691AC3ED3D00617758 /* UIKit.framework */, @@ -710,6 +719,7 @@ 0A8F54B61AB67D11004AD60E /* Main.storyboard in Resources */, 5033159D1AC101C80008A209 /* ChatRightTextCell.xib in Resources */, 50E114FD1ABC549300F13000 /* ContactsCell.xib in Resources */, + 0AAFCFFB1AC8CDFE00EB0EF8 /* icon-1024.png in Resources */, 502AE54F1AB93175005BD199 /* ProfileHeaderCell.xib in Resources */, 0A8F54BB1AB67D11004AD60E /* LaunchScreen.xib in Resources */, 0A8F54B81AB67D11004AD60E /* Images.xcassets in Resources */, @@ -976,6 +986,7 @@ FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Yep", + "$(PROJECT_DIR)/build/Debug-iphoneos/Pods", ); GCC_OPTIMIZATION_LEVEL = 3; INFOPLIST_FILE = Yep/Info.plist; @@ -1000,6 +1011,7 @@ FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Yep", + "$(PROJECT_DIR)/build/Debug-iphoneos/Pods", ); INFOPLIST_FILE = Yep/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; diff --git a/Yep/Services/YepNetworking.swift b/Yep/Services/YepNetworking.swift index d1515081..c41f8ffb 100644 --- a/Yep/Services/YepNetworking.swift +++ b/Yep/Services/YepNetworking.swift @@ -7,6 +7,7 @@ // import Foundation +import MobileCoreServices.UTType public enum Method: String, Printable { case OPTIONS = "OPTIONS" @@ -234,3 +235,84 @@ public func jsonResource(#token: String?, #path: String, #method: Method, #re return Resource(path: path, method: method, requestBody: jsonBody, headers: headers, parse: jsonParse) } + +// MARK: Upload + +extension NSMutableData { + + /// Append string to NSMutableData + /// + /// Rather than littering my code with calls to `dataUsingEncoding` to convert strings to NSData, and then add that data to the NSMutableData, this wraps it in a nice convenient little extension to NSMutableData. This converts using UTF-8. + /// + /// :param: string The string to be added to the `NSMutableData`. + + func appendString(string: String) { + let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) + appendData(data!) + } +} + +/// Create body of the multipart/form-data request +/// +/// :param: parameters The optional dictionary containing keys and values to be passed to web service +/// :param: filePathKey The optional field name to be used when uploading files. If you supply paths, you must supply filePathKey, too. +/// :param: paths The optional array of file paths of the files to be uploaded +/// :param: boundary The multipart/form-data boundary +/// +/// :returns: The NSData of the body of the request + +func createBodyWithParameters(parameters: [String: String]?, #filePathKey: String?, #paths: [String]?, #boundary: String) -> NSData { + let body = NSMutableData() + + if parameters != nil { + for (key, value) in parameters! { + body.appendString("--\(boundary)\r\n") + body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") + body.appendString("\(value)\r\n") + } + } + + if paths != nil { + for path in paths! { + let filename = path.lastPathComponent + let data = NSData(contentsOfFile: path) + let mimetype = mimeTypeForPath(path) + + body.appendString("--\(boundary)\r\n") + body.appendString("Content-Disposition: form-data; name=\"\(filePathKey!)\"; filename=\"\(filename)\"\r\n") + body.appendString("Content-Type: \(mimetype)\r\n\r\n") + body.appendData(data!) + body.appendString("\r\n") + } + } + + body.appendString("--\(boundary)--\r\n") + return body +} + +/// Create boundary string for multipart/form-data request +/// +/// :returns: The boundary string that consists of "Boundary-" followed by a UUID string. + +func generateBoundaryString() -> String { + return "Boundary-\(NSUUID().UUIDString)" +} + +/// Determine mime type on the basis of extension of a file. +/// +/// This requires MobileCoreServices framework. +/// +/// :param: path The path of the file for which we are going to determine the mime type. +/// +/// :returns: Returns the mime type if successful. Returns application/octet-stream if unable to determine mime type. + +func mimeTypeForPath(path: String) -> String { + let pathExtension = path.pathExtension + + if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() { + if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { + return mimetype as String + } + } + return "application/octet-stream"; +} diff --git a/Yep/Services/YepStorageService.swift b/Yep/Services/YepStorageService.swift index 886c6423..e299ee4a 100644 --- a/Yep/Services/YepStorageService.swift +++ b/Yep/Services/YepStorageService.swift @@ -8,7 +8,105 @@ import Foundation -func uploadFileToAWSS3(fileUrl: NSURL) -> String { +/// Create request +/// +/// :param: userid The userid to be passed to web service +/// :param: password The password to be passed to web service +/// :param: email The email address to be passed to web service +/// +/// :returns: The NSURLRequest that was created + +func createRequest(#url:NSURL, #filePath: String, #key: String, #acl: String, #algorithm: String, #signature: String, #date: String, #credential: String, #policy: String) -> NSURLRequest { + let param = [ + "key" : key, + "acl" : acl, + "X-Amz-Algorithm": algorithm, + "X-Amz-Signature": signature, + "X-Amz-Date": date, + "X-Amz-Credential": credential, + "Policy": policy, + ] // build your dictionary however appropriate + + let boundary = generateBoundaryString() + let request = NSMutableURLRequest(URL: url) + request.HTTPMethod = "POST" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + request.HTTPBody = createBodyWithParameters(param, filePathKey: "file", paths: [filePath], boundary: boundary) + + return request +} + +func uploadFileToAWSS3(#filePath: String, #dataForm: JSONDictionary) -> String { + + let policy = dataForm["policy"] as! [String: AnyObject] + let conditions = policy["conditions"] as! [[String: AnyObject]] + let signature = dataForm["signature"] as! String + let policyEncoded = dataForm["encoded_policy"] as! String + let url = dataForm["url"] as! String + + var key = "" + var acl = "" + var algorithm = "" + var credential = "" + var date = "" + var bucket = "" + + + for dict in conditions { + let keyString = dict.keys.first! + switch keyString{ + case "key": + key = dict.values.first as! String + case "acl": + acl = dict.values.first as! String + case "x-amz-algorithm": + algorithm = dict.values.first as! String + case "x-amz-credential": + credential = dict.values.first as! String + case "x-amz-date": + date = dict.values.first as! String + default: + break + } + } + + + var request = createRequest(url:NSURL(string: url)!, filePath:filePath , key: key, acl: acl, algorithm: algorithm, signature: signature, date: date, credential: credential, policy: policyEncoded) + + let task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { + data, response, error in + + if error != nil { + // handle error here + return + } + + // if response was JSON, then parse it + + var parseError: NSError? + let responseObject: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &parseError) + + if let responseDictionary = responseObject as? NSDictionary { + // handle the parsed dictionary here + println("did upload \(responseDictionary)") + } else { + println("upload error") + // handle parsing error here + } + + // if response was text or html, then just convert it to a string + // + // let responseString = NSString(data: data, encoding: NSUTF8StringEncoding) + // println("responseString = \(responseString)") + + // note, if you want to update the UI, make sure to dispatch that to the main queue, e.g.: + // + // dispatch_async(dispatch_get_main_queue()) { + // // update your UI and model objects here + // } + }) + task.resume() return "" } diff --git a/Yep/ViewControllers/Conversation/ConversationViewController.swift b/Yep/ViewControllers/Conversation/ConversationViewController.swift index 2d76e359..73b1cce8 100644 --- a/Yep/ViewControllers/Conversation/ConversationViewController.swift +++ b/Yep/ViewControllers/Conversation/ConversationViewController.swift @@ -143,6 +143,11 @@ class ConversationViewController: UIViewController { println("\(error)") }, completion: { token in println("Got S3 token is \(token)") + + let filePath = NSBundle.mainBundle().pathForResource("icon-1024", ofType: "png")! + let form = token["options"] as! JSONDictionary + uploadFileToAWSS3(filePath: filePath, dataForm: form) + }) } } diff --git a/Yep/icon-1024.png b/Yep/icon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..d0e3531327dd925afc3f897dcde72a252f9183d8 GIT binary patch literal 54960 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 Date: Mon, 30 Mar 2015 20:21:34 +0800 Subject: [PATCH 3/4] Make S3 Work --- Podfile | 2 + Podfile.lock | 26 +++ Yep.xcodeproj/project.pbxproj | 14 +- Yep/{icon-1024.png => 1.png} | Bin Yep/Base.lproj/Intro.storyboard | 8 +- Yep/Services/YepNetworking.swift | 84 +-------- Yep/Services/YepStorageService.swift | 171 +++++++++++++----- .../ConversationViewController.swift | 2 +- 8 files changed, 174 insertions(+), 133 deletions(-) rename Yep/{icon-1024.png => 1.png} (100%) diff --git a/Podfile b/Podfile index 72a0d185..d883629f 100644 --- a/Podfile +++ b/Podfile @@ -2,6 +2,8 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' use_frameworks! +pod 'AFNetworking' +pod 'Ono', '~> 1.2' pod 'MZFayeClient', :path => '../CatchLib-iOS/MZFayeClient/' pod 'JPushSDK' pod 'Realm' diff --git a/Podfile.lock b/Podfile.lock index 3118e9fe..ab389a55 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,17 +1,41 @@ PODS: + - AFNetworking (2.5.2): + - AFNetworking/NSURLConnection (= 2.5.2) + - AFNetworking/NSURLSession (= 2.5.2) + - AFNetworking/Reachability (= 2.5.2) + - AFNetworking/Security (= 2.5.2) + - AFNetworking/Serialization (= 2.5.2) + - AFNetworking/UIKit (= 2.5.2) + - AFNetworking/NSURLConnection (2.5.2): + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/NSURLSession (2.5.2): + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/Reachability (2.5.2) + - AFNetworking/Security (2.5.2) + - AFNetworking/Serialization (2.5.2) + - AFNetworking/UIKit (2.5.2): + - AFNetworking/NSURLConnection + - AFNetworking/NSURLSession - Base64 (1.0.1) - JPushSDK (1.8.2) - MZFayeClient (1.0.0): - Base64 (~> 1.0.1) - SocketRocket (~> 0.3.1-beta2) + - Ono (1.2.0) - Realm (0.91.1): - Realm/Headers (= 0.91.1) - Realm/Headers (0.91.1) - SocketRocket (0.3.1-beta2) DEPENDENCIES: + - AFNetworking - JPushSDK - MZFayeClient (from `../CatchLib-iOS/MZFayeClient/`) + - Ono (~> 1.2) - Realm EXTERNAL SOURCES: @@ -19,9 +43,11 @@ EXTERNAL SOURCES: :path: "../CatchLib-iOS/MZFayeClient/" SPEC CHECKSUMS: + AFNetworking: fefbce9660acb17f48ae0011292d4da0f457bf36 Base64: 4924bf3ca6fa559a5161ef717291bd450eb7bd1a JPushSDK: c68dd04c595a5c93aa003f212974010790410d8e MZFayeClient: 19df756a4c86c8f414668e6edc54fccfee54adbb + Ono: f8c2b610f28f40d411b0d75130e77fd78b92e4e1 Realm: b9c1fd9defa26aa7276ce938a8df52ec81f24528 SocketRocket: 7284ab9370a06c99aba92b2fe3a32aedd0f9a6fa diff --git a/Yep.xcodeproj/project.pbxproj b/Yep.xcodeproj/project.pbxproj index ebe60131..ea011b04 100644 --- a/Yep.xcodeproj/project.pbxproj +++ b/Yep.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0A02E46F1AC9208A00235DBF /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A02E46E1AC9208A00235DBF /* libxml2.dylib */; }; 0A5D83941AC5D3C8000045BF /* YepTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5D83931AC5D3C8000045BF /* YepTabBarController.swift */; }; 0A5D83991AC5D91E000045BF /* ContactsSearchTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5D83981AC5D91E000045BF /* ContactsSearchTableViewController.swift */; }; 0A8F54B11AB67D11004AD60E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8F54B01AB67D11004AD60E /* AppDelegate.swift */; }; @@ -16,7 +17,7 @@ 0A8F54C71AB67D11004AD60E /* YepTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8F54C61AB67D11004AD60E /* YepTests.swift */; }; 0A944C251AC8BB2F00037A06 /* YepStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A944C241AC8BB2F00037A06 /* YepStorageService.swift */; }; 0AAFCFF91AC8CA5B00EB0EF8 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AAFCFF81AC8CA5B00EB0EF8 /* MobileCoreServices.framework */; }; - 0AAFCFFB1AC8CDFE00EB0EF8 /* icon-1024.png in Resources */ = {isa = PBXBuildFile; fileRef = 0AAFCFFA1AC8CDFE00EB0EF8 /* icon-1024.png */; }; + 0AAFCFFB1AC8CDFE00EB0EF8 /* 1.png in Resources */ = {isa = PBXBuildFile; fileRef = 0AAFCFFA1AC8CDFE00EB0EF8 /* 1.png */; }; 0AD7EA5E1AC3ED1200617758 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA5D1AC3ED1200617758 /* CFNetwork.framework */; }; 0AD7EA601AC3ED1900617758 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA5F1AC3ED1900617758 /* CoreFoundation.framework */; }; 0AD7EA621AC3ED2100617758 /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD7EA611AC3ED2100617758 /* CoreTelephony.framework */; }; @@ -91,6 +92,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0A02E46C1AC9205000235DBF /* KissXML.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = KissXML.framework; path = "Pods/../build/Debug-iphoneos/Pods/KissXML.framework"; sourceTree = ""; }; + 0A02E46E1AC9208A00235DBF /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; }; 0A5D83931AC5D3C8000045BF /* YepTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = YepTabBarController.swift; path = ViewControllers/TabBar/YepTabBarController.swift; sourceTree = ""; }; 0A5D83981AC5D91E000045BF /* ContactsSearchTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContactsSearchTableViewController.swift; path = ViewControllers/Contacts/Search/ContactsSearchTableViewController.swift; sourceTree = ""; }; 0A8F54AB1AB67D11004AD60E /* Yep.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Yep.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -105,7 +108,7 @@ 0A944C241AC8BB2F00037A06 /* YepStorageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = YepStorageService.swift; path = Services/YepStorageService.swift; sourceTree = ""; }; 0AAFCFF61AC8C75900EB0EF8 /* AWSS3.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AWSS3.framework; path = "Pods/../build/Debug-iphoneos/Pods/AWSS3.framework"; sourceTree = ""; }; 0AAFCFF81AC8CA5B00EB0EF8 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; - 0AAFCFFA1AC8CDFE00EB0EF8 /* icon-1024.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon-1024.png"; sourceTree = ""; }; + 0AAFCFFA1AC8CDFE00EB0EF8 /* 1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 1.png; sourceTree = ""; }; 0AD7EA511AC3EA6F00617758 /* JPushSDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JPushSDK.framework; path = "Pods/../build/Debug-iphoneos/Pods/JPushSDK.framework"; sourceTree = ""; }; 0AD7EA581AC3EAF300617758 /* Yep-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Yep-Bridging-Header.h"; sourceTree = ""; }; 0AD7EA5B1AC3ECD700617758 /* libPushSDK-1.8.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPushSDK-1.8.3.a"; path = "Yep/lib/libPushSDK-1.8.3.a"; sourceTree = ""; }; @@ -180,6 +183,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0A02E46F1AC9208A00235DBF /* libxml2.dylib in Frameworks */, 0AAFCFF91AC8CA5B00EB0EF8 /* MobileCoreServices.framework in Frameworks */, 0AD7EA741AC3EE9600617758 /* libPushSDK-1.8.3.a in Frameworks */, 0AD7EA6E1AC3ED4A00617758 /* libz.dylib in Frameworks */, @@ -245,7 +249,7 @@ 0A8F54AD1AB67D11004AD60E /* Yep */ = { isa = PBXGroup; children = ( - 0AAFCFFA1AC8CDFE00EB0EF8 /* icon-1024.png */, + 0AAFCFFA1AC8CDFE00EB0EF8 /* 1.png */, 0AFE75AE1AC43DE0005AA33E /* Crashlytics.framework */, 0A8F54B01AB67D11004AD60E /* AppDelegate.swift */, 0AD7EA581AC3EAF300617758 /* Yep-Bridging-Header.h */, @@ -604,6 +608,8 @@ 67B708B60AAD1DEA82A6A682 /* Frameworks */ = { isa = PBXGroup; children = ( + 0A02E46E1AC9208A00235DBF /* libxml2.dylib */, + 0A02E46C1AC9205000235DBF /* KissXML.framework */, 0AAFCFF81AC8CA5B00EB0EF8 /* MobileCoreServices.framework */, 0AAFCFF61AC8C75900EB0EF8 /* AWSS3.framework */, 0AD7EA6D1AC3ED4A00617758 /* libz.dylib */, @@ -719,7 +725,7 @@ 0A8F54B61AB67D11004AD60E /* Main.storyboard in Resources */, 5033159D1AC101C80008A209 /* ChatRightTextCell.xib in Resources */, 50E114FD1ABC549300F13000 /* ContactsCell.xib in Resources */, - 0AAFCFFB1AC8CDFE00EB0EF8 /* icon-1024.png in Resources */, + 0AAFCFFB1AC8CDFE00EB0EF8 /* 1.png in Resources */, 502AE54F1AB93175005BD199 /* ProfileHeaderCell.xib in Resources */, 0A8F54BB1AB67D11004AD60E /* LaunchScreen.xib in Resources */, 0A8F54B81AB67D11004AD60E /* Images.xcassets in Resources */, diff --git a/Yep/icon-1024.png b/Yep/1.png similarity index 100% rename from Yep/icon-1024.png rename to Yep/1.png diff --git a/Yep/Base.lproj/Intro.storyboard b/Yep/Base.lproj/Intro.storyboard index f62a5b94..22e69408 100644 --- a/Yep/Base.lproj/Intro.storyboard +++ b/Yep/Base.lproj/Intro.storyboard @@ -194,7 +194,7 @@ - + @@ -209,7 +209,7 @@ - + @@ -537,7 +537,7 @@ - + @@ -552,7 +552,7 @@ - + diff --git a/Yep/Services/YepNetworking.swift b/Yep/Services/YepNetworking.swift index c41f8ffb..13ba8003 100644 --- a/Yep/Services/YepNetworking.swift +++ b/Yep/Services/YepNetworking.swift @@ -7,7 +7,6 @@ // import Foundation -import MobileCoreServices.UTType public enum Method: String, Printable { case OPTIONS = "OPTIONS" @@ -234,85 +233,4 @@ public func jsonResource(#token: String?, #path: String, #method: Method, #re } return Resource(path: path, method: method, requestBody: jsonBody, headers: headers, parse: jsonParse) -} - -// MARK: Upload - -extension NSMutableData { - - /// Append string to NSMutableData - /// - /// Rather than littering my code with calls to `dataUsingEncoding` to convert strings to NSData, and then add that data to the NSMutableData, this wraps it in a nice convenient little extension to NSMutableData. This converts using UTF-8. - /// - /// :param: string The string to be added to the `NSMutableData`. - - func appendString(string: String) { - let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) - appendData(data!) - } -} - -/// Create body of the multipart/form-data request -/// -/// :param: parameters The optional dictionary containing keys and values to be passed to web service -/// :param: filePathKey The optional field name to be used when uploading files. If you supply paths, you must supply filePathKey, too. -/// :param: paths The optional array of file paths of the files to be uploaded -/// :param: boundary The multipart/form-data boundary -/// -/// :returns: The NSData of the body of the request - -func createBodyWithParameters(parameters: [String: String]?, #filePathKey: String?, #paths: [String]?, #boundary: String) -> NSData { - let body = NSMutableData() - - if parameters != nil { - for (key, value) in parameters! { - body.appendString("--\(boundary)\r\n") - body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") - body.appendString("\(value)\r\n") - } - } - - if paths != nil { - for path in paths! { - let filename = path.lastPathComponent - let data = NSData(contentsOfFile: path) - let mimetype = mimeTypeForPath(path) - - body.appendString("--\(boundary)\r\n") - body.appendString("Content-Disposition: form-data; name=\"\(filePathKey!)\"; filename=\"\(filename)\"\r\n") - body.appendString("Content-Type: \(mimetype)\r\n\r\n") - body.appendData(data!) - body.appendString("\r\n") - } - } - - body.appendString("--\(boundary)--\r\n") - return body -} - -/// Create boundary string for multipart/form-data request -/// -/// :returns: The boundary string that consists of "Boundary-" followed by a UUID string. - -func generateBoundaryString() -> String { - return "Boundary-\(NSUUID().UUIDString)" -} - -/// Determine mime type on the basis of extension of a file. -/// -/// This requires MobileCoreServices framework. -/// -/// :param: path The path of the file for which we are going to determine the mime type. -/// -/// :returns: Returns the mime type if successful. Returns application/octet-stream if unable to determine mime type. - -func mimeTypeForPath(path: String) -> String { - let pathExtension = path.pathExtension - - if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() { - if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { - return mimetype as String - } - } - return "application/octet-stream"; -} +} \ No newline at end of file diff --git a/Yep/Services/YepStorageService.swift b/Yep/Services/YepStorageService.swift index e299ee4a..e730bec4 100644 --- a/Yep/Services/YepStorageService.swift +++ b/Yep/Services/YepStorageService.swift @@ -7,7 +7,10 @@ // import Foundation - +import MobileCoreServices.UTType +import Ono +import AWSS3 +import AFNetworking /// Create request /// /// :param: userid The userid to be passed to web service @@ -18,26 +21,29 @@ import Foundation func createRequest(#url:NSURL, #filePath: String, #key: String, #acl: String, #algorithm: String, #signature: String, #date: String, #credential: String, #policy: String) -> NSURLRequest { let param = [ - "key" : key, - "acl" : acl, - "X-Amz-Algorithm": algorithm, - "X-Amz-Signature": signature, - "X-Amz-Date": date, - "X-Amz-Credential": credential, - "Policy": policy, + ["key" : key], + ["acl" : acl], + ["X-Amz-Algorithm": algorithm], + ["X-Amz-Signature": signature], + ["X-Amz-Date": date], + ["X-Amz-Credential": credential], + ["Policy": policy] ] // build your dictionary however appropriate let boundary = generateBoundaryString() let request = NSMutableURLRequest(URL: url) request.HTTPMethod = "POST" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - request.HTTPBody = createBodyWithParameters(param, filePathKey: "file", paths: [filePath], boundary: boundary) + request.HTTPBody = createBodyWithParameters(parameters: param, filePathKey: "file", paths: [filePath], boundary: boundary) + +// request.setValue("\(request.HTTPBody?.length)", forHTTPHeaderField: "Content-Length") return request } -func uploadFileToAWSS3(#filePath: String, #dataForm: JSONDictionary) -> String { +func uploadFileToAWSS3(#filePath: String, #dataForm: JSONDictionary){ let policy = dataForm["policy"] as! [String: AnyObject] let conditions = policy["conditions"] as! [[String: AnyObject]] @@ -50,7 +56,6 @@ func uploadFileToAWSS3(#filePath: String, #dataForm: JSONDictionary) -> String { var algorithm = "" var credential = "" var date = "" - var bucket = "" for dict in conditions { @@ -71,43 +76,127 @@ func uploadFileToAWSS3(#filePath: String, #dataForm: JSONDictionary) -> String { } } + let param = [ + "key" : key, + "acl" : acl, + "X-Amz-Algorithm": algorithm, + "X-Amz-Signature": signature, + "X-Amz-Date": date, + "X-Amz-Credential": credential, + "Policy": policyEncoded + ] - var request = createRequest(url:NSURL(string: url)!, filePath:filePath , key: key, acl: acl, algorithm: algorithm, signature: signature, date: date, credential: credential, policy: policyEncoded) +// var request = createRequest(url:NSURL(string: url)!, filePath:filePath , key: key, acl: acl, algorithm: algorithm, signature: signature, date: date, credential: credential, policy: policyEncoded) + let filename = filePath.lastPathComponent + let mimetype = mimeTypeForPath(filePath) - let task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { - data, response, error in +// YepAWSService.uploadFiletoAWS(filePath, filename: filename, mimetype: mimetype,withKey:key, acl: acl, algorithm: algorithm, signature: signature, date: date, credential: credential, policy: policyEncoded) + + let request = AFHTTPRequestSerializer().multipartFormRequestWithMethod("POST", URLString: url, parameters: param, constructingBodyWithBlock: { formData in - if error != nil { - // handle error here - return + formData.appendPartWithFileURL(NSURL(fileURLWithPath: filePath)!, name: "file", fileName: filename, mimeType: mimetype, error: nil) + + }, error: nil) + + let manager = AFURLSessionManager(sessionConfiguration: NSURLSessionConfiguration.defaultSessionConfiguration()) + manager.responseSerializer = AFXMLParserResponseSerializer() +// var progress = NSProgress() + + var uploadTask = manager.uploadTaskWithStreamedRequest(request, progress: nil, completionHandler: { (response, responseObject, error) in + + if (error != nil) { + println("Error \(error.description)") + }else { + println("Upload \(response) \(responseObject)") } - // if response was JSON, then parse it - var parseError: NSError? - let responseObject: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &parseError) - - if let responseDictionary = responseObject as? NSDictionary { - // handle the parsed dictionary here - println("did upload \(responseDictionary)") - } else { - println("upload error") - // handle parsing error here - } - - // if response was text or html, then just convert it to a string - // - // let responseString = NSString(data: data, encoding: NSUTF8StringEncoding) - // println("responseString = \(responseString)") - - // note, if you want to update the UI, make sure to dispatch that to the main queue, e.g.: - // - // dispatch_async(dispatch_get_main_queue()) { - // // update your UI and model objects here - // } }) - task.resume() - return "" + + uploadTask.resume() +} + +// MARK: Upload + +extension NSMutableData { + + /// Append string to NSMutableData + /// + /// Rather than littering my code with calls to `dataUsingEncoding` to convert strings to NSData, and then add that data to the NSMutableData, this wraps it in a nice convenient little extension to NSMutableData. This converts using UTF-8. + /// + /// :param: string The string to be added to the `NSMutableData`. + + func appendString(string: String) { + let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) + appendData(data!) + } +} + + +/// Create body of the multipart/form-data request +/// +/// :param: parameters The optional dictionary containing keys and values to be passed to web service +/// :param: filePathKey The optional field name to be used when uploading files. If you supply paths, you must supply filePathKey, too. +/// :param: paths The optional array of file paths of the files to be uploaded +/// :param: boundary The multipart/form-data boundary +/// +/// :returns: The NSData of the body of the request + +func createBodyWithParameters(#parameters: [[String: String]]?, #filePathKey: String?, #paths: [String]?, #boundary: String) -> NSData { + let body = NSMutableData() + + if parameters != nil { + for dict in parameters! { + body.appendString("--\(boundary)\r\n") + body.appendString("Content-Disposition: form-data; name=\"\(dict.keys.first!)\"\r\n\r\n") + body.appendString("\(dict.values.first!)\r\n") + } + } + + if paths != nil { + for path in paths! { + let filename = path.lastPathComponent + let data = NSData(contentsOfFile: path) + let mimetype = mimeTypeForPath(path) + + body.appendString("--\(boundary)\r\n") + body.appendString("Content-Disposition: form-data; name=\"\(filePathKey!)\"; filename=\"\(filename)\"\r\n") + body.appendString("Content-Type: \(mimetype)\r\n\r\n") + body.appendData(data!) + body.appendString("\r\n") + } + } + + body.appendString("--\(boundary)--\r\n") + return body +} + + +/// Create boundary string for multipart/form-data request +/// +/// :returns: The boundary string that consists of "Boundary-" followed by a UUID string. + +func generateBoundaryString() -> String { + return "Boundary-\(NSUUID().UUIDString)" +} + +/// Determine mime type on the basis of extension of a file. +/// +/// This requires MobileCoreServices framework. +/// +/// :param: path The path of the file for which we are going to determine the mime type. +/// +/// :returns: Returns the mime type if successful. Returns application/octet-stream if unable to determine mime type. + +func mimeTypeForPath(path: String) -> String { + let pathExtension = path.pathExtension + + if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() { + if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { + return mimetype as String + } + } + return "application/octet-stream"; } func requestAWSS3UploadForm(#failureHandler: ((Reason, String?) -> ())?, #completion: JSONDictionary -> Void) { diff --git a/Yep/ViewControllers/Conversation/ConversationViewController.swift b/Yep/ViewControllers/Conversation/ConversationViewController.swift index 73b1cce8..a00a5290 100644 --- a/Yep/ViewControllers/Conversation/ConversationViewController.swift +++ b/Yep/ViewControllers/Conversation/ConversationViewController.swift @@ -144,7 +144,7 @@ class ConversationViewController: UIViewController { }, completion: { token in println("Got S3 token is \(token)") - let filePath = NSBundle.mainBundle().pathForResource("icon-1024", ofType: "png")! + let filePath = NSBundle.mainBundle().pathForResource("1", ofType: "png")! let form = token["options"] as! JSONDictionary uploadFileToAWSS3(filePath: filePath, dataForm: form) From 64dab355ee40e65b235af68e1f5db3547c481129 Mon Sep 17 00:00:00 2001 From: kevinzhow Date: Mon, 30 Mar 2015 22:18:09 +0800 Subject: [PATCH 4/4] Refine S3 upload --- Yep/Services/YepStorageService.swift | 188 ++++++------------ .../ConversationViewController.swift | 14 +- 2 files changed, 72 insertions(+), 130 deletions(-) diff --git a/Yep/Services/YepStorageService.swift b/Yep/Services/YepStorageService.swift index e730bec4..b6652be6 100644 --- a/Yep/Services/YepStorageService.swift +++ b/Yep/Services/YepStorageService.swift @@ -9,111 +9,54 @@ import Foundation import MobileCoreServices.UTType import Ono -import AWSS3 import AFNetworking -/// Create request -/// -/// :param: userid The userid to be passed to web service -/// :param: password The password to be passed to web service -/// :param: email The email address to be passed to web service -/// -/// :returns: The NSURLRequest that was created -func createRequest(#url:NSURL, #filePath: String, #key: String, #acl: String, #algorithm: String, #signature: String, #date: String, #credential: String, #policy: String) -> NSURLRequest { - let param = [ - ["key" : key], - ["acl" : acl], - ["X-Amz-Algorithm": algorithm], - ["X-Amz-Signature": signature], - ["X-Amz-Date": date], - ["X-Amz-Credential": credential], - ["Policy": policy] - ] // build your dictionary however appropriate - - let boundary = generateBoundaryString() - let request = NSMutableURLRequest(URL: url) - request.HTTPMethod = "POST" - - request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - - request.HTTPBody = createBodyWithParameters(parameters: param, filePathKey: "file", paths: [filePath], boundary: boundary) - -// request.setValue("\(request.HTTPBody?.length)", forHTTPHeaderField: "Content-Length") - - return request +struct S3UploadParams { + let url: String + let key: String + let acl: String + let algorithm: String + let signature: String + let date: String + let credential: String + let encodedPolicy: String } -func uploadFileToAWSS3(#filePath: String, #dataForm: JSONDictionary){ - let policy = dataForm["policy"] as! [String: AnyObject] - let conditions = policy["conditions"] as! [[String: AnyObject]] - let signature = dataForm["signature"] as! String - let policyEncoded = dataForm["encoded_policy"] as! String - let url = dataForm["url"] as! String - - var key = "" - var acl = "" - var algorithm = "" - var credential = "" - var date = "" - - - for dict in conditions { - let keyString = dict.keys.first! - switch keyString{ - case "key": - key = dict.values.first as! String - case "acl": - acl = dict.values.first as! String - case "x-amz-algorithm": - algorithm = dict.values.first as! String - case "x-amz-credential": - credential = dict.values.first as! String - case "x-amz-date": - date = dict.values.first as! String - default: - break - } - } +func uploadFileToS3WithFilePath(filePath: String, #s3UploadParams: S3UploadParams){ let param = [ - "key" : key, - "acl" : acl, - "X-Amz-Algorithm": algorithm, - "X-Amz-Signature": signature, - "X-Amz-Date": date, - "X-Amz-Credential": credential, - "Policy": policyEncoded + "key" : s3UploadParams.key, + "acl" : s3UploadParams.acl, + "X-Amz-Algorithm": s3UploadParams.algorithm, + "X-Amz-Signature": s3UploadParams.signature, + "X-Amz-Date": s3UploadParams.date, + "X-Amz-Credential": s3UploadParams.credential, + "Policy": s3UploadParams.encodedPolicy ] -// var request = createRequest(url:NSURL(string: url)!, filePath:filePath , key: key, acl: acl, algorithm: algorithm, signature: signature, date: date, credential: credential, policy: policyEncoded) let filename = filePath.lastPathComponent let mimetype = mimeTypeForPath(filePath) - -// YepAWSService.uploadFiletoAWS(filePath, filename: filename, mimetype: mimetype,withKey:key, acl: acl, algorithm: algorithm, signature: signature, date: date, credential: credential, policy: policyEncoded) - let request = AFHTTPRequestSerializer().multipartFormRequestWithMethod("POST", URLString: url, parameters: param, constructingBodyWithBlock: { formData in - + let request = AFHTTPRequestSerializer().multipartFormRequestWithMethod("POST", URLString: s3UploadParams.url, parameters: param, constructingBodyWithBlock: { formData in formData.appendPartWithFileURL(NSURL(fileURLWithPath: filePath)!, name: "file", fileName: filename, mimeType: mimetype, error: nil) - }, error: nil) let manager = AFURLSessionManager(sessionConfiguration: NSURLSessionConfiguration.defaultSessionConfiguration()) manager.responseSerializer = AFXMLParserResponseSerializer() -// var progress = NSProgress() - - var uploadTask = manager.uploadTaskWithStreamedRequest(request, progress: nil, completionHandler: { (response, responseObject, error) in + var progress: NSProgress? + + var uploadTask = manager.uploadTaskWithStreamedRequest(request, progress: &progress, completionHandler: { (response, responseObject, error) in if (error != nil) { println("Error \(error.description)") }else { println("Upload \(response) \(responseObject)") } - - - }) + }) uploadTask.resume() + } // MARK: Upload @@ -127,50 +70,12 @@ extension NSMutableData { /// :param: string The string to be added to the `NSMutableData`. func appendString(string: String) { - let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) + let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) appendData(data!) } } -/// Create body of the multipart/form-data request -/// -/// :param: parameters The optional dictionary containing keys and values to be passed to web service -/// :param: filePathKey The optional field name to be used when uploading files. If you supply paths, you must supply filePathKey, too. -/// :param: paths The optional array of file paths of the files to be uploaded -/// :param: boundary The multipart/form-data boundary -/// -/// :returns: The NSData of the body of the request - -func createBodyWithParameters(#parameters: [[String: String]]?, #filePathKey: String?, #paths: [String]?, #boundary: String) -> NSData { - let body = NSMutableData() - - if parameters != nil { - for dict in parameters! { - body.appendString("--\(boundary)\r\n") - body.appendString("Content-Disposition: form-data; name=\"\(dict.keys.first!)\"\r\n\r\n") - body.appendString("\(dict.values.first!)\r\n") - } - } - - if paths != nil { - for path in paths! { - let filename = path.lastPathComponent - let data = NSData(contentsOfFile: path) - let mimetype = mimeTypeForPath(path) - - body.appendString("--\(boundary)\r\n") - body.appendString("Content-Disposition: form-data; name=\"\(filePathKey!)\"; filename=\"\(filename)\"\r\n") - body.appendString("Content-Type: \(mimetype)\r\n\r\n") - body.appendData(data!) - body.appendString("\r\n") - } - } - - body.appendString("--\(boundary)--\r\n") - return body -} - /// Create boundary string for multipart/form-data request /// @@ -199,10 +104,49 @@ func mimeTypeForPath(path: String) -> String { return "application/octet-stream"; } -func requestAWSS3UploadForm(#failureHandler: ((Reason, String?) -> ())?, #completion: JSONDictionary -> Void) { +func s3UploadParams(#failureHandler: ((Reason, String?) -> ())?, #completion: S3UploadParams -> Void) { - let parse: JSONDictionary -> JSONDictionary? = { data in - return data + let parse: JSONDictionary -> S3UploadParams? = { data in + println("s3FormData: \(data)") + + if let options = data["options"] as? JSONDictionary { + if + let encodedPolice = options["encoded_policy"] as? String, + let key = options["key"] as? String, + let signature = options["signature"] as? String, + let urlString = options["url"] as? String, + let policy = options["policy"] as? JSONDictionary, + let conditions = policy["conditions"] as? [JSONDictionary] { + + var acl: String? + var credential: String? + var algorithm: String? + var date: String? + + for dict in conditions { + for (key, value) in dict { + switch key { + case "acl": + acl = value as? String + case "x-amz-credential": + credential = value as? String + case "x-amz-algorithm": + algorithm = value as? String + case "x-amz-date": + date = value as? String + default: + break + } + } + } + + if let acl = acl, let credential = credential, let algorithm = algorithm, let date = date { + return S3UploadParams(url: urlString, key: key, acl: acl, algorithm: algorithm, signature: signature, date: date, credential: credential, encodedPolicy: encodedPolice) + } + } + } + + return nil } let resource = authJsonResource(path: "/api/v1/attachments/s3_upload_public_form_fields", method: .GET, requestParameters:[:], parse: parse) diff --git a/Yep/ViewControllers/Conversation/ConversationViewController.swift b/Yep/ViewControllers/Conversation/ConversationViewController.swift index a00a5290..0e678ec0 100644 --- a/Yep/ViewControllers/Conversation/ConversationViewController.swift +++ b/Yep/ViewControllers/Conversation/ConversationViewController.swift @@ -139,16 +139,14 @@ class ConversationViewController: UIViewController { } messageToolbar.voiceSendAction = { messageToolbar in - requestAWSS3UploadForm(failureHandler: { (reason, error) in - println("\(error)") - }, completion: { token in - println("Got S3 token is \(token)") + s3UploadParams(failureHandler: nil) { s3UploadParams in + println("s3UploadParams: \(s3UploadParams)") let filePath = NSBundle.mainBundle().pathForResource("1", ofType: "png")! - let form = token["options"] as! JSONDictionary - uploadFileToAWSS3(filePath: filePath, dataForm: form) - - }) + //let form = token["options"] as! JSONDictionary + //uploadFileToAWSS3(filePath: filePath, dataForm: form) + uploadFileToS3WithFilePath(filePath, s3UploadParams: s3UploadParams) + } } }