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 *now = [[[NSDate date] description] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
132 NSString *urlString = [account.filesURL description];
133 if (account.sharingAccount) {
134 NSRange authUserRange = [urlString rangeOfString:account.username];
135 urlString = [NSString stringWithFormat:@"%@%@", [urlString substringToIndex:authUserRange.location], account.sharingAccount];
138 NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@?format=json%@&now=%@",
141 account.shared ? @"&shared=" : @"",
144 return [OpenStackRequest request:account method:method url:url];
148 #pragma mark Auth Retry
150 - (void)authRetrySucceded:(OpenStackRequest *)retryRequest {
151 self.account.authToken = [[retryRequest responseHeaders] objectForKey:@"X-Auth-Token"];
152 [self.account persist];
154 // try the original request again!
156 retriedRequest = [self copy];
158 [retriedRequest addRequestHeader:@"X-Auth-Token" value:self.account.authToken];
160 if (backupCompletionBlock) {
161 [retriedRequest setCompletionBlock:^{
162 backupCompletionBlock();
165 if (backupFailureBlock) {
166 [retriedRequest setFailedBlock:^{
167 backupFailureBlock();
171 [retriedRequest startSynchronous];
174 - (void)authRetryFailed:(OpenStackRequest *)retryRequest {
175 // if it fails due to bad connection, try again?
176 NSNotification *notification = [NSNotification notificationWithName:[self.account.manager notificationName:@"authRetryFailed" identifier:0] object:nil userInfo:[NSDictionary dictionaryWithObject:retryRequest forKey:@"request"]];
177 [[NSNotificationCenter defaultCenter] postNotification:notification];
181 #pragma mark ASIHTTPRequest Overrides
183 - (void)failWithError:(NSError *)theError {
184 if (responseStatusCode == 401 && ![url isEqual:account.provider.authEndpointURL]) {
185 // auth is expired, so get a fresh token
186 if (account && ![account.provider isGRNet]) {
187 OpenStackRequest *retryRequest = [OpenStackRequest authenticationRequest:account];
188 retryRequest.delegate = self;
189 retryRequest.didFinishSelector = @selector(authRetrySucceded:);
190 retryRequest.didFailSelector = @selector(authRetryFailed:);
191 [retryRequest startSynchronous];
193 } else if (responseStatusCode == 503) {
194 NSNotification *notification = [NSNotification notificationWithName:@"serviceUnavailable" object:nil userInfo:nil];
195 [[NSNotificationCenter defaultCenter] postNotification:notification];
196 // [super failWithError:theError];
197 } else if (responseStatusCode == 0) {
198 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
199 if (![defaults boolForKey:@"already_failed_on_connection"]) {
200 UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Connection Error" message:@"Please check your connection or API URL and try again." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
204 [defaults setBool:YES forKey:@"already_failed_on_connection"];
205 [defaults synchronize];
208 [super failWithError:theError];
211 #pragma mark - Authentication
213 + (OpenStackRequest *)authenticationRequest:(OpenStackAccount *)account {
215 OpenStackRequest *request = [[[OpenStackRequest alloc] initWithURL:account.provider.authEndpointURL] autorelease];
216 request.account = account;
217 [request addRequestHeader:@"X-Auth-User" value:account.username];
218 if (account.apiKey) {
219 [request addRequestHeader:@"X-Auth-Key" value:account.apiKey];
221 [request addRequestHeader:@"X-Auth-Key" value:@""];
228 #pragma mark Object Storage Requests
230 + (OpenStackRequest *)getStorageAccountInfoRequest:(OpenStackAccount *)account {
231 return [OpenStackRequest filesRequest:account method:@"HEAD" path:@""];
234 + (OpenStackRequest *)getContainersRequest:(OpenStackAccount *)account {
235 return [OpenStackRequest filesRequest:account method:@"GET" path:@""];
238 - (NSMutableDictionary *)containers {
239 SBJSON *parser = [[SBJSON alloc] init];
240 NSArray *jsonObjects = [parser objectWithString:[self responseString]];
241 NSMutableDictionary *objects = [NSMutableDictionary dictionaryWithCapacity:[jsonObjects count]];
243 for (int i = 0; i < [jsonObjects count]; i++) {
244 NSDictionary *dict = [jsonObjects objectAtIndex:i];
245 Container *container = [Container fromJSON:dict];
246 [objects setObject:container forKey:container.name];
253 + (OpenStackRequest *)createContainerRequest:(OpenStackAccount *)account container:(Container *)container {
254 return [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]];
257 + (OpenStackRequest *)deleteContainerRequest:(OpenStackAccount *)account container:(Container *)container {
258 return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]];
261 + (OpenStackRequest *)getObjectsRequest:(OpenStackAccount *)account container:(Container *)container {
262 return [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]];
265 - (NSMutableDictionary *)objects {
266 SBJSON *parser = [[SBJSON alloc] init];
267 NSArray *jsonObjects = [parser objectWithString:[self responseString]];
269 NSMutableDictionary *objects = [[[NSMutableDictionary alloc] initWithCapacity:[jsonObjects count]] autorelease];
271 for (int i = 0; i < [jsonObjects count]; i++) {
272 NSDictionary *dict = [jsonObjects objectAtIndex:i];
273 StorageObject *object = [StorageObject fromJSON:dict];
274 [objects setObject:object forKey:object.name];
281 + (OpenStackRequest *)getContainerInfoRequest:(OpenStackAccount *)account container:(Container *)container {
282 return [OpenStackRequest filesRequest:account method:@"HEAD" path:[NSString stringWithFormat:@"/%@",[NSString encodeToPercentEscape:container.name]]];
285 + (OpenStackRequest *)getObjectInfoRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
286 NSString *objectFullPath = object.fullPath;
287 if ([objectFullPath hasPrefix:@"/"])
288 objectFullPath = [objectFullPath substringFromIndex:1];
289 return [OpenStackRequest filesRequest:account method:@"HEAD" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:objectFullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
292 + (OpenStackRequest *)getObjectInfoRequest:(OpenStackAccount *)account
293 container:(Container *)container
294 object:(StorageObject *)object
295 version:(NSString *)version {
296 OpenStackRequest *request = [OpenStackRequest getObjectInfoRequest:account container:container object:object];
297 request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=%@",request.url.description, version]];
301 + (OpenStackRequest *)getObjectVersionsRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
302 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
304 request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=list",request.url.description]];
308 + (OpenStackRequest *)getObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
309 return [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
312 + (OpenStackRequest *)getObjectRequest:(OpenStackAccount *)account
313 container:(Container *)container
314 object:(StorageObject *)object
315 version:(NSString *)version {
316 OpenStackRequest *request = [OpenStackRequest getObjectRequest:account container:container object:object];
318 request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=%@",request.url.description, version]];
323 + (OpenStackRequest *)writeObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
324 NSString *fullPath = object.fullPath;
325 if ([fullPath characterAtIndex:0] == '/') {
326 fullPath = [fullPath substringFromIndex:1];
329 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
332 [request.requestHeaders setObject:object.sharing forKey:@"X-Object-Sharing"];
334 NSString *metadataKeyHeaderPrefix;
335 if ([fullPath length] == 0)
336 metadataKeyHeaderPrefix = @"X-Container-Meta-";
338 metadataKeyHeaderPrefix = @"X-Object-Meta-";
340 for (NSString *metadataKey in object.metadata) {
341 NSString *metadataKeyHeader = [NSString stringWithFormat:@"%@%@", metadataKeyHeaderPrefix, metadataKey];
342 metadataKeyHeader = [NSString encodeToPercentEscape:metadataKeyHeader];
343 NSString *metadataValue = [NSString encodeToPercentEscape:[object.metadata objectForKey:metadataKey]];
344 [request.requestHeaders setObject:metadataValue forKey:metadataKeyHeader];
347 [request setPostBody:[NSMutableData dataWithData:object.data]];
348 [request.requestHeaders setObject:object.contentType forKey:@"Content-Type"];
352 + (OpenStackRequest *)writeObjectMetadataRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
353 NSString *fullPath = object.fullPath;
354 if ([fullPath length] != 0 && [fullPath characterAtIndex:0] == '/') {
355 fullPath = [fullPath substringFromIndex:1];
358 NSString *metadataKeyHeaderPrefix;
359 if ([fullPath length] == 0)
360 metadataKeyHeaderPrefix = @"X-Container-Meta-";
362 metadataKeyHeaderPrefix = @"X-Object-Meta-";
364 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"POST" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
366 for (NSString *metadataKey in object.metadata) {
367 NSString *metadataKeyHeader = [NSString stringWithFormat:@"%@%@", metadataKeyHeaderPrefix, metadataKey];
368 metadataKeyHeader = [NSString encodeToPercentEscape:metadataKeyHeader];
369 NSString *metadataValue = [NSString encodeToPercentEscape:[object.metadata objectForKey:metadataKey]];
370 [request.requestHeaders setObject:metadataValue forKey:metadataKeyHeader];
372 if (!account.sharingAccount) {
373 NSString *objectIsPublic = ([object.publicURI length] > 0) ? @"true" : @"false";
374 [request.requestHeaders setObject:objectIsPublic forKey:@"X-Object-Public"];
376 if (object.sharing) {
377 NSString *urlEncodedSharingString = [NSString encodeToPercentEscape:object.sharing];
378 [request.requestHeaders setObject:urlEncodedSharingString forKey:@"X-Object-Sharing"];
384 + (OpenStackRequest *)deleteObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object {
385 if ([object.fullPath characterAtIndex:0] == '/') {
386 return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
388 return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]];
392 + (OpenStackRequest *)writeContainerPolicyRequest:(OpenStackAccount *)account container:(Container *)container {
393 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]];
395 [request.requestHeaders setObject:container.versioning forKey:@"X-Container-Policy-Versioning"];
396 [request.requestHeaders setObject:[NSString stringWithFormat:@"%u", container.quota] forKey:@"X-Container-Policy-Quota"];
401 + (OpenStackRequest *)writeAccountMetadataRequest:(OpenStackAccount *)account withAccountInfo:(NSDictionary *)accountInfo {
402 OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"POST" path:@""];
404 NSMutableDictionary *groups = [accountInfo objectForKey:@"groups"];
405 for (NSString *groupName in groups) {
406 NSString *group = [NSString encodeToPercentEscape:[groups objectForKey:groupName]];
407 groupName = [NSString encodeToPercentEscape:groupName];
408 [request.requestHeaders setObject:group forKey:[NSString stringWithFormat:@"X-Account-Group-%@", groupName]];
410 if ([groups count] == 0)
411 [request.requestHeaders setObject:@"" forKey:@"X-Account-Group-group"];
413 NSMutableDictionary *accountMetadata = [accountInfo objectForKey:@"metadata"];
414 for (NSString *metadataKey in accountMetadata) {
415 NSString *metadataValue = [NSString encodeToPercentEscape:[accountMetadata objectForKey:metadataKey]];
416 metadataKey = [NSString encodeToPercentEscape:[accountMetadata objectForKey:metadataKey]];
417 [request.requestHeaders setObject:metadataValue forKey:[NSString stringWithFormat:@"X-Account-Meta-%@",metadataKey]];
424 #pragma mark Memory Management
426 - (void)releaseBackupBlocksOnMainThread {
427 NSMutableArray *blocks = [NSMutableArray array];
428 if (backupCompletionBlock) {
429 [blocks addObject:backupCompletionBlock];
430 [backupCompletionBlock release];
431 backupCompletionBlock = nil;
433 if (backupFailureBlock) {
434 [blocks addObject:backupFailureBlock];
435 [backupFailureBlock release];
436 backupFailureBlock = nil;
438 [[self class] performSelectorOnMainThread:@selector(releaseBackupBlocks:) withObject:blocks waitUntilDone:[NSThread isMainThread]];
441 // Always called on main thread
442 + (void)releaseBackupBlocks:(NSArray *)blocks {
443 // Blocks will be released when this method exits
448 [errorAlerter release];
449 [self releaseBackupBlocksOnMainThread];