Uploading to s3, with success callback on iOS - ios

This question perhaps can be trivial, be as I am starting with IOS I'm not sure on what to search for.
I have a backend which handles processing of images, and with browser-based uploads, the images get upload directly to a temp folder in the app's bucket in S3, once done, I send the url to the backend for processing of the image in a background worker.
On IOS, I don't understand how and who is responsible for getting the url of the just-uploaded file to the server or to the app along with some meta data (which user uploaded the file, etc)
Looking at an S3 SDK for mobile example https://aws.amazon.com/articles/3002109349624271 I don't see that this is included.
Is that possible? Is that a common practice to do (get the url back from S3 and send it to the server along with meta data)?

The sample code from link in question.
S3PutObjectRequest *putObjectRequest = [[[S3PutObjectRequest alloc] initWithKey:MY_PICTURE_NAME inBucket:MY_PICTURE_BUCKET] autorelease];
putObjectRequest.contentType = #"image/jpeg";
putObjectRequest.data = imageData;
// Now we need to get the response and check the error which was not in the sample code given in the link
S3PutObjectResponse *putObjectResponse = [s3 putObject:putObjectRequest];
if (!putObjectResponse.error)
{
// Send the KEY to server.
}
Let me know if you have any questions.

Related

How to handle CloudFront Signed Cookie with .m3u8 files on iOS?

I have an App which is playing audio files stored on AWS.
The audio content is secured by AWS CloudFront signed cookies functionality. I had no problem in creating the signed cookies and set them in the original HTTP request using AVURLAsset, and everything works just fine for .mp3 content. Nevertheless, when accessing .m3u8 files, I get a 403 HTTP error. I noticed that the initial request is ok and the .m3u8 file is downloaded correctly, but the subsequent requests (for the audio fragments) do not work and receive a 403 as the cookie is not sent.
I already tried using the NSHTTPCookieStorage, but it did not work ;-(
NSURL *url = [NSURL URLWithString: #"http://..../stream.m3u8"]
// Get the Cookie Storage
NSHTTPCookieStorage *cookiesStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
// Create the array to store the cookies
NSMutableArray *cookies = [NSMutableArray array];
// Create the Cloud-Front-Policy Cookie
NSHTTPCookie *cloudFrontPolicyCookie = ...
[cookies addObject:cloudFrontPolicyCookie];
// Create the Cloud-Front-Signature Cookie
NSHTTPCookie *cloudFrontSignatureCookie = ...
[cookies addObject:cloudFrontSignatureCookie];
// Create the Cloud-Front-Key-Paid-Id Cookie
NSHTTPCookie *cloudFrontKeyPairIdCookie = ...
[cookies addObject:cloudFrontKeyPairIdCookie];
// Create the HTTP Header Dictionary
NSMutableDictionary * headers = [NSMutableDictionary dictionary];
// I omitted the cookie creation, but it is ok! I tested using curl on the command line
NSString *cookieAsHeader = ...
// Set the header
[headers setObject:cookieAsHeader forKey:#"Cookie"];
// Create the AVURLAsset so that I can use the headers and the cookies
// Notice that I tried using only the headers (which works)!
AVURLAsset * asset = [AVURLAsset URLAssetWithURL:url options:#{#"AVURLAssetHTTPHeaderFieldsKey": headers, AVURLAssetHTTPCookiesKey : [cookiesStorage cookies] }];
// For secured .mp3 files, it works just fine
// but for .m3u8, the content does not play as the first file part receives 403. Notice that the first request (for the .m3u8) works just fine.
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
You need to generate the cookies, unfortunately aws ios sdk doesn't have this function. My temporary solution for now is one the the following:
1) Ask web server to generate the cookies (using aws php sdk). In this way you can set timeout in a few minutes or anytime you desire, so you can keep the data safe.
2) Hardcoded the cookies inside the code. You can generate it locally using aws php sdk, and then set duration as long as possible. Maybe a year or more depend on your condition. However if someone can see it, your data will be exposed.
3) Create our own cookies generator function in swift/obj-c. We can use aws php sdk for reference.
It looks like would be a good way to handle it: https://github.com/ykying/Swift_Player_AWS_Cookie
You will likely need to generate the cookies on whatever server you have written for serving content and whenever the user opens the app or logs in, give them the cookies with a 24 hour expiration (or however long your current session TTL is).
I hope this helps someone else, we're going to try this out in our app as well.

iOS: JSON upload progress

I need to upload a big json (containing a UIImage as base64) file to a server and would like to track progress. I tried with Alamofire, but it seems as the file is uploaded first and only the response is progress-able. I can't use multipart as the API does not support that. Is it even possible to get the upload progress when only a body-json is sent?
It seems to work with upload method and json converted to data. Used some random image and mock api for testing.
let imageData = UIImagePNGRepresentation(UIImage(named: "test")!)!
let json = ["test": imageData.base64EncodedString(options: .lineLength64Characters)]
let jsonData = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
self.sessionManager.upload(jsonData!, to: "https://demo1752132.mockable.io/test", method: .post).responseJSON { dataResponse in
}.uploadProgress { progress in
print("Upload Progress: \(progress.fractionCompleted)")
}
As I tried this already the answer to this is NO. I have succeeded with files progress whereas for body-json I couldn't able to track the progress.
I will try to explain my scenario which is may be similar to you. I have to upload multiple images with text on stream. For us, the API for media is different. I mean for each image I have to upload to server and get the response. In the final request, I have to send all collected urls along with text in body of json. In this scenario, I tracked all images uploading successfully and showed it well. When coming to the final request, I couldn't able to track it.
Will be happy to know If someone finds tracking it.

How do I use an iOS app's bundle identifier to 'authorize' upload to Google Cloud Storage?

Our service is using Google App Engine as our backend, and we're now implementing an upload-function for images etc.
Using the answers from several different questions here on stack, I have made it working, but not completely as I want. We are not using the built-in OAuth etc, and for now we want the storage to be public, but not entirely public. We would like to limit it to users of our own app (I.E no authentication). In the Cloud-console we can create an API-key for iOS. When doing this, we copy the API-key to the app, and pass it along with every upload-request. This is currently working, when the bucket-permission is set to allUsers - WRITE
However, inside the API-key, we can supply our app's own Bundle Identifier, so that, supposedly, only requests from our app is allowed. (App Store ID/URL is also permitted, apparently).
Adding this bundle-id does nothing as long as the bucket has the permission allUsers - WRITE. If I change the bundle-id to not match the actual bundle-id, it still works. So which permission should it use for the bucket to make the bundle-id in the API-key apply? And what should be sent along in the upload-code on iOS (acl?)?.
If I remove the allUsers-permission, and use something else, I get this error when trying to upload:
{message:"There is a per-IP or per-Referer restriction configured
on your API key and the request does not match these
restrictions. Please use the Google Developers Console
to update your API key configuration if request from this
IP or referer should be allowed." data:[1] code:403}}
This is how I'm using it right now (though I have tried several different things, all picked up from different questions/answers):
GTLServiceStorage *serv = [[GTLServiceStorage alloc] init];
serv.additionalHTTPHeaders = [NSDictionary dictionaryWithObjectsAndKeys:
#"[my project id]", #"x-goog-project-id",
#"application/json-rpc", #"Content-Type",
#"application/json-rpc", #"Accept", nil];
serv.APIKey = #"[my iOS API key, gotten from console, (linked to bundle-id?)]";
serv.retryEnabled = YES;
GTLStorageBucket *bucket = [[GTLStorageBucket alloc] init];
bucket.name = #"[my bucket]";
GTLUploadParameters *params = [GTLUploadParameters uploadParametersWithFileHandle:fileHandle MIMEType:#"image/jpeg"];
GTLStorageObject *storageObject = [[GTLStorageObject alloc] init];
storageObject.name = #"testFile.jpg";
//I have no idea what I'm doing with the following stuff, but I've tried several things:
GTLStorageObjectAccessControl *objAccessControl
= [GTLStorageObjectAccessControl new];
//This is working
objAccessControl.entity = #"allUsers";
objAccessControl.email = #"[my app-id]#project.gserviceaccount.com";
objAccessControl.role = #"OWNER";
//If I try this instead, it is not working.
//objAccessControl.domain = #"[my app-id].apps.googleusercontent.com";
//objAccessControl.role = #"WRITER";
//Probably because it's bullshit, I have no idea what I'm doing.
storageObject.acl = #[objAccessControl];
[...] //Bucket and upload and stuff. It seems like it's the ACL-thing above that's not working..
It seems like I have to connect the permissions on the bucket to the iOS API Key somehow, but I don't know if it's even possible.
What I want: All users to be able to use the cloud, given that they are requesting it from my iOS app.
As this question never got an answer I'll add one here, based on the information currently in the post.
The reason you got the error 'There is a per-IP or per-Referer restriction..' when calling the GCS API with the iOS API Key is simply because the GCS API doesn't work with API Keys for private data, only Bearer Tokens (ie. using OAuth). There isn't anything you could have done to make the API Key work with the GCS API directly with private data. The reason it worked when you had 'allUsers - WRITE' set as the ACL is simply because that ACL allows public access.
To access the private data without user intervention requires a Service Account, however the Google APIs Objective-C Client only supports OAuth2 Client IDs. The rationale being that Service Accounts are intended for server-side authentication only. Using a Service Account in a client would involve distributing the private key along with the app, which could easily be compromised. For reference, here's a sample of how you might authorize the GCS service using OAuth:
NSString *keychainItemName = #"My App";
NSString *clientID = <your-client-id>;
NSString *clientSecret = <your-client-secret>;
// How to check for existing credentials in the keychain
GTMOAuth2Authentication *auth;
auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:kKeychainItemName
clientID:clientID
clientSecret:clientSecret];
...
// How to set up a window controller for sign-in
NSBundle *frameworkBundle = [NSBundle bundleForClass:[GTMOAuth2WindowController class]];
GTMOAuth2WindowController *windowController;
windowController = [GTMOAuth2WindowController controllerWithScope:kGTLAuthScopeStorageDevstorageFullControl
clientID:clientID
clientSecret:clientSecret
keychainItemName:kKeychainItemName
resourceBundle:frameworkBundle];
[windowController signInSheetModalForWindow:[self window]
completionHandler:^(GTMOAuth2Authentication *auth,
NSError *error) {
if (error == nil) {
self.storageService.authorizer = auth;
}
}];
...
// Initialize service with auth
GTLServiceStorage *serv = [[GTLServiceStorage alloc] init];
serv.authorizer = auth;
All of this was taken from the storage sample on the google-api-objectivec-client GitHub page, so you can refer to it for a complete example with context.
This still leaves the question of how to implement access to GCS from iOS without user authorization. The short answer to this is that the iOS Key can be used to restrict access to your own backend API hosted on Google Cloud Endpoints, and that backend application can authorize against GCS using a Service Account (usually the Application Default Service Account). The Cloud Storage Client Library Examples page has samples using the default credentials for different languages.
Further details on how to implement an Endpoints API for this purpose are probably getting outside of the scope of this question, but this should serve as a good starting point.

How do I upload files to Google Cloud Storage in Objective-C/iOS?

I've been looking for documentation on how to upload files to my 'bucket' in Google Cloud Storage from my iOS app, but I can't find anything at all on the subject. No documentations, no tutorials, no example projects. Am I completely blind? I am simply trying to find a way for my app-users to upload a file to a "public bucket", and get a URL in return. All I can find is chunks of HTTP-protocols or JSON etc, and I have no idea how to use that, but there's no reference to it either. It feels like the author of those documentations expects me to know everything already. I've found some OSX-example codes, but they are too without documentation, and I've been trying to read the code they have provided, but with no luck.
What I'm looking for is something like this:
(This code is made up. It's what I want. I noticed Google used the prefix GTL* for their classes)
NSData *dataToUpload = ... ; //Or UIImage or some movie-format or whatever
NSURL *destination;
GTLStorageUploader *uploader = [GTLStorageUploader alloc]initWithBucket:#"myBucket" withHashOrKeyOrSomething:#"a1b2c3hashkeyOrWhatever"];
destination = [uploader uploadData:dataToUpload];//inBackground etc..
It's actually easier than this when using Parse.com, but there's simply not enough storage space for my app there, so I need to be able to upload the data files to Google Cloud Storage. How?
I did eventually get this to work. It wasn't pretty, though.
It's quite a long time ago now, so I can't really remember all the logic etc, but I'll post what made it work, and change the ID-stuff. I hope it helps, and sorry I didn't remember to write this when I found out and it was fresh in mind. I don't have time to get into this at the moment.
An important note: I also had to change a lot of permissions on the bucket and on the users/authorized on GoogleCloudStorage to make this work. We tried so many different combinations, but I THINK this was the stuff we did:
On each bucket: "Allow everyone to upload/delete/edit etc".
On the auth for the entire CloudStorage: "Allow only entities with certain accessToken to access this CloudStorage.
Allow only www.yourAppEngineURL.com to request such an accessToken.
This felt wrong, and still does. If anyone gets a hold of this accessToken, they can do whatever they want as long as that accessToken is valid. Like.. delete all files. But that was the only way we could make it work. Of course, only authorized users could request this accessToken from OUR appEngine, but still.. meh. I'm no security-guru, and this was just a fun project, so we let it go. Now to the code.
When uploading:
GTLServiceStorage *serv = [[GTLServiceStorage alloc] init];
serv.additionalHTTPHeaders = [NSDictionary dictionaryWithObjectsAndKeys:
#"123123123", #"x-goog-project-id",
#"application/json-rpc", #"Content-Type",
#"application/json-rpc", #"Accept", nil];
GTLStorageObjectAccessControl *objAccessControl = [GTLStorageObjectAccessControl new];
objAccessControl.entity = #"project-owners-123456"; //Some value from the control panel on CloudStorage or something. Or Apps.. Or AppEngine, not sure.
//Probably connected to the accessToken my AppEngine requests and sends to users.
objAccessControl.role = #"OWNER";
GTLStorageObjectAccessControl *objAccessControl2 = [GTLStorageObjectAccessControl new];
objAccessControl2.entity = #"allUsers";
objAccessControl2.role = #"READER";
//Don't remember why I need both. Or what they do. Hey ho.
//It looks like.. Everybody can read. Only my authorized accessToken-people can write? probably.
GTLStorageBucket *bucket = [[GTLStorageBucket alloc] init];
bucket.name = #"my_bucket";
NSError *err;
NSFileHandle *fileHandle = [NSFileHandle myLocalFileURLiThink error:&err];
if(err)
{
NSLog(#"Some error here");
}
GTLUploadParameters *params = [GTLUploadParameters uploadParametersWithFileHandle:fileHandle MIMEType:#"video/mp4 (or something else)"];
GTLStorageObject *storageObject = [[GTLStorageObject alloc] init];
storageObject.acl = #[objAccessControl, objAccessControl2];
NSString *key = [NSString stringWithFormat:#"fileName.mp4"];
// should probably be unique.. The url will be cloud.com/my_bucket/fileName.mp4
//You can generate something unique by putting together the user's userID and the timeStamp for the dateTime right now.
//This user will never upload two things within this second.
storageObject.name = key;
After this point in the code I do some magic I'm not gonna post. I ask for and receive an accessToken to use on GoogleCloudStorage from our own API.
I don't remember where or how I got that token to begin with, but I believe that the backend (AppEngine) had to request it from the CloudStorage-thing, using a pretty standard call.
And, as I said in the beginning, we changed some settings on CloudStorage making our AppEngine the only entity allowed to request this token. Or something.. This token has a lifecycle of like.. 15 minutes or so.. I don't know, it's provided by some Google-default-thing. I might look into it later if any of you need it.
NSString *receivedAccessToken = #"abc123"; //Received from CloudStorage via AppEngine.
NSString *accessToken = #"Bearer %#", receivedAccessToken" //(pseudo) Because it needed to say "Bearer " first. Don't know why, or how I found out..
[serv setAdditionalHTTPHeaders:[NSDictionary dictionaryWithObject:accessToken forKey:#"Authorization"]];
//Upload-magic:
GTLQueryStorage *query = [GTLQueryStorage queryForObjectsInsertWithObject:storageObject bucket:bucket.name uploadParameters:params];
GTLServiceTicket *t = [serv executeQuery:query completionHandler:^(GTLServiceTicket *ticket, id object, NSError *error) {
//Handle error first.
NSLog(#"Success!");
dispatch_async(dispatch_get_main_queue(), ^{
actualURL = [NSString stringWithFormat:#"%#%#", yourFullBucketURL /*( e.g www.googleCloud.com/my_bucket)*/, key /*file.mp4*/];
});
}];
And you can track the progress of the upload after this block, with the ticket-object (like, t.uploadProgressBlock = ...).
This code has been edited quite a bit for Stack-purposes now, so I might have screwed up something, so I probably doesn't work exactly like this. But read it, and try to understand how it works. Good luck. If you have the option, stay with Amazon or something else, this was not fun to work with. Worst documentation ever. Also, Amazon's S3 uploads/downloads faster than GoogleCloudStorage. I regret changing from Amazon to Google. Amazon had so much better API too, almost like the one in my question.
Here's the code used by AppEngine to request the AccessToken:
private GoogleCredential getGoogleCredential(String scope) throws GeneralSecurityException, IOException
{
JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId(Constants.SERVICE_ACCOUNT_EMAIL)
.setServiceAccountPrivateKeyFromP12File(new File(Constants.CLOUD_STORAGE_KEY))
.setServiceAccountScopes(Collections.singleton(scope))
.build();
return credential;
}
The parameter "scope" sent in to this method is either https://www.googleapis.com/auth/devstorage.read_only or https://www.googleapis.com/auth/devstorage.read_write
The method returns a GoogleCredential-object, on which you can call googleCredential.refreshToken(). I believe this is the call made to get the token. I'm not sure though.
The Constants (email and key) are stuff you need to set up and get from the auth-page on Google Cloud Storage, I think.
This documentation should cover some of it (it looks more documented now than it did then, I think): https://cloud.google.com/storage/docs/authentication

Survey application with offline capabilities and how to capture the data from multiple ipads to one?

I am trying to build a Survey application where the surveys will be taken offline on multiple ipads and when these ipads are online they are going to upload the data(survey answers) to our servers? I am really struggling how to send the survey to multiple ipads and more importantly to capture from different ipads to one source ?
I need help to clear my architecture part and I need some examples to do the coding part. Do you know anything similar?
What are you ideas?
Many Thanks in advance,
Arda
Create a web server to accept and send survey questions and answers.
I would envision an app that goes like this:
1) Slave iPads makes a HTTP POST request to server asking for the survey
This is usually done using a networking library for iOS like MKNetworkKit or AFNetworking. The general process is to:
create a NSDictionary of key-value pairs to form the HTTP POST request
submit the data through a block construct with completion handler
So something like:
MKNetworkOperation *op = [engine operationWithURLString:#"http://www.mywebserver.com/api/fetchQuestions"
params:nil
httpMethod:#"POST"];
2) Server receives request, grabs all survey questions in database and return JSON encoded questions to slave ipads.
I'm not sure what platform your web server is on but in the past, I used Symfony 2.0 which is a PHP web framework.
It provides very helpful tools like Doctrine (an Object Relational Mapper or ORM) to let me work with my MySQL data as if they're programming objects.
So my general process for fetching data would be something like:
// pseudo php function codes
public function sendSurveyQuestionAction()
{
$repository = $this->getDoctrine()->getRepository('MyAppBundle:Survey');
$query = $repository->createQueryBuilder('query')->getQuery();
$arrObjs = $query->getResult();
$arrObjDatas = NULL;
foreach($arrObjs as $obj)
{
$arrObjDatas[] = $obj->toArray();
}
$response = new Response(json_encode(array('data' => $arrObjDatas)));
$response->headers->set('Content-Type', 'application/json');
$return $response;
}
This would return all survey in JSON format, ready to be parsed by your master iPad app.
3) Users on slave iPads fill in the questions through the app UI and submits. The app saves
the data to disk, checks for a working internet connection before sending data back to server.
Submitting the answer is very similar grabbing the questions, so your iOS code should be something like:
// ------------------------------------------------------------------------------------
// store all question-answers into a dictionary to be submitted as HTTP POST variables
// obviously, you wouldn't create it here, this is just example code, you would likely
// have stored your questions and answers when user presses 'finish' button
// ------------------------------------------------------------------------------------
NSMutableDictionary *paramDictionary = [[NSMutableDictionary alloc] init];
[paramDictionary setObject:#"5" forKey:#"q1"];
[paramDictionary setObject:#"10" forKey:#"q2"];
[paramDictionary setObject:#"15" forKey:#"q3"];
// this helps your web server know how many question-answers to expect, or you could hard code it into your business logic
[paramDictionary setObject:[NSNumber numberWithInteger:3] forKey:#"numberOfQA"];
MKNetworkOperation *op = [engine operationWithURLString:#"http://www.mywebserver.com/api/submitAnswers"
params:paramDictionary
httpMethod:#"POST"];
This will submit your answers for each of your question. You may have noticed I used q1, q2, q3.
These are for your web server code to identify each questions and extract the respective answers from them.
4) Server receives finished answers and commit them to database
So if you were using Symfony 2.0 PHP code, then something like:
// pseudo php function
public function saveAnswersAction()
{
$numOfQA = $_REQUEST['numberOfQA'];
for($i = 0; $i < $numOfQA; $i++)
{
// ----------------------------------------------------------------------
// looping through all the questions such as q1, q2, q3, q4, q5....
// by appending the counter variable to the question identifier
// ----------------------------------------------------------------------
$currentAnswer = $_REQUEST['q'.$i];
// use Doctrine to create new answer entities, and fill in their data
$answerEntity.answer = $currentAnswer;
$surveyEntity->addAnswerEntity($answerEntity);
// mark survey as complete so we can fetch all 'completed' surveys later
$surveyEntity.complete = true;
}
// tell Doctrine to commit changes to MySQL Database
// return HTTP OK status message
}
5) Now all that's left is for your master iPad app to make a HTTP POST request to get all surveys.
The process is the same with your iOS code making a HTTP POST requesting for all 'completed' survey entities from your web server.
The web server grabs them and return them as JSON encoded data.
Your app then receives the completed surveys with question answer like this:
surveys
{
{
questionNumber: 1,
questionAnswer: "5"
},
{
questionNumber: 2,
questionAnswer: "10"
},
{
questionNumber: 3,
questionAnswer: "15"
}
}
Now you use JSONKit to parse this JSON data. You should end up with a NSDictionary from JSONKit.
You can then go something like:
// pseudo code
-(void)displayCompletedSurveys
{
[MKNetworkOperationEngine doRequest:
...
^completionBlock {
// parse JSON data
NSDictionary *surveyData = [JSONKit dictionaryFromJSONData:data)
NSEnumerator *enumerator = [surveyData enumerator];
NSDictionary *currentQuestion = nil;
while([enumerator nextObject] != nil)
{
// do something with each of your question-answer e.g. show it on screen
}
}];
}
Points To Consider
Most of the code above are pseudo-codes. Your final real code would probably be much more in depth.
You'll need to build some master login into your app to prevent everyone from seeing the completed surveys.
Some Extra Information You Should Know
Here are some extra information to help you
JSONKit for fast JSON data decoding from your web server
MKNetworking or AFNetworking to submit your data to your web server
You need to know how to write web services to handle accepting the survey answers. I recommend learning a web framework like Symfony 2.0
Hope that helps.

Resources