Initial implementation of drag and drop directory upload.
[pithos-macos] / pithos-macos / PithosFileUtilities.m
1 //
2 //  PithosFileUtilities.m
3 //  pithos-macos
4 //
5 // Copyright 2011 GRNET S.A. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
9 // conditions are met:
10 // 
11 //   1. Redistributions of source code must retain the above
12 //      copyright notice, this list of conditions and the following
13 //      disclaimer.
14 // 
15 //   2. Redistributions in binary form must reproduce the above
16 //      copyright notice, this list of conditions and the following
17 //      disclaimer in the documentation and/or other materials
18 //      provided with the distribution.
19 // 
20 // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 // 
33 // The views and conclusions contained in the software and
34 // documentation are those of the authors and should not be
35 // interpreted as representing official policies, either expressed
36 // or implied, of GRNET S.A.
37
38 #import "PithosFileUtilities.h"
39 #import "ASIPithosContainerRequest.h"
40 #import "ASIPithosObjectRequest.h"
41 #import "ASIPithosObject.h"
42 #import "HashMapHash.h"
43
44 @implementation PithosFileUtilities
45
46 #pragma mark -
47 #pragma mark Download
48
49 + (ASIPithosObjectRequest *)objectDataRequestWithContainerName:(NSString *)containerName 
50                                                     objectName:(NSString *)objectName 
51                                                    toDirectory:(NSString *)directoryPath 
52                                                  checkIfExists:(BOOL)ifExists {
53     NSString *fileName = [objectName lastPathComponent];
54     if([objectName hasSuffix:@"/"])
55         fileName = [fileName stringByAppendingString:@"/"];    
56     fileName = [fileName stringByReplacingOccurrencesOfString:@"/" withString:@":"];
57     
58     NSFileManager *defaultManager = [NSFileManager defaultManager];
59     
60     NSString *destinationPath = [directoryPath stringByAppendingPathComponent:fileName];
61     if (ifExists && [defaultManager fileExistsAtPath:destinationPath]) {
62         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
63         [alert setMessageText:@"File Exists"];
64         [alert setInformativeText:[NSString stringWithFormat:@"A file or directory named '%@' already exists, do you want to replace it?", fileName]];
65         [alert addButtonWithTitle:@"OK"];
66         [alert addButtonWithTitle:@"Cancel"];
67         NSInteger choice = [alert runModal];
68         if (choice == NSAlertSecondButtonReturn)
69             return nil;
70     }
71     
72     BOOL directoryIsDirectory;
73     BOOL directoryExists = [defaultManager fileExistsAtPath:directoryPath isDirectory:&directoryIsDirectory];
74     NSError *error = nil;
75     if (!directoryExists) {
76         [defaultManager createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&error];
77     } else if (!directoryIsDirectory) {
78         [defaultManager removeItemAtPath:directoryPath error:&error];
79     }
80     if (error) {
81         NSLog(@"Cannot remove existing file '%@': %@", fileName, error);
82         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
83         [alert setMessageText:@"Removal Error"];
84         [alert setInformativeText:[NSString stringWithFormat:@"Cannot remove existing file '%@': %@", fileName, error]];
85         [alert addButtonWithTitle:@"OK"];
86         [alert runModal];
87         return nil;
88     }
89
90     ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectDataRequestWithContainerName:containerName 
91                                                                                             objectName:objectName];
92     objectRequest.downloadDestinationPath = destinationPath;
93     objectRequest.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
94                               fileName, @"fileName", 
95                               destinationPath, @"filePath", 
96                               nil];
97     return objectRequest;
98 }
99
100 + (NSArray *)objectDataRequestsForSubdirWithContainerName:(NSString *)containerName 
101                                                objectName:(NSString *)objectName 
102                                               toDirectory:(NSString *)directoryPath 
103                                             checkIfExists:(BOOL)ifExists {
104     NSString *subdirName = [objectName lastPathComponent];
105     NSString *destinationPath = [directoryPath stringByAppendingPathComponent:subdirName];
106     if (ifExists && [[NSFileManager defaultManager] fileExistsAtPath:destinationPath]) {
107         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
108         [alert setMessageText:@"File exists"];
109         [alert setInformativeText:[NSString stringWithFormat:@"A file or directory named '%@' already exists, do you want to replace it?", subdirName]];
110         [alert addButtonWithTitle:@"OK"];
111         [alert addButtonWithTitle:@"Cancel"];
112         NSInteger choice = [alert runModal];
113         if (choice == NSAlertSecondButtonReturn)
114             return nil;
115     }
116     
117     NSMutableArray *objects = [NSMutableArray array];
118     NSString *marker = nil;
119     do {
120         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName:containerName 
121                                                                                                                limit:0 
122                                                                                                               marker:marker 
123                                                                                                               prefix:objectName 
124                                                                                                            delimiter:nil 
125                                                                                                                 path:nil 
126                                                                                                                 meta:nil 
127                                                                                                               shared:NO 
128                                                                                                                until:nil];
129         [containerRequest startSynchronous];
130         if ([containerRequest error]) {
131             NSLog(@"HTTP Request Error: %@\nResponse String: %@", [containerRequest error], [containerRequest responseString]);
132             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
133             [alert setMessageText:@"HTTP Request Error"];
134             [alert setInformativeText:[NSString stringWithFormat:@"HTTP Request Error: %@\nResponse String: %@", 
135                                        [containerRequest error], [containerRequest responseString]]];
136             [alert addButtonWithTitle:@"OK"];
137             [alert runModal];        
138             return nil;
139         }
140         NSArray *someObjects = [containerRequest objects];
141         [objects addObjectsFromArray:someObjects];
142         if ([someObjects count] < 10000)
143             marker = nil;
144         else
145             marker = [[someObjects lastObject] name];
146     } while (marker);
147     
148     NSFileManager *defaultManager = [NSFileManager defaultManager];
149     NSMutableArray *objectRequests = [NSMutableArray arrayWithCapacity:[objects count]];
150     NSUInteger subdirPrefixLength = [objectName length];
151     for (ASIPithosObject *object in objects) {
152         if (object.subdir || [object.contentType isEqualToString:@"application/directory"]) {
153             NSString *subdirDirectoryPath = [directoryPath stringByAppendingPathComponent:subdirName];
154             subdirDirectoryPath = [subdirDirectoryPath stringByAppendingPathComponent:[object.name substringFromIndex:subdirPrefixLength]];
155             
156             BOOL directoryIsDirectory;
157             BOOL directoryExists = [defaultManager fileExistsAtPath:subdirDirectoryPath isDirectory:&directoryIsDirectory];
158             NSError *error = nil;
159             if (!directoryExists) {
160                 [defaultManager createDirectoryAtPath:subdirDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error];
161                 if (error) {
162                     NSLog(@"Cannot create directory at '%@': %@", subdirDirectoryPath, error);
163                     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
164                     [alert setMessageText:@"Create Directory Error"];
165                     [alert setInformativeText:[NSString stringWithFormat:@"Cannot create directory at '%@': %@", 
166                                                subdirDirectoryPath, error]];
167                     [alert addButtonWithTitle:@"OK"];
168                     [alert runModal];
169                 }
170             } else if (!directoryIsDirectory) {
171                 [defaultManager removeItemAtPath:subdirDirectoryPath error:&error];
172                 if (error) {
173                     NSLog(@"Cannot remove existing file at '%@': %@", subdirDirectoryPath, error);
174                     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
175                     [alert setMessageText:@"Remove File Error"];
176                     [alert setInformativeText:[NSString stringWithFormat:@"Cannot remove existing file at '%@': %@", 
177                                                subdirDirectoryPath, error]];
178                     [alert addButtonWithTitle:@"OK"];
179                     [alert runModal];
180                 }
181             }
182         } else {
183             NSString *fileName = [object.name lastPathComponent];
184             if([object.name hasSuffix:@"/"])
185                 fileName = [fileName stringByAppendingString:@"/"];
186             
187             NSString *objectDirectoryPath = [directoryPath stringByAppendingPathComponent:subdirName];
188             objectDirectoryPath = [objectDirectoryPath stringByAppendingPathComponent:[object.name substringWithRange:NSMakeRange(subdirPrefixLength, [object.name length] - subdirPrefixLength - [fileName length])]];
189             
190             [objectRequests addObject:[self objectDataRequestWithContainerName:containerName 
191                                                                     objectName:object.name 
192                                                                    toDirectory:objectDirectoryPath 
193                                                                  checkIfExists:NO]];
194         }
195     }
196     
197     return objectRequests;
198 }
199
200 #pragma mark -
201 #pragma mark Upload
202
203 + (ASIPithosObjectRequest *)writeObjectDataRequestWithContainerName:(NSString *)containerName
204                                                          objectName:(NSString *)objectName
205                                                         contentType:(NSString *)contentType 
206                                                           blockSize:(NSUInteger)blockSize 
207                                                           blockHash:(NSString *)blockHash 
208                                                             forFile:(NSString *)filePath 
209                                                       checkIfExists:(BOOL)ifExists 
210                                                              hashes:(NSArray **)hashes {
211     if (ifExists) {
212         NSString *responseString;
213         NSError *error;
214         BOOL isDirectory;
215         BOOL objectExists = [self objectExistsAtContainerName:containerName objectName:objectName 
216                                                responseString:&responseString error:&error isDirectory:&isDirectory];
217         NSAlert *alert;
218         if (error) {
219             alert = [[[NSAlert alloc] init] autorelease];
220             [alert setMessageText:@"HTTP Request Error"];
221             [alert setInformativeText:[NSString stringWithFormat:@"HTTP Request Error: %@", error]];
222             [alert setInformativeText:[NSString stringWithFormat:@"HTTP Request Error: %@\nResponse String: %@", 
223                                        error, responseString]];
224             [alert addButtonWithTitle:@"OK"];
225             [alert runModal];
226             return nil;
227         } else if (objectExists) {
228             alert = [[[NSAlert alloc] init] autorelease];
229             if (isDirectory) {
230                 [alert setMessageText:@"Directory Exists"];
231                 [alert setInformativeText:[NSString stringWithFormat:@"A directory with path '%@' in the container '%@' already exists, do you want to replace it?", objectName, containerName]];
232             } else {
233                 [alert setMessageText:@"Object Exists"];
234                 [alert setInformativeText:[NSString stringWithFormat:@"An object with path '%@' in the container '%@' already exists, do you want to replace it?", objectName, containerName]];
235             }
236             [alert addButtonWithTitle:@"OK"];
237             [alert addButtonWithTitle:@"Cancel"];
238             NSInteger choice = [alert runModal];
239             if (choice == NSAlertSecondButtonReturn)
240                 return nil;
241         }
242     }
243     
244     if (*hashes == nil)
245         *hashes = [HashMapHash calculateObjectHashMap:filePath withBlockHash:blockHash andBlockSize:blockSize];
246     if (*hashes == nil)
247         return nil;
248     NSUInteger bytes = [self bytesOfFile:filePath];
249     ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:containerName 
250                                                                                                  objectName:objectName 
251                                                                                                 contentType:contentType 
252                                                                                             contentEncoding:nil 
253                                                                                          contentDisposition:nil 
254                                                                                                    manifest:nil 
255                                                                                                     sharing:nil 
256                                                                                                    isPublic:ASIPithosObjectRequestPublicIgnore 
257                                                                                                    metadata:nil
258                                                                                                   blockSize:blockSize
259                                                                                                   blockHash:blockHash 
260                                                                                                      hashes:*hashes 
261                                                                                                       bytes:bytes];
262     return objectRequest;
263 }
264
265 + (ASIPithosObjectRequest *)updateObjectDataRequestWithContainerName:(NSString *)containerName 
266                                                           objectName:(NSString *)objectName 
267                                                          contentType:(NSString *)contentType 
268                                                            blockSize:(NSUInteger)blockSize 
269                                                              forFile:(NSString *)filePath 
270                                                               hashes:(NSArray *)hashes 
271                                                missingHashesResponse:(NSString *)missingHashesResponse {
272     NSArray *responseLines = [missingHashesResponse componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
273     NSMutableIndexSet *missingBlocks = [NSMutableIndexSet indexSet];
274     for (NSString *line in responseLines) {
275         if (![line length])
276             break;
277         NSUInteger missingBlock = [hashes indexOfObject:line];
278         if (missingBlock != -1)
279             [missingBlocks addIndex:missingBlock];
280     }
281     
282     NSFileManager *defaultManager = [NSFileManager defaultManager];
283     NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
284     
285     // http://cocoawithlove.com/2009/07/temporary-files-and-folders-in-cocoa.html
286     NSString *tempFileTemplate = NSTemporaryDirectory();
287     if (tempFileTemplate == nil)
288         tempFileTemplate = @"/tmp";
289     tempFileTemplate = [tempFileTemplate stringByAppendingPathComponent:@"pithos-macos.XXXXXX"];
290     const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation];
291     char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1);
292     strcpy(tempFileNameCString, tempFileTemplateCString);
293     int fileDescriptor = mkstemp(tempFileNameCString);
294     NSString *tempFilePath = [defaultManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)];
295     if (fileDescriptor == -1) {
296         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
297         [alert setMessageText:@"Create Temporary File Error"];
298         [alert setInformativeText:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", tempFilePath]];
299         [alert addButtonWithTitle:@"OK"];
300         [alert runModal];
301         return nil;
302     }
303     free(tempFileNameCString);
304     NSFileHandle *tempFileHandle = [[NSFileHandle alloc] initWithFileDescriptor:fileDescriptor closeOnDealloc:YES];
305
306     [missingBlocks enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){
307         [fileHandle seekToFileOffset:(idx*blockSize)];
308         [tempFileHandle writeData:[fileHandle readDataOfLength:blockSize]];
309     }];
310     [tempFileHandle closeFile];
311
312     return [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:containerName 
313                                                                 objectName:objectName 
314                                                                       eTag:nil
315                                                                contentType:contentType 
316                                                            contentEncoding:nil 
317                                                         contentDisposition:nil 
318                                                                   manifest:nil 
319                                                                    sharing:nil 
320                                                                   isPublic:ASIPithosObjectRequestPublicIgnore 
321                                                                   metadata:nil 
322                                                                       file:tempFilePath];
323 }
324
325 + (NSArray *)writeObjectDataRequestsWithContainerName:(NSString *)containerName
326                                            objectName:(NSString *)objectName
327                                             blockSize:(NSUInteger)blockSize 
328                                             blockHash:(NSString *)blockHash 
329                                          forDirectory:(NSString *)directoryPath 
330                                         checkIfExists:(BOOL)ifExists 
331                                           objectNames:(NSMutableArray **)objectNames
332                                          contentTypes:(NSMutableArray **)contentTypes
333                                             filePaths:(NSMutableArray **)filePaths 
334                                          hashesArrays:(NSMutableArray **)hashesArrays {
335     if (ifExists) {
336         NSString *responseString;
337         NSError *error;
338         BOOL isDirectory;
339         BOOL objectExists = [self objectExistsAtContainerName:containerName objectName:objectName 
340                                                responseString:&responseString error:&error isDirectory:&isDirectory];
341         NSAlert *alert;
342         if (error) {
343             alert = [[[NSAlert alloc] init] autorelease];
344             [alert setMessageText:@"HTTP Request Error"];
345             [alert setInformativeText:[NSString stringWithFormat:@"HTTP Request Error: %@\nResponse String: %@", 
346                                        error, responseString]];
347             [alert addButtonWithTitle:@"OK"];
348             [alert runModal];
349             return nil;
350         } else if (objectExists && !isDirectory) {
351             alert = [[[NSAlert alloc] init] autorelease];
352             [alert setMessageText:@"Object Exists"];
353             [alert setInformativeText:[NSString stringWithFormat:@"An object with path '%@' in the container '%@' already exists, do you want to replace it?", objectName, containerName]];
354             [alert addButtonWithTitle:@"OK"];
355             [alert addButtonWithTitle:@"Cancel"];
356             NSInteger choice = [alert runModal];
357             if (choice == NSAlertSecondButtonReturn)
358                 return nil;
359         }
360     }
361
362     NSFileManager *defaultManager = [NSFileManager defaultManager];
363     NSError *error = nil;
364     NSArray *subPaths = [defaultManager subpathsOfDirectoryAtPath:directoryPath error:&error];
365     if (error) {
366         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
367         [alert setMessageText:@"Directory Read Error"];
368         [alert setInformativeText:[NSString stringWithFormat:@"Cannot read contents of directory '%@': %@", 
369                                    [directoryPath lastPathComponent], error]];
370         [alert addButtonWithTitle:@"OK"];
371         [alert runModal];
372         return nil;
373     }
374     NSMutableArray *objectRequests = [NSMutableArray arrayWithCapacity:[subPaths count]];
375     *objectNames = [NSMutableArray arrayWithCapacity:[subPaths count]];
376     *contentTypes = [NSMutableArray arrayWithCapacity:[subPaths count]];
377     *filePaths = [NSMutableArray arrayWithCapacity:[subPaths count]];
378     *hashesArrays = [NSMutableArray arrayWithCapacity:[subPaths count]];
379     BOOL isDirectory;
380     NSString *subObjectName;
381     NSArray *hashes;
382     NSUInteger bytes;
383     NSString *contentType;
384     NSString *filePath;
385     for (NSString *objectNameSuffix in subPaths) {
386         filePath = [directoryPath stringByAppendingPathComponent:objectNameSuffix];
387         if ([defaultManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
388             if (!isDirectory) {
389                 hashes = [HashMapHash calculateObjectHashMap:filePath withBlockHash:blockHash andBlockSize:blockSize];
390                 if (hashes) {
391                     subObjectName = [objectName stringByAppendingPathComponent:objectNameSuffix];
392                     bytes = [self bytesOfFile:filePath];
393                     error = nil;
394                     contentType = [self contentTypeOfFile:filePath error:&error];
395                     if (contentType == nil)
396                         contentType = @"application/binary";
397                     if (error)
398                         NSLog(@"contentType detection error: %@", error);
399                     [objectRequests addObject:[ASIPithosObjectRequest writeObjectDataRequestWithContainerName:containerName 
400                                                                                                    objectName:subObjectName 
401                                                                                                   contentType:contentType 
402                                                                                               contentEncoding:nil 
403                                                                                            contentDisposition:nil 
404                                                                                                      manifest:nil 
405                                                                                                       sharing:nil 
406                                                                                                      isPublic:ASIPithosObjectRequestPublicIgnore 
407                                                                                                      metadata:nil
408                                                                                                     blockSize:blockSize
409                                                                                                     blockHash:blockHash 
410                                                                                                        hashes:hashes 
411                                                                                                         bytes:bytes]];
412                     [*objectNames addObject:subObjectName];
413                     [*contentTypes addObject:contentType];
414                     [*filePaths addObject:filePath];
415                     [*hashesArrays addObject:hashes];
416                 }
417                 
418             }//XXX else
419         }
420     }
421     
422     return objectRequests;
423 }
424
425 #pragma mark -
426 #pragma mark Helper Methods
427
428 + (NSUInteger)bytesOfFile:(NSString *)filePath {
429     NSFileManager *defaultManager = [NSFileManager defaultManager];
430     NSDictionary *attributes = [defaultManager attributesOfItemAtPath:filePath error:nil];
431     return [[attributes objectForKey:NSFileSize] intValue];
432 }
433
434 + (NSString *)contentTypeOfFile:(NSString *)filePath error:(NSError **)error {
435     NSURLResponse *response = nil;
436     [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:filePath] 
437                                                              cachePolicy:NSURLCacheStorageNotAllowed 
438                                                          timeoutInterval:.1] 
439                           returningResponse:&response 
440                                       error:error];
441     return [response MIMEType];
442 }
443
444 + (BOOL)objectExistsAtContainerName:(NSString *)containerName objectName:(NSString *)objectName 
445                      responseString:(NSString **)responseString error:(NSError **)error isDirectory:(BOOL *)isDirectory {
446     ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectMetadataRequestWithContainerName:containerName 
447                                                                                                 objectName:objectName];
448     [objectRequest startSynchronous];
449     *responseString = [objectRequest responseString];
450     *error = [objectRequest error];
451     if (*error) {
452         return NO;
453     } else if (objectRequest.responseStatusCode == 200) {
454         *isDirectory = [[objectRequest contentType] isEqualToString:@"application/directory"];
455         return YES;
456     }
457     return NO;
458 }
459
460 + (int)deleteObjectAtContainerName:(NSString *)containerName objectName:(NSString *)objectName 
461              responseStatusMessage:(NSString **)responseStatusMessage error:(NSError **)error {
462     ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteContainerRequestWithContainerName:containerName 
463                                                                                                  objectName:objectName];
464     [objectRequest startSynchronous];
465     *error = [objectRequest error];
466     *responseStatusMessage = [NSString stringWithString:objectRequest.responseStatusMessage];
467     return objectRequest.responseStatusCode;
468 }
469
470 #pragma mark -
471 #pragma mark Alerts
472
473 + (NSInteger)httpRequestErrorAlertWithRequest:(ASIPithosRequest *)request {
474     NSString *message = [NSString stringWithFormat:
475                          @"HTTP request error: %@\n%@ URL: %@\nResponse String: %@", 
476                          [request error], 
477                          request.requestMethod, 
478                          request.url, 
479                          [request responseString]];
480     NSLog(@"%@", message);
481     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
482     [alert setMessageText:@"HTTP Request Error"];
483     [alert setInformativeText:message];
484     [alert addButtonWithTitle:@"OK"];
485     return [alert runModal];
486 }
487
488 + (NSInteger)unexpectedResponseStatusAlertWithRequest:(ASIPithosRequest *)request {
489     NSString *message = [NSString stringWithFormat:
490                          @"Unexpected response status %d: %@\n%@ URL: %@\nResponse String: %@", 
491                          request.responseStatusCode, 
492                          request.responseStatusMessage, 
493                          request.requestMethod, 
494                          request.url, 
495                          [request responseString]];
496     NSLog(@"%@", message);
497     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
498     [alert setMessageText:@"Unexpected Response Status"];
499     [alert setInformativeText:message];
500     [alert addButtonWithTitle:@"OK"];
501     return [alert runModal];
502 }
503
504 @end