Statistics
| Branch: | Revision:

root / asi-http-request-with-pithos / Classes / ASIDownloadCache.m @ be116d22

History | View | Annotate | Download (16.8 kB)

1
//
2
//  ASIDownloadCache.m
3
//  Part of ASIHTTPRequest -> http://allseeing-i.com/ASIHTTPRequest
4
//
5
//  Created by Ben Copsey on 01/05/2010.
6
//  Copyright 2010 All-Seeing Interactive. All rights reserved.
7
//
8

    
9
#import "ASIDownloadCache.h"
10
#import "ASIHTTPRequest.h"
11
#import <CommonCrypto/CommonHMAC.h>
12

    
13
static ASIDownloadCache *sharedCache = nil;
14

    
15
static NSString *sessionCacheFolder = @"SessionStore";
16
static NSString *permanentCacheFolder = @"PermanentStore";
17

    
18
@interface ASIDownloadCache ()
19
+ (NSString *)keyForURL:(NSURL *)url;
20
- (NSString *)pathToFile:(NSString *)file;
21
@end
22

    
23
@implementation ASIDownloadCache
24

    
25
- (id)init
26
{
27
	self = [super init];
28
	[self setShouldRespectCacheControlHeaders:YES];
29
	[self setDefaultCachePolicy:ASIUseDefaultCachePolicy];
30
	[self setAccessLock:[[[NSRecursiveLock alloc] init] autorelease]];
31
	return self;
32
}
33

    
34
+ (id)sharedCache
35
{
36
	if (!sharedCache) {
37
		@synchronized(self) {
38
			if (!sharedCache) {
39
				sharedCache = [[self alloc] init];
40
				[sharedCache setStoragePath:[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"ASIHTTPRequestCache"]];
41
			}
42
		}
43
	}
44
	return sharedCache;
45
}
46

    
47
- (void)dealloc
48
{
49
	[storagePath release];
50
	[accessLock release];
51
	[super dealloc];
52
}
53

    
54
- (NSString *)storagePath
55
{
56
	[[self accessLock] lock];
57
	NSString *p = [[storagePath retain] autorelease];
58
	[[self accessLock] unlock];
59
	return p;
60
}
61

    
62

    
63
- (void)setStoragePath:(NSString *)path
64
{
65
	[[self accessLock] lock];
66
	[self clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy];
67
	[storagePath release];
68
	storagePath = [path retain];
69

    
70
	NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
71

    
72
	BOOL isDirectory = NO;
73
	NSArray *directories = [NSArray arrayWithObjects:path,[path stringByAppendingPathComponent:sessionCacheFolder],[path stringByAppendingPathComponent:permanentCacheFolder],nil];
74
	for (NSString *directory in directories) {
75
		BOOL exists = [fileManager fileExistsAtPath:directory isDirectory:&isDirectory];
76
		if (exists && !isDirectory) {
77
			[[self accessLock] unlock];
78
			[NSException raise:@"FileExistsAtCachePath" format:@"Cannot create a directory for the cache at '%@', because a file already exists",directory];
79
		} else if (!exists) {
80
			[fileManager createDirectoryAtPath:directory withIntermediateDirectories:NO attributes:nil error:nil];
81
			if (![fileManager fileExistsAtPath:directory]) {
82
				[[self accessLock] unlock];
83
				[NSException raise:@"FailedToCreateCacheDirectory" format:@"Failed to create a directory for the cache at '%@'",directory];
84
			}
85
		}
86
	}
87
	[self clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy];
88
	[[self accessLock] unlock];
89
}
90

    
91
- (void)updateExpiryForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge
92
{
93
	NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request];
94
	NSMutableDictionary *cachedHeaders = [NSMutableDictionary dictionaryWithContentsOfFile:headerPath];
95
	if (!cachedHeaders) {
96
		return;
97
	}
98
	NSDate *expires = [self expiryDateForRequest:request maxAge:maxAge];
99
	if (!expires) {
100
		return;
101
	}
102
	[cachedHeaders setObject:[NSNumber numberWithDouble:[expires timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"];
103
	[cachedHeaders writeToFile:headerPath atomically:NO];
104
}
105

    
106
- (NSDate *)expiryDateForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge
107
{
108
	NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionaryWithDictionary:[request responseHeaders]];
109

    
110
	// If we weren't given a custom max-age, lets look for one in the response headers
111
	if (!maxAge) {
112
		NSString *cacheControl = [[responseHeaders objectForKey:@"Cache-Control"] lowercaseString];
113
		if (cacheControl) {
114
			NSScanner *scanner = [NSScanner scannerWithString:cacheControl];
115
			[scanner scanUpToString:@"max-age" intoString:NULL];
116
			if ([scanner scanString:@"max-age" intoString:NULL]) {
117
				[scanner scanString:@"=" intoString:NULL];
118
				[scanner scanDouble:&maxAge];
119
			}
120
		}
121
	}
122

    
123
	// RFC 2612 says max-age must override any Expires header
124
	if (maxAge) {
125
		return [[NSDate date] addTimeInterval:maxAge];
126
	} else {
127
		NSString *expires = [responseHeaders objectForKey:@"Expires"];
128
		if (expires) {
129
			return [ASIHTTPRequest dateFromRFC1123String:expires];
130
		}
131
	}
132
	return nil;
133
}
134

    
135
- (void)storeResponseForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge
136
{
137
	[[self accessLock] lock];
138

    
139
	if ([request error] || ![request responseHeaders] || ([request cachePolicy] & ASIDoNotWriteToCacheCachePolicy)) {
140
		[[self accessLock] unlock];
141
		return;
142
	}
143

    
144
	// We only cache 200/OK or redirect reponses (redirect responses are cached so the cache works better with no internet connection)
145
	int responseCode = [request responseStatusCode];
146
	if (responseCode != 200 && responseCode != 301 && responseCode != 302 && responseCode != 303 && responseCode != 307) {
147
		[[self accessLock] unlock];
148
		return;
149
	}
150

    
151
	if ([self shouldRespectCacheControlHeaders] && ![[self class] serverAllowsResponseCachingForRequest:request]) {
152
		[[self accessLock] unlock];
153
		return;
154
	}
155

    
156
	NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request];
157
	NSString *dataPath = [self pathToStoreCachedResponseDataForRequest:request];
158

    
159
	NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionaryWithDictionary:[request responseHeaders]];
160
	if ([request isResponseCompressed]) {
161
		[responseHeaders removeObjectForKey:@"Content-Encoding"];
162
	}
163

    
164
	// Create a special 'X-ASIHTTPRequest-Expires' header
165
	// This is what we use for deciding if cached data is current, rather than parsing the expires / max-age headers individually each time
166
	// We store this as a timestamp to make reading it easier as NSDateFormatter is quite expensive
167

    
168
	NSDate *expires = [self expiryDateForRequest:request maxAge:maxAge];
169
	if (expires) {
170
		[responseHeaders setObject:[NSNumber numberWithDouble:[expires timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"];
171
	}
172

    
173
	// Store the response code in a custom header so we can reuse it later
174

    
175
	// We'll change 304/Not Modified to 200/OK because this is likely to be us updating the cached headers with a conditional GET
176
	int statusCode = [request responseStatusCode];
177
	if (statusCode == 304) {
178
		statusCode = 200;
179
	}
180
	[responseHeaders setObject:[NSNumber numberWithInt:statusCode] forKey:@"X-ASIHTTPRequest-Response-Status-Code"];
181

    
182
	[responseHeaders writeToFile:headerPath atomically:NO];
183

    
184
	if ([request responseData]) {
185
		[[request responseData] writeToFile:dataPath atomically:NO];
186
	} else if ([request downloadDestinationPath] && ![[request downloadDestinationPath] isEqualToString:dataPath]) {
187
		NSError *error = nil;
188
		[[[[NSFileManager alloc] init] autorelease] copyItemAtPath:[request downloadDestinationPath] toPath:dataPath error:&error];
189
	}
190
	[[self accessLock] unlock];
191
}
192

    
193
- (NSDictionary *)cachedResponseHeadersForURL:(NSURL *)url
194
{
195
	NSString *path = [self pathToCachedResponseHeadersForURL:url];
196
	if (path) {
197
		return [NSDictionary dictionaryWithContentsOfFile:path];
198
	}
199
	return nil;
200
}
201

    
202
- (NSData *)cachedResponseDataForURL:(NSURL *)url
203
{
204
	NSString *path = [self pathToCachedResponseDataForURL:url];
205
	if (path) {
206
		return [NSData dataWithContentsOfFile:path];
207
	}
208
	return nil;
209
}
210

    
211
- (NSString *)pathToCachedResponseDataForURL:(NSURL *)url
212
{
213
	// Grab the file extension, if there is one. We do this so we can save the cached response with the same file extension - this is important if you want to display locally cached data in a web view 
214
	NSString *extension = [[url path] pathExtension];
215
	if (![extension length]) {
216
		extension = @"html";
217
	}
218
	return [self pathToFile:[[[self class] keyForURL:url] stringByAppendingPathExtension:extension]];
219
}
220

    
221
- (NSString *)pathToCachedResponseHeadersForURL:(NSURL *)url
222
{
223
	return [self pathToFile:[[[self class] keyForURL:url] stringByAppendingPathExtension:@"cachedheaders"]];
224
}
225

    
226
- (NSString *)pathToFile:(NSString *)file
227
{
228
	[[self accessLock] lock];
229
	if (![self storagePath]) {
230
		[[self accessLock] unlock];
231
		return nil;
232
	}
233

    
234
	NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
235

    
236
	// Look in the session store
237
	NSString *dataPath = [[[self storagePath] stringByAppendingPathComponent:sessionCacheFolder] stringByAppendingPathComponent:file];
238
	if ([fileManager fileExistsAtPath:dataPath]) {
239
		[[self accessLock] unlock];
240
		return dataPath;
241
	}
242
	// Look in the permanent store
243
	dataPath = [[[self storagePath] stringByAppendingPathComponent:permanentCacheFolder] stringByAppendingPathComponent:file];
244
	if ([fileManager fileExistsAtPath:dataPath]) {
245
		[[self accessLock] unlock];
246
		return dataPath;
247
	}
248
	[[self accessLock] unlock];
249
	return nil;
250
}
251

    
252

    
253
- (NSString *)pathToStoreCachedResponseDataForRequest:(ASIHTTPRequest *)request
254
{
255
	[[self accessLock] lock];
256
	if (![self storagePath]) {
257
		[[self accessLock] unlock];
258
		return nil;
259
	}
260

    
261
	NSString *path = [[self storagePath] stringByAppendingPathComponent:([request cacheStoragePolicy] == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)];
262

    
263
	// Grab the file extension, if there is one. We do this so we can save the cached response with the same file extension - this is important if you want to display locally cached data in a web view 
264
	NSString *extension = [[[request url] path] pathExtension];
265
	if (![extension length]) {
266
		extension = @"html";
267
	}
268
	path =  [path stringByAppendingPathComponent:[[[self class] keyForURL:[request url]] stringByAppendingPathExtension:extension]];
269
	[[self accessLock] unlock];
270
	return path;
271
}
272

    
273
- (NSString *)pathToStoreCachedResponseHeadersForRequest:(ASIHTTPRequest *)request
274
{
275
	[[self accessLock] lock];
276
	if (![self storagePath]) {
277
		[[self accessLock] unlock];
278
		return nil;
279
	}
280
	NSString *path = [[self storagePath] stringByAppendingPathComponent:([request cacheStoragePolicy] == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)];
281
	path =  [path stringByAppendingPathComponent:[[[self class] keyForURL:[request url]] stringByAppendingPathExtension:@"cachedheaders"]];
282
	[[self accessLock] unlock];
283
	return path;
284
}
285

    
286
- (void)removeCachedDataForURL:(NSURL *)url
287
{
288
	[[self accessLock] lock];
289
	if (![self storagePath]) {
290
		[[self accessLock] unlock];
291
		return;
292
	}
293
	NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
294

    
295
	NSString *path = [self pathToCachedResponseHeadersForURL:url];
296
	if (path) {
297
		[fileManager removeItemAtPath:path error:NULL];
298
	}
299

    
300
	path = [self pathToCachedResponseDataForURL:url];
301
	if (path) {
302
		[fileManager removeItemAtPath:path error:NULL];
303
	}
304
	[[self accessLock] unlock];
305
}
306

    
307
- (void)removeCachedDataForRequest:(ASIHTTPRequest *)request
308
{
309
	[self removeCachedDataForURL:[request url]];
310
}
311

    
312
- (BOOL)isCachedDataCurrentForRequest:(ASIHTTPRequest *)request
313
{
314
	[[self accessLock] lock];
315
	if (![self storagePath]) {
316
		[[self accessLock] unlock];
317
		return NO;
318
	}
319
	NSDictionary *cachedHeaders = [self cachedResponseHeadersForURL:[request url]];
320
	if (!cachedHeaders) {
321
		[[self accessLock] unlock];
322
		return NO;
323
	}
324
	NSString *dataPath = [self pathToCachedResponseDataForURL:[request url]];
325
	if (!dataPath) {
326
		[[self accessLock] unlock];
327
		return NO;
328
	}
329

    
330
	// New content is not different
331
	if ([request responseStatusCode] == 304) {
332
		[[self accessLock] unlock];
333
		return YES;
334
	}
335

    
336
	// If we already have response headers for this request, check to see if the new content is different
337
	// We check [request complete] so that we don't end up comparing response headers from a redirection with these
338
	if ([request responseHeaders] && [request complete]) {
339

    
340
		// If the Etag or Last-Modified date are different from the one we have, we'll have to fetch this resource again
341
		NSArray *headersToCompare = [NSArray arrayWithObjects:@"Etag",@"Last-Modified",nil];
342
		for (NSString *header in headersToCompare) {
343
			if (![[[request responseHeaders] objectForKey:header] isEqualToString:[cachedHeaders objectForKey:header]]) {
344
				[[self accessLock] unlock];
345
				return NO;
346
			}
347
		}
348
	}
349

    
350
	if ([self shouldRespectCacheControlHeaders]) {
351

    
352
		// Look for X-ASIHTTPRequest-Expires header to see if the content is out of date
353
		NSNumber *expires = [cachedHeaders objectForKey:@"X-ASIHTTPRequest-Expires"];
354
		if (expires) {
355
			if ([[NSDate dateWithTimeIntervalSince1970:[expires doubleValue]] timeIntervalSinceNow] >= 0) {
356
				[[self accessLock] unlock];
357
				return YES;
358
			}
359
		}
360

    
361
		// No explicit expiration time sent by the server
362
		[[self accessLock] unlock];
363
		return NO;
364
	}
365
	
366

    
367
	[[self accessLock] unlock];
368
	return YES;
369
}
370

    
371
- (ASICachePolicy)defaultCachePolicy
372
{
373
	[[self accessLock] lock];
374
	ASICachePolicy cp = defaultCachePolicy;
375
	[[self accessLock] unlock];
376
	return cp;
377
}
378

    
379

    
380
- (void)setDefaultCachePolicy:(ASICachePolicy)cachePolicy
381
{
382
	[[self accessLock] lock];
383
	if (!cachePolicy) {
384
		defaultCachePolicy = ASIAskServerIfModifiedWhenStaleCachePolicy;
385
	}  else {
386
		defaultCachePolicy = cachePolicy;	
387
	}
388
	[[self accessLock] unlock];
389
}
390

    
391
- (void)clearCachedResponsesForStoragePolicy:(ASICacheStoragePolicy)storagePolicy
392
{
393
	[[self accessLock] lock];
394
	if (![self storagePath]) {
395
		[[self accessLock] unlock];
396
		return;
397
	}
398
	NSString *path = [[self storagePath] stringByAppendingPathComponent:(storagePolicy == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)];
399

    
400
	NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
401

    
402
	BOOL isDirectory = NO;
403
	BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
404
	if (!exists || !isDirectory) {
405
		[[self accessLock] unlock];
406
		return;
407
	}
408
	NSError *error = nil;
409
	NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:path error:&error];
410
	if (error) {
411
		[[self accessLock] unlock];
412
		[NSException raise:@"FailedToTraverseCacheDirectory" format:@"Listing cache directory failed at path '%@'",path];	
413
	}
414
	for (NSString *file in cacheFiles) {
415
		[fileManager removeItemAtPath:[path stringByAppendingPathComponent:file] error:&error];
416
		if (error) {
417
			[[self accessLock] unlock];
418
			[NSException raise:@"FailedToRemoveCacheFile" format:@"Failed to remove cached data at path '%@'",path];
419
		}
420
	}
421
	[[self accessLock] unlock];
422
}
423

    
424
+ (BOOL)serverAllowsResponseCachingForRequest:(ASIHTTPRequest *)request
425
{
426
	NSString *cacheControl = [[[request responseHeaders] objectForKey:@"Cache-Control"] lowercaseString];
427
	if (cacheControl) {
428
		if ([cacheControl isEqualToString:@"no-cache"] || [cacheControl isEqualToString:@"no-store"]) {
429
			return NO;
430
		}
431
	}
432
	NSString *pragma = [[[request responseHeaders] objectForKey:@"Pragma"] lowercaseString];
433
	if (pragma) {
434
		if ([pragma isEqualToString:@"no-cache"]) {
435
			return NO;
436
		}
437
	}
438
	return YES;
439
}
440

    
441
+ (NSString *)keyForURL:(NSURL *)url
442
{
443
	NSString *urlString = [url absoluteString];
444
	if ([urlString length] == 0) {
445
		return nil;
446
	}
447

    
448
	// Strip trailing slashes so http://allseeing-i.com/ASIHTTPRequest/ is cached the same as http://allseeing-i.com/ASIHTTPRequest
449
	if ([[urlString substringFromIndex:[urlString length]-1] isEqualToString:@"/"]) {
450
		urlString = [urlString substringToIndex:[urlString length]-1];
451
	}
452

    
453
	// Borrowed from: http://stackoverflow.com/questions/652300/using-md5-hash-on-a-string-in-cocoa
454
	const char *cStr = [urlString UTF8String];
455
	unsigned char result[16];
456
	CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
457
	return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],result[8], result[9], result[10], result[11],result[12], result[13], result[14], result[15]]; 	
458
}
459

    
460
- (BOOL)canUseCachedDataForRequest:(ASIHTTPRequest *)request
461
{
462
	// Ensure the request is allowed to read from the cache
463
	if ([request cachePolicy] & ASIDoNotReadFromCacheCachePolicy) {
464
		return NO;
465

    
466
	// If we don't want to load the request whatever happens, always pretend we have cached data even if we don't
467
	} else if ([request cachePolicy] & ASIDontLoadCachePolicy) {
468
		return YES;
469
	}
470

    
471
	NSDictionary *headers = [self cachedResponseHeadersForURL:[request url]];
472
	if (!headers) {
473
		return NO;
474
	}
475
	NSString *dataPath = [self pathToCachedResponseDataForURL:[request url]];
476
	if (!dataPath) {
477
		return NO;
478
	}
479

    
480
	// If we get here, we have cached data
481

    
482
	// If we have cached data, we can use it
483
	if ([request cachePolicy] & ASIOnlyLoadIfNotCachedCachePolicy) {
484
		return YES;
485

    
486
	// If we want to fallback to the cache after an error
487
	} else if ([request complete] && [request cachePolicy] & ASIFallbackToCacheIfLoadFailsCachePolicy) {
488
		return YES;
489

    
490
	// If we have cached data that is current, we can use it
491
	} else if ([request cachePolicy] & ASIAskServerIfModifiedWhenStaleCachePolicy) {
492
		if ([self isCachedDataCurrentForRequest:request]) {
493
			return YES;
494
		}
495

    
496
	// If we've got headers from a conditional GET and the cached data is still current, we can use it
497
	} else if ([request cachePolicy] & ASIAskServerIfModifiedCachePolicy) {
498
		if (![request responseHeaders]) {
499
			return NO;
500
		} else if ([self isCachedDataCurrentForRequest:request]) {
501
			return YES;
502
		}
503
	}
504
	return NO;
505
}
506

    
507
@synthesize storagePath;
508
@synthesize defaultCachePolicy;
509
@synthesize accessLock;
510
@synthesize shouldRespectCacheControlHeaders;
511
@end