AMZRemoteService
Swift
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
        }
    }
    
}