Загрузка NSURLSession и amazon S3
У меня есть приложение, которое в настоящее время загружает изображения на амазонку S3. Я пытался переключить его с NSURLConnection на NSURLSession, чтобы загрузка могла продолжаться, пока приложение находится в фоновом режиме! Кажется, я немного задеваю проблему. NSURLRequest создается и передается NSURLSession, но amazon отправляет обратно 403-запрещенный ответ, если я передаю тот же запрос NSURLConnection, он полностью загружает файл.
Вот код, который создает ответ:
NSString *requestURLString = [NSString stringWithFormat:@"http://%@.%@/%@/%@", BUCKET_NAME, AWS_HOST, DIRECTORY_NAME, filename];
NSURL *requestURL = [NSURL URLWithString:requestURLString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL
cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
timeoutInterval:60.0];
// Configure request
[request setHTTPMethod:@"PUT"];
[request setValue:[NSString stringWithFormat:@"%@.%@", BUCKET_NAME, AWS_HOST] forHTTPHeaderField:@"Host"];
[request setValue:[self formattedDateString] forHTTPHeaderField:@"Date"];
[request setValue:@"public-read" forHTTPHeaderField:@"x-amz-acl"];
[request setHTTPBody:imageData];
И затем это знаменует ответ (я думаю, что это произошло из другого ответа SO):
NSString *contentMd5 = [request valueForHTTPHeaderField:@"Content-MD5"];
NSString *contentType = [request valueForHTTPHeaderField:@"Content-Type"];
NSString *timestamp = [request valueForHTTPHeaderField:@"Date"];
if (nil == contentMd5) contentMd5 = @"";
if (nil == contentType) contentType = @"";
NSMutableString *canonicalizedAmzHeaders = [NSMutableString string];
NSArray *sortedHeaders = [[[request allHTTPHeaderFields] allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
for (id key in sortedHeaders)
{
NSString *keyName = [(NSString *)key lowercaseString];
if ([keyName hasPrefix:@"x-amz-"]){
[canonicalizedAmzHeaders appendFormat:@"%@:%@\n", keyName, [request valueForHTTPHeaderField:(NSString *)key]];
}
}
NSString *bucket = @"";
NSString *path = request.URL.path;
NSString *query = request.URL.query;
NSString *host = [request valueForHTTPHeaderField:@"Host"];
if (![host isEqualToString:@"s3.amazonaws.com"]) {
bucket = [host substringToIndex:[host rangeOfString:@".s3.amazonaws.com"].location];
}
NSString* canonicalizedResource;
if (nil == path || path.length < 1) {
if ( nil == bucket || bucket.length < 1 ) {
canonicalizedResource = @"/";
}
else {
canonicalizedResource = [NSString stringWithFormat:@"/%@/", bucket];
}
}
else {
canonicalizedResource = [NSString stringWithFormat:@"/%@%@", bucket, path];
}
if (query != nil && [query length] > 0) {
canonicalizedResource = [canonicalizedResource stringByAppendingFormat:@"?%@", query];
}
NSString* stringToSign = [NSString stringWithFormat:@"%@\n%@\n%@\n%@\n%@%@", [request HTTPMethod], contentMd5, contentType, timestamp, canonicalizedAmzHeaders, canonicalizedResource];
NSString *signature = [self signatureForString:stringToSign];
[request setValue:[NSString stringWithFormat:@"AWS %@:%@", self.S3AccessKey, signature] forHTTPHeaderField:@"Authorization"];
Тогда, если я использую эту строку кода:
[NSURLConnection connectionWithRequest:request delegate:self];
Он работает и загружает файл, но если я использую:
NSURLSessionUploadTask *task = [self.session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:filePath]];
[task resume];
Я получаю запрещенную ошибку..!?
Кто-нибудь пытался загрузить на S3 с этим и ударил аналогичные проблемы? Интересно, связано ли это с тем, как сеанс приостанавливает и возобновляет загрузку, или он делает что-то смешное для запроса.?
Одним из возможных решений было бы загрузить файл на промежуточный сервер, который я контролирую, и передать его на S3, когда он будет завершен... но это явно не идеальное решение!
Любая помощь очень ценится!
Спасибо!
Ответы
Ответ 1
Я сделал это на основе ответа Зеева Вакса. Я хочу дать некоторое представление о проблемах, с которыми я столкнулся, и внести незначительные улучшения.
Создайте обычный PutRequest, например
S3PutObjectRequest* putRequest = [[S3PutObjectRequest alloc] initWithKey:keyName inBucket:bucketName];
putRequest.credentials = credentials;
putRequest.filename = theFilePath;
Теперь нам нужно выполнить некоторую работу, которую обычно делает для нас S3Client
// set the endpoint, so it is not null
putRequest.endpoint = s3Client.endpoint;
// if you are using session based authentication, otherwise leave it out
putRequest.securityToken = messageTokenDTO.securityToken;
// sign the request (also computes md5 checksums etc.)
NSMutableURLRequest *request = [s3Client signS3Request:putRequest];
Теперь скопируйте все это на новый запрос. Amazon использует свой собственный класс NSUrlRequest, который вызывает исключение
NSMutableURLRequest* request2 = [[NSMutableURLRequest alloc]initWithURL:request.URL];
[request2 setHTTPMethod:request.HTTPMethod];
[request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];
Теперь мы можем начать фактическую передачу
NSURLSession* backgroundSession = [self backgroundSession];
_uploadTask = [backgroundSession uploadTaskWithRequest:request2 fromFile:[NSURL fileURLWithPath:theFilePath]];
[_uploadTask resume];
Это код, создающий фоновый сеанс:
- (NSURLSession *)backgroundSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.example.my.unique.id"];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session;
}
Мне потребовалось некоторое время, чтобы понять, что делегат сессии/задачи должен обрабатывать вызов auth (мы на самом деле являемся аутентификацией для s3). Поэтому просто реализуйте
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
NSLog(@"session did receive challenge");
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
Ответ 2
Ответ здесь немного устарел, потратил большой мой день, пытаясь получить эту работу в Swift и новом AWS SDK. Итак, вот как это сделать в Swift, используя новый AWSS3PreSignedURLBuilder
(доступный в версии 2.0.7 +):
class S3BackgroundUpload : NSObject {
// Swift doesn't support static properties yet, so have to use structs to achieve the same thing.
struct Static {
static var session : NSURLSession?
}
override init() {
super.init()
// Note: There are probably safer ways to store the AWS credentials.
let configPath = NSBundle.mainBundle().pathForResource("appconfig", ofType: "plist")
let config = NSDictionary(contentsOfFile: configPath!)
let accessKey = config.objectForKey("awsAccessKeyId") as String?
let secretKey = config.objectForKey("awsSecretAccessKey") as String?
let credentialsProvider = AWSStaticCredentialsProvider .credentialsWithAccessKey(accessKey!, secretKey: secretKey!)
// AWSRegionType.USEast1 is the default S3 endpoint (use it if you don't need specific endpoints such as s3-us-west-2.amazonaws.com)
let configuration = AWSServiceConfiguration(region: AWSRegionType.USEast1, credentialsProvider: credentialsProvider)
// This is setting the configuration for all AWS services, you can also pass in this configuration to the AWSS3PreSignedURLBuilder directly.
AWSServiceManager.defaultServiceManager().setDefaultServiceConfiguration(configuration)
if Static.session == nil {
let configIdentifier = "com.example.s3-background-upload"
var config : NSURLSessionConfiguration
if NSURLSessionConfiguration.respondsToSelector("backgroundSessionConfigurationWithIdentifier:") {
// iOS8
config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configIdentifier)
} else {
// iOS7
config = NSURLSessionConfiguration.backgroundSessionConfiguration(configIdentifier)
}
// NSURLSession background sessions *need* to have a delegate.
Static.session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
}
}
func upload() {
let s3path = "/some/path/some_file.jpg"
let filePath = "/var/etc/etc/some_file.jpg"
// Check if the file actually exists to prevent weird uncaught obj-c exceptions.
if NSFileManager.defaultManager().fileExistsAtPath(filePath) == false {
NSLog("file does not exist at %@", filePath)
return
}
// NSURLSession needs the filepath in a "file://" NSURL format.
let fileUrl = NSURL(string: "file://\(filePath)")
let preSignedReq = AWSS3GetPreSignedURLRequest()
preSignedReq.bucket = "bucket-name"
preSignedReq.key = s3path
preSignedReq.HTTPMethod = AWSHTTPMethod.PUT // required
preSignedReq.contentType = "image/jpeg" // required
preSignedReq.expires = NSDate(timeIntervalSinceNow: 60*60) // required
// The defaultS3PreSignedURLBuilder uses the global config, as specified in the init method.
let urlBuilder = AWSS3PreSignedURLBuilder.defaultS3PreSignedURLBuilder()
// The new AWS SDK uses BFTasks to chain requests together:
urlBuilder.getPreSignedURL(preSignedReq).continueWithBlock { (task) -> AnyObject! in
if task.error != nil {
NSLog("getPreSignedURL error: %@", task.error)
return nil
}
var preSignedUrl = task.result as NSURL
NSLog("preSignedUrl: %@", preSignedUrl)
var request = NSMutableURLRequest(URL: preSignedUrl)
request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalCacheData
// Make sure the content-type and http method are the same as in preSignedReq
request.HTTPMethod = "PUT"
request.setValue(preSignedReq.contentType, forHTTPHeaderField: "Content-Type")
// NSURLSession background session does *not* support completionHandler, so don't set it.
let uploadTask = Static.session?.uploadTaskWithRequest(request, fromFile: fileUrl)
// Start the upload task:
uploadTask?.resume()
return nil
}
}
}
extension S3BackgroundUpload : NSURLSessionDelegate {
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
NSLog("did receive data: %@", NSString(data: data, encoding: NSUTF8StringEncoding))
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
NSLog("session did complete")
if error != nil {
NSLog("error: %@", error!.localizedDescription)
}
// Finish up your post-upload tasks.
}
}
Ответ 3
Я еще не знаю NSURLSessionUploadTask
, но могу сказать, как отлаживать это.
Я хотел бы использовать инструмент, например Charles, чтобы видеть запросы HTTP (S), которые делает мое приложение. Вероятно, проблема состоит в том, что NSURLSessionUploadTask
игнорирует заголовок, который вы установили, или использует другой метод HTTP, чем Amazon S3 ожидает загрузки файла. Это легко проверить с помощью прокси-сервера.
Кроме того, когда Amazon S3 возвращает ошибку, например, 403, она фактически отправляет обратно XML-документ, содержащий дополнительную информацию об ошибке. Может быть, есть метод делегата для NSURLSession
, который может получить тело ответа? Если нет, то Чарльз, несомненно, даст вам более глубокое понимание.
Ответ 4
Вот мой код для запуска задачи:
AmazonS3Client *s3Client = [[AmazonS3Client alloc] initWithAccessKey:accessKey withSecretKey:secretKey];
S3PutObjectRequest *s3PutObjectRequest = [[S3PutObjectRequest alloc] initWithKey:[url lastPathComponent] inBucket:bucket];
s3PutObjectRequest.cannedACL = [S3CannedACL publicRead];
s3PutObjectRequest.endpoint = s3Client.endpoint;
s3PutObjectRequest.contentType = fileMIMEType([url absoluteString]);
[s3PutObjectRequest configureURLRequest];
NSMutableURLRequest *request = [s3Client signS3Request:s3PutObjectRequest];
NSMutableURLRequest *request2 = [[NSMutableURLRequest alloc]initWithURL:request.URL];
[request2 setHTTPMethod:request.HTTPMethod];
[request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];
NSURLSessionUploadTask *task = [[self backgroundURLSession] uploadTaskWithRequest:request2 fromFile:url];
[task resume];
Я открываю исходный загруженный фоновый файл S3 https://github.com/genadyo/S3Uploader/
Ответ 5
Для загрузки/загрузки фона вам необходимо использовать NSURLSession с настройкой фона.
Начиная с AWS SDK 2.0.7 вы можете использовать предварительно подписанные запросы:
Предзарегистрированный URL Builder ** - SDK теперь включает поддержку предварительно подписанных URL-адреса Amazon Simple Storage Service (S3). Вы можете использовать эти URLS для выполнять фоновые передачи с использованием класса NSURLSession.
Инициализировать фон NSURLSession и AWS Services
- (void)initBackgroundURLSessionAndAWS
{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:AWSS3BackgroundSessionUploadIdentifier];
self.urlSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
AWSServiceConfiguration *configuration = [AWSServiceConfiguration configurationWithRegion:DefaultServiceRegionType credentialsProvider:credentialsProvider];
[AWSServiceManager defaultServiceManager].defaultServiceConfiguration = configuration;
self.awss3 = [[AWSS3 alloc] initWithConfiguration:configuration];
}
Реализация функции загрузки файлов
- (void)uploadFile
{
AWSS3GetPreSignedURLRequest *getPreSignedURLRequest = [AWSS3GetPreSignedURLRequest new];
getPreSignedURLRequest.bucket = @"your_bucket";
getPreSignedURLRequest.key = @"your_key";
getPreSignedURLRequest.HTTPMethod = AWSHTTPMethodPUT;
getPreSignedURLRequest.expires = [NSDate dateWithTimeIntervalSinceNow:3600];
//Important: must set contentType for PUT request
getPreSignedURLRequest.contentType = @"your_contentType";
[[[AWSS3PreSignedURLBuilder defaultS3PreSignedURLBuilder] getPreSignedURL:getPreSignedURLRequest] continueWithBlock:^id(BFTask *task) {
if (task.error)
{
NSLog(@"Error BFTask: %@", task.error);
}
else
{
NSURL *presignedURL = task.result;
NSLog(@"upload presignedURL is: \n%@", presignedURL);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:presignedURL];
request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
[request setHTTPMethod:@"PUT"];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
// Background NSURLSessions do not support the block interfaces, delegate only.
NSURLSessionUploadTask *uploadTask = [self.session uploadTaskWithRequest:request fromFile:@"file_path"];
[uploadTask resume];
}
return nil;
}];
}
Функция делегата NSURLSession:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error)
{
NSLog(@"S3 UploadTask: %@ completed with error: %@", task, [error localizedDescription]);
}
else
{
// AWSS3GetPreSignedURLRequest does not contain ACL property, so it has to be set after file was uploaded
AWSS3PutObjectAclRequest *aclRequest = [AWSS3PutObjectAclRequest new];
aclRequest.bucket = @"your_bucket";
aclRequest.key = @"yout_key";
aclRequest.ACL = AWSS3ObjectCannedACLPublicRead;
[[self.awss3 putObjectAcl:aclRequest] continueWithBlock:^id(BFTask *bftask) {
dispatch_async(dispatch_get_main_queue(), ^{
if (bftask.error)
{
NSLog(@"Error putObjectAcl: %@", [bftask.error localizedDescription]);
}
else
{
NSLog(@"ACL for an uploaded file was changed successfully!");
}
});
return nil;
}];
}
}
Ответ 6
Я когда-то проводил это и, наконец, добился успеха. Лучший способ - использовать AWS-библиотеку для создания запроса с подписанными заголовками и скопировать запрос. Крайне важно копировать запрос, поскольку NSURLSessionTask потерпит неудачу с другой стороны. В приведенном ниже примере кода я использовал AFNetworking и подклассифицированный AFHTTPSessionManager, но этот код также работает с NSURLSession.
@implementation MyAFHTTPSessionManager
{
}
static MyAFHTTPSessionManager *sessionManager = nil;
+ (instancetype)manager {
if (!sessionManager)
sessionManager = [[MyAFHTTPSessionManager alloc] init];
return sessionManager;
}
- (id)init {
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:toutBackgroundSessionNameAF];
sessionConfiguration.timeoutIntervalForRequest = 30;
sessionConfiguration.timeoutIntervalForResource = 300;
self = [super initWithSessionConfiguration:sessionConfiguration];
if (self)
{
}
return self;
}
- (NSURLSessionDataTask *)POSTDataToS3:(NSURL *)fromFile
Key:(NSString *)key
completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
S3PutObjectRequest *s3Request = [[S3PutObjectRequest alloc] initWithKey:key inBucket:_s3Bucket];
s3Request.cannedACL = [S3CannedACL publicReadWrite];
s3Request.securityToken = [CTUserDefaults awsS3SessionToken];
[s3Request configureURLRequest];
NSMutableURLRequest *request = [_s3Client signS3Request:s3Request];
// For some reason, the signed S3 request comes back with '(null)' as a host.
NSString *urlString = [NSString stringWithFormat:@"%@/%@/%@", _s3Client.endpoint, _s3Bucket, [key stringWithURLEncoding]] ;
request.URL = [NSURL URLWithString:urlString];
// Have to create a new request and copy all the headers otherwise the NSURLSessionDataTask will fail (since request get a pointer back to AmazonURLRequest which is a subclass of NSMutableURLRequest)
NSMutableURLRequest *request2 = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
[request2 setHTTPMethod:@"PUT"];
[request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];
NSURLSessionDataTask *task = [self uploadTaskWithRequest:request2
fromFile:fromFile
progress:nil
completionHandler:completionHandler];
return task;
}
@end
Другим хорошим ресурсом является код примера яблока здесь и поиск "Простая передача фона"
Ответ 7
Недавно Amazon обновил там AWS api до 2.2.4.
специальность этого обновления заключается в том, что он поддерживает загрузку фона, вам не нужно использовать NSURLSession для загрузки видео довольно просто, вы можете использовать следующий исходный блок для его проверки, я протестировал против моего более ранняя версия, она на 30-40% быстрее предыдущей версии
в приложении AppDelegate.m doneFinishLaunchingWithOptions // ~ GM ~ настройка cognito для конфигураций AWS V2
AWSStaticCredentialsProvider *staticProvider = [[AWSStaticCredentialsProvider alloc] initWithAccessKey:@"xxxx secretKey:@"xxxx"];
AWSServiceConfiguration *configuration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSWest2 credentialsProvider:staticProvider];
AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = configuration;
в методе handleEventsForBackgroundURLSession
[AWSS3TransferUtility interceptApplication:application
handleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
в классе загрузки
NSURL *fileURL = // The file to upload.
AWSS3TransferUtilityUploadExpression *expression = [AWSS3TransferUtilityUploadExpression new];
expression.uploadProgress = ^(AWSS3TransferUtilityTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
dispatch_async(dispatch_get_main_queue(), ^{
// Do something e.g. Update a progress bar.
});
};
AWSS3TransferUtilityUploadCompletionHandlerBlock completionHandler = ^(AWSS3TransferUtilityUploadTask *task, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
// Do something e.g. Alert a user for transfer completion.
// On failed uploads, `error` contains the error object.
});
};
AWSS3TransferUtility *transferUtility = [AWSS3TransferUtility defaultS3TransferUtility];
[[transferUtility uploadFile:fileURL
bucket:@"YourBucketName"
key:@"YourObjectKeyName"
contentType:@"text/plain"
expression:expression
completionHander:completionHandler] continueWithBlock:^id(AWSTask *task) {
if (task.error) {
NSLog(@"Error: %@", task.error);
}
if (task.exception) {
NSLog(@"Exception: %@", task.exception);
}
if (task.result) {
AWSS3TransferUtilityUploadTask *uploadTask = task.result;
// Do something with uploadTask.
}
return nil;
}];
Дополнительные ссылки: https://aws.amazon.com/blogs/mobile/amazon-s3-transfer-utility-for-ios/