5 // Created by Mike Mayo on 10/8/10.
6 // The OpenStack project is provided under the Apache 2.0 license.
9 #import "OpenStackRequest.h"
11 #import "OpenStackAccount.h"
14 #import "StorageObject.h"
16 #import "AccountManager.h"
17 #import "APICallback.h"
18 #import "APILogEntry.h"
19 #import "NSString+Conveniences.h"
21 static NSRecursiveLock *accessDetailsLock = nil;
23 @implementation OpenStackRequest
25 @synthesize account, callback, retriedCount, errorAlerter;
28 return (200 <= [self responseStatusCode]) && ([self responseStatusCode] <= 299);
31 - (void)notify:(NSString *)name {
32 NSDictionary *callbackUserInfo = [NSDictionary dictionaryWithObject:self forKey:@"response"];
33 NSNotification *notification = [NSNotification notificationWithName:name object:nil userInfo:callbackUserInfo];
34 [[NSNotificationCenter defaultCenter] postNotification:notification];
38 NSString *observeName = [NSString stringWithFormat:@"%@ %@ %@", [self isSuccess] ? @"SUCCESS" : @"FAILURE", self.requestMethod, [self.url description]];
39 NSString *callbackName = [NSString stringWithFormat:@"%@ %@ %@ %@", [self isSuccess] ? @"SUCCESS" : @"FAILURE", self.requestMethod, [self.url description], self.callback.uuid];
41 NSDictionary *callbackUserInfo = [NSDictionary dictionaryWithObject:self forKey:@"response"];
43 NSNotification *observeNotification = [NSNotification notificationWithName:observeName object:nil userInfo:callbackUserInfo];
44 [[NSNotificationCenter defaultCenter] postNotification:observeNotification];
46 NSNotification *callbackNotification = [NSNotification notificationWithName:callbackName object:nil userInfo:callbackUserInfo];
47 [[NSNotificationCenter defaultCenter] postNotification:callbackNotification];
51 - (NSString *)responseString {
53 return [retriedRequest responseString];
55 return [super responseString];
59 - (NSData *)responseData {
61 return [retriedRequest responseData];
63 return [super responseData];
67 - (NSDictionary *)responseHeaders {
69 return [retriedRequest responseHeaders];
71 return [super responseHeaders];
75 - (int)responseStatusCode {
77 return [retriedRequest responseStatusCode];
79 return [super responseStatusCode];
83 - (NSString *)responseStatusMessage {
85 return [retriedRequest responseStatusMessage];
87 return [super responseStatusMessage];
91 - (void)setCompletionBlock:(ASIBasicBlock)aCompletionBlock {
92 [super setCompletionBlock:aCompletionBlock];
93 [backupCompletionBlock release];
94 backupCompletionBlock = [aCompletionBlock copy];
97 - (void)setFailedBlock:(ASIBasicBlock)aFailedBlock {
98 [super setFailedBlock:aFailedBlock];
99 [backupFailureBlock release];
100 backupFailureBlock = [aFailedBlock copy];
104 #pragma mark Generic Constructors
107 if (self == [OpenStackRequest class]) {
108 accessDetailsLock = [[NSRecursiveLock alloc] init];
112 + (id)request:(OpenStackAccount *)account method:(NSString *)method url:(NSURL *)url {
113 OpenStackRequest *request = [[[OpenStackRequest alloc] initWithURL:url] autorelease];
114 request.account = account;
115 [request setRequestMethod:method];
116 [request addRequestHeader:@"X-Auth-Token" value:[account authToken]];
117 [request addRequestHeader:@"Content-Type" value:@"application/json"];
118 [request setTimeOutSeconds:60];
119 [request setNumberOfTimesToRetryOnTimeout:5];
120 request.retriedCount = 0;
124 + (id)getSharingAccountsRequest:(OpenStackAccount *)account {
125 NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@?format=json", account.provider.authEndpointURL]];
126 return [OpenStackRequest request:account method:@"GET" url:url];
129 + (id)filesRequest:(OpenStackAccount *)account method:(NSString *)method path:(NSString *)path {
130 NSString *urlString = [account.filesURL description];
131 if (account.sharingAccount) {
132 NSRange authUserRange = [urlString rangeOfString:account.username];
133 urlString = [NSString stringWithFormat:@"%@%@", [urlString substringToIndex:authUserRange.location], account.sharingAccount];
136 NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@?format=json%@",
139 account.shared ? @"&shared=" : @""]];
141 return [OpenStackRequest request:account method:method url:url];
145 #pragma mark Auth Retry
147 - (void)authRetrySucceded:(OpenStackRequest *)retryRequest {
148 self.account.authToken = [[retryRequest responseHeaders] objectForKey:@"X-Auth-Token"];
149 [self.account persist];
151 // try the original request again!
153 retriedRequest = [self copy];
155 [retriedRequest addRequestHeader:@"X-Auth-Token" value:self.account.authToken];
157 if (backupCompletionBlock) {
158 [retriedRequest setCompletionBlock:^{
159 backupCompletionBlock();
162 if (backupFailureBlock) {
163 [retriedRequest setFailedBlock:^{
164 backupFailureBlock();
168 [retriedRequest startSynchronous];
171 - (void)authRetryFailed:(OpenStackRequest *)retryRequest {
172 // if it fails due to bad connection, try again?
173 NSNotification *notification = [NSNotification notificationWithName:[self.account.manager notificationName:@"authRetryFailed" identifier:0] object:nil userInfo:[NSDictionary dictionaryWithObject:retryRequest forKey:@"request"]];
174 [[NSNotificationCenter defaultCenter] postNotification:notification];
178 #pragma mark ASIHTTPRequest Overrides
180 - (void)failWithError:(NSError *)theError {
181 if (responseStatusCode == 401 && ![url isEqual:account.provider.authEndpointURL]) {
182 // auth is expired, so get a fresh token
183 if (account && ![account.provider isGRNet]) {
184 OpenStackRequest *retryRequest = [OpenStackRequest authenticationRequest:account];
185 retryRequest.delegate = self;
186 retryRequest.didFinishSelector = @selector(authRetrySucceded:);
187 retryRequest.didFailSelector = @selector(authRetryFailed:);
188 [retryRequest startSynchronous];
190 } else if (responseStatusCode == 503) {
191 NSNotification *notification = [NSNotification notificationWithName:@"serviceUnavailable" object:nil userInfo:nil];
192 [[NSNotificationCenter defaultCenter] postNotification:notification];
193 // [super failWithError:theError];
194 } else if (responseStatusCode == 0) {
195 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
196 if (![defaults boolForKey:@"already_failed_on_connection"]) {
197 UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Connection Error" message:@"Please check your connection or API URL and try again." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
201 [defaults setBool:YES forKey:@"already_failed_on_connection"];
202 [defaults synchronize];
205 [super failWithError:theError];
208 #pragma mark - Authentication
210 + (OpenStackRequest *)authenticationRequest:(OpenStackAccount *)account {
212 OpenStackRequest *request = [[[OpenStackRequest alloc] initWithURL:account.provider.authEndpointURL] autorelease];
213 request.account = account;
214 [request addRequestHeader:@"X-Auth-User" value:account.username];
215 if (account.authToken) {
216 [request addRequestHeader:@"X-Auth-Token" value:account.authToken];
218 [request addRequestHeader:@"X-Auth-Token" value:@""];
225 #pragma mark Object Storage Requests
227 + (OpenStackRequest *)getStorageAccountInfoRequest:(OpenStackAccount *)account {
228 return [OpenStackRequest filesRequest:account method:@"HEAD" path:@""];
231 + (OpenStackRequest *)getContainersRequest:(OpenStackAccount *)account {
232 return [OpenStackRequest filesRequest:account method:@"GET" path:@""];
235 - (NSMutableDictionary *)containers {
236 SBJSON *parser = [[SBJSON alloc] init];
237 NSArray *jsonObjects = [parser objectWithString:[self responseString]];
238 NSMutableDictionary *objects = [NSMutableDictionary dictionaryWithCapacity:[jsonObjects count]];
240 for (int i = 0; i < [jsonObjects count]; i++) {
241 NSDictionary *dict = [jsonObjects objectAtIndex:i];
242 Container *container = [Container fromJSON:dict];
243 [objects setObject:container forKey:container.name];
250 + (OpenStackRequest *)createContainerRequest:(OpenStackAccount *)account container:(Container *)container {
251 return [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]];
254 + (OpenStackRequest *)deleteContainerRequest:(OpenStackAccount *)account container:(Container *)container {
255 return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]];
258 + (OpenStackRequest *)getObjectsRequest:(OpenStackAccount *)account container:(Container *)container {
259 return [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]];
262 - (NSMutableDictionary *)objects {
263 SBJSON *parser = [[SBJSON alloc] init];
264 NSArray *jsonObjects = [parser objectWithString:[self responseString]];
266 NSMutableDictionary *objects = [[[NSMutableDictionary alloc] initWithCapacity:[jsonObjects count]] autorelease];
268 for (int i = 0; i < [jsonObjects count]; i++) {
269 NSDictionary *dict = [jsonObjects objectAtIndex:i];
270 StorageObject *object = [StorageObject fromJSON:dict];
271 [objects setObject:object forKey:object.name];
278 + (OpenStackRequest *)getContainerInfoRequest:(OpenStackAccount *)account container:(Container *)container {
279 return [OpenStackRequest filesRequest:account method:@"HEAD" path:[NSString stringWithFormat:@"/%@",[NSString encodeToPercentEscape:container.name]]];
282 + (OpenStackRequest *)getObjectInfoRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
283 NSString *objectFullPath = object.fullPath;
284 if ([objectFullPath hasPrefix:@"/"])
285 objectFullPath = [objectFullPath substringFromIndex:1];
286 return [OpenStackRequest filesRequest:account method:@"HEAD" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:objectFullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
289 + (OpenStackRequest *)getObjectInfoRequest:(OpenStackAccount *)account
290 container:(Container *)container
291 object:(StorageObject *)object
292 version:(NSString *)version {
293 OpenStackRequest *request = [OpenStackRequest getObjectInfoRequest:account container:container object:object];
294 request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=%@",request.url.description, version]];
298 + (OpenStackRequest *)getObjectVersionsRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
299 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
301 request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=list",request.url.description]];
305 + (OpenStackRequest *)getObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
306 return [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
309 + (OpenStackRequest *)getObjectRequest:(OpenStackAccount *)account
310 container:(Container *)container
311 object:(StorageObject *)object
312 version:(NSString *)version {
313 OpenStackRequest *request = [OpenStackRequest getObjectRequest:account container:container object:object];
315 request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=%@",request.url.description, version]];
320 + (OpenStackRequest *)writeObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
321 NSString *fullPath = object.fullPath;
322 if ([fullPath characterAtIndex:0] == '/') {
323 fullPath = [fullPath substringFromIndex:1];
326 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
329 [request.requestHeaders setObject:object.sharing forKey:@"X-Object-Sharing"];
331 NSString *metadataKeyHeaderPrefix;
332 if ([fullPath length] == 0)
333 metadataKeyHeaderPrefix = @"X-Container-Meta-";
335 metadataKeyHeaderPrefix = @"X-Object-Meta-";
337 for (NSString *metadataKey in object.metadata) {
338 NSString *metadataKeyHeader = [NSString stringWithFormat:@"%@%@", metadataKeyHeaderPrefix, metadataKey];
339 metadataKeyHeader = [NSString encodeToPercentEscape:metadataKeyHeader];
340 NSString *metadataValue = [NSString encodeToPercentEscape:[object.metadata objectForKey:metadataKey]];
341 [request.requestHeaders setObject:metadataValue forKey:metadataKeyHeader];
344 [request setPostBody:[NSMutableData dataWithData:object.data]];
345 [request.requestHeaders setObject:object.contentType forKey:@"Content-Type"];
349 + (OpenStackRequest *)writeObjectMetadataRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
350 NSString *fullPath = object.fullPath;
351 if ([fullPath length] != 0 && [fullPath characterAtIndex:0] == '/') {
352 fullPath = [fullPath substringFromIndex:1];
355 NSString *metadataKeyHeaderPrefix;
356 if ([fullPath length] == 0)
357 metadataKeyHeaderPrefix = @"X-Container-Meta-";
359 metadataKeyHeaderPrefix = @"X-Object-Meta-";
361 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"POST" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
363 for (NSString *metadataKey in object.metadata) {
364 NSString *metadataKeyHeader = [NSString stringWithFormat:@"%@%@", metadataKeyHeaderPrefix, metadataKey];
365 metadataKeyHeader = [NSString encodeToPercentEscape:metadataKeyHeader];
366 NSString *metadataValue = [NSString encodeToPercentEscape:[object.metadata objectForKey:metadataKey]];
367 [request.requestHeaders setObject:metadataValue forKey:metadataKeyHeader];
369 if (!account.sharingAccount) {
370 NSString *objectIsPublic = ([object.publicURI length] > 0) ? @"true" : @"false";
371 [request.requestHeaders setObject:objectIsPublic forKey:@"X-Object-Public"];
373 if (object.sharing) {
374 NSString *urlEncodedSharingString = [NSString encodeToPercentEscape:object.sharing];
375 [request.requestHeaders setObject:urlEncodedSharingString forKey:@"X-Object-Sharing"];
381 + (OpenStackRequest *)deleteObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
382 if ([object.fullPath characterAtIndex:0] == '/') {
383 return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
385 return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
389 + (OpenStackRequest *)writeContainerPolicyRequest:(OpenStackAccount *)account container:(Container *)container {
390 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]];
392 [request.requestHeaders setObject:container.versioning forKey:@"X-Container-Policy-Versioning"];
393 [request.requestHeaders setObject:[NSString stringWithFormat:@"%u", container.quota] forKey:@"X-Container-Policy-Quota"];
398 + (OpenStackRequest *)writeAccountMetadataRequest:(OpenStackAccount *)account withAccountInfo:(NSDictionary *)accountInfo {
399 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"POST" path:@""];
401 NSMutableDictionary *groups = [accountInfo objectForKey:@"groups"];
402 for (NSString *groupName in groups) {
403 NSString *group = [NSString encodeToPercentEscape:[groups objectForKey:groupName]];
404 groupName = [NSString encodeToPercentEscape:groupName];
405 [request.requestHeaders setObject:group forKey:[NSString stringWithFormat:@"X-Account-Group-%@", groupName]];
407 if ([groups count] == 0)
408 [request.requestHeaders setObject:@"" forKey:@"X-Account-Group-group"];
410 NSMutableDictionary *accountMetadata = [accountInfo objectForKey:@"metadata"];
411 for (NSString *metadataKey in accountMetadata) {
412 NSString *metadataValue = [NSString encodeToPercentEscape:[accountMetadata objectForKey:metadataKey]];
413 metadataKey = [NSString encodeToPercentEscape:[accountMetadata objectForKey:metadataKey]];
414 [request.requestHeaders setObject:metadataValue forKey:[NSString stringWithFormat:@"X-Account-Meta-%@",metadataKey]];
421 #pragma mark Memory Management
423 - (void)releaseBackupBlocksOnMainThread {
424 NSMutableArray *blocks = [NSMutableArray array];
425 if (backupCompletionBlock) {
426 [blocks addObject:backupCompletionBlock];
427 [backupCompletionBlock release];
428 backupCompletionBlock = nil;
430 if (backupFailureBlock) {
431 [blocks addObject:backupFailureBlock];
432 [backupFailureBlock release];
433 backupFailureBlock = nil;
435 [[self class] performSelectorOnMainThread:@selector(releaseBackupBlocks:) withObject:blocks waitUntilDone:[NSThread isMainThread]];
438 // Always called on main thread
439 + (void)releaseBackupBlocks:(NSArray *)blocks {
440 // Blocks will be released when this method exits
445 [errorAlerter release];
446 [self releaseBackupBlocksOnMainThread];