1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
import Foundation import AWSCore import AWSDynamoDB import AWSS3 class AMZRemoteService { // MARK: - RemoteService Properties var hasCurrentUserIdentity: Bool { return persistentUserId != nil } var currentUser: UserData? // MARK: - Properties var persistentUserId: String? { set { NSUserDefaults.standardUserDefaults().setValue(newValue, forKey: "userId") NSUserDefaults.standardUserDefaults().synchronize() } get { return NSUserDefaults.standardUserDefaults().stringForKey("userId") } } private (set) var identityProvider: AWSCognitoCredentialsProvider? private var deviceDirectoryForUploads: NSURL? private var deviceDirectoryForDownloads: NSURL? private static var sharedInstance: AMZRemoteService? // MARK: - Functions static func defaultService() -> RemoteService { if sharedInstance == nil { sharedInstance = AMZRemoteService() sharedInstance!.configure() } return sharedInstance! } func configure() { identityProvider = AWSCognitoCredentialsProvider( regionType: AMZConstants.COGNITO_REGIONTYPE, identityPoolId: AMZConstants.COGNITO_IDENTITY_POOL_ID) let configuration = AWSServiceConfiguration( region: AMZConstants.DEFAULT_SERVICE_REGION, credentialsProvider: identityProvider) AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration // The api I am using for uploading to and downloading from S3 (AWSS3TransferManager)can not deal with NSData directly, but uses files. // I need to create tmp directories for these files. deviceDirectoryForUploads = createLocalTmpDirectory("upload") deviceDirectoryForDownloads = createLocalTmpDirectory("download") } private func createLocalTmpDirectory(let directoryName: String) -> NSURL? { do { let url = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent(directoryName) try NSFileManager.defaultManager().createDirectoryAtURL( url, withIntermediateDirectories: true, attributes: nil) return url } catch let error as NSError { print("Creating \(directoryName) directory failed. Error: \(error)") return nil } } // This is where the saving to S3 (image) and DynamoDB (data) is done. func saveAMZUser(user: AMZUser, completion: ErrorResultBlock) { precondition(user.userId != nil, "You should provide a user object with a userId when saving a user") let mapper = AWSDynamoDBObjectMapper.defaultDynamoDBObjectMapper() // We create a task that will save the user to DynamoDB // This works because AMZUser extends AWSDynamoDBObjectModel and conforms to AWSDynamoDBModeling let saveToDynamoDBTask: AWSTask = mapper.save(user) // If there is no imageData we only have to save to DynamoDB if user.imageData == nil { saveToDynamoDBTask.continueWithBlock({ (task) -> AnyObject? in completion(error: task.error) return nil }) } else { // We have to save data to DynamoDB, and the image to S3 saveToDynamoDBTask.continueWithSuccessBlock({ (task) -> AnyObject? in // An example of the AWSTask api. We return a task and continueWithBlock is called on this task. return self.createUploadImageTask(user) }).continueWithBlock({ (task) -> AnyObject? in completion(error: task.error) return nil }) } } private func createUploadImageTask(user: UserData) -> AWSTask { guard let userId = user.userId else { preconditionFailure("You should provide a user object with a userId when uploading a user image") } guard let imageData = user.imageData else { preconditionFailure("You are trying to create an UploadImageTask, but the user has no imageData") } // Save the image as a file. The filename is the userId let fileName = "\(userId).jpg" let fileURL = deviceDirectoryForUploads!.URLByAppendingPathComponent(fileName) imageData.writeToFile(fileURL.path!, atomically: true) // Create a task to upload the file let uploadRequest = AWSS3TransferManagerUploadRequest() uploadRequest.body = fileURL uploadRequest.key = fileName uploadRequest.bucket = AMZConstants.S3BUCKET_USERS let transferManager = AWSS3TransferManager.defaultS3TransferManager() return transferManager.upload(uploadRequest) } private func createDownloadImageTask(userId: String) -> AWSTask { // The location where the downloaded file has to be saved on the device let fileName = "\(userId).jpg" let fileURL = deviceDirectoryForDownloads!.URLByAppendingPathComponent(fileName) // Create a task to download the file let downloadRequest = AWSS3TransferManagerDownloadRequest() downloadRequest.downloadingFileURL = fileURL downloadRequest.bucket = AMZConstants.S3BUCKET_USERS downloadRequest.key = fileName let transferManager = AWSS3TransferManager.defaultS3TransferManager() return transferManager.download(downloadRequest) } } // MARK: - RemoteService extension AMZRemoteService: RemoteService { func createCurrentUser(userData: UserData? , completion: ErrorResultBlock ) { precondition(currentUser == nil, "currentUser should not exist when createCurrentUser(..) is called") precondition(userData == nil || userData!.userId == nil, "You can not create a user with a given userId. UserId's are assigned automatically") precondition(persistentUserId == nil, "A persistent userId should not yet exist") guard let identityProvider = identityProvider else { preconditionFailure("No identity provider available, did you forget to call configure() before using AMZRemoteService?") } // This covers the scenario that an app was deleted and later reinstalled. // The goal is to create a new identity and a new user profile for this use case. // By default, Cognito stores a Cognito identity in the keychain. // This identity survives app uninstalls, so there can be an identity left from a previous app install. // When we detect this scenario we remove all data from the keychain, so we can start from scratch. if identityProvider.identityId != nil { identityProvider.clearKeychain() assert(identityProvider.identityId == nil) } // Create a new Cognito identity let task: AWSTask = identityProvider.getIdentityId() task.continueWithBlock { (task) -> AnyObject? in if let error = task.error { completion(error: error) } else { // The new cognito identity token is now stored in the keychain. // Create a new empty user object of type AMZUser var newUser = AMZUser() // Copy the data from the parameter userData if let userData = userData { newUser.updateWithData(userData) } // create a unique ID for the new user newUser.userId = NSUUID().UUIDString // Now save the data on AWS. This will save the image on S3, the other data in DynamoDB self.saveAMZUser(newUser) { (error) -> Void in if let error = error { completion(error: error) } else { // Here we can be certain that the user was saved on AWS, so we set the local user instance self.currentUser = newUser self.persistentUserId = newUser.userId completion(error: nil) } } } return nil } } func updateCurrentUser(userData: UserData, completion: ErrorResultBlock) { guard var currentUser = currentUser else { preconditionFailure("currentUser should already exist when updateCurrentUser(..) is called") } precondition(userData.userId == nil || userData.userId == currentUser.userId, "Updating current user with a different userId is not allowed") precondition(persistentUserId != nil, "A persistent userId should exist") // create a new empty user var updatedUser = AMZUser() // apply the new userData updatedUser.updateWithData(userData) // restore the userId of the current user updatedUser.userId = currentUser.userId // If there are no changes, there is no need to update. if updatedUser.isEqualTo(currentUser) { completion(error: nil) return } self.saveAMZUser(updatedUser) { (error) -> Void in if let error = error { completion(error: error) } else { // Here we can be certain that the user was saved on AWS, so we update the local user instance. currentUser.updateWithData(updatedUser) completion(error: nil) } } } func fetchCurrentUser(completion: UserDataResultBlock ) { precondition(persistentUserId != nil, "A persistent userId should exist") // Task to download the image let downloadImageTask: AWSTask = createDownloadImageTask(persistentUserId!) // Task to fetch the DynamoDB data let mapper = AWSDynamoDBObjectMapper.defaultDynamoDBObjectMapper() let loadFromDynamoDBTask: AWSTask = mapper.load(AMZUser.self, hashKey: persistentUserId!, rangeKey: nil) // Download the image downloadImageTask.continueWithBlock { (imageTask) -> AnyObject? in var didDownloadImage = false if let error = imageTask.error { // If there is an error we will ignore it, it's not fatal. Maybe there is no user image. print("Error downloading image: \(error)") } else { didDownloadImage = true } // Download the data from DynamoDB loadFromDynamoDBTask.continueWithBlock({ (dynamoTask) -> AnyObject? in if let error = dynamoTask.error { completion(userData: nil, error: error) } else { if let user = dynamoTask.result as? AMZUser { if didDownloadImage { let fileName = "\(self.persistentUserId!).jpg" let fileURL = self.deviceDirectoryForDownloads!.URLByAppendingPathComponent(fileName) user.imageData = NSData(contentsOfURL: fileURL) } if var currentUser = self.currentUser { currentUser.updateWithData(user) } else { self.currentUser = user } completion(userData: user, error: nil) } else { // should probably never happen assertionFailure("No userData and no error, why?") completion(userData: nil, error: nil) } } return nil }) return nil } } } |